diff --git a/.gitea/workflows/apk.yml b/.gitea/workflows/apk.yml deleted file mode 100644 index 19e7429..0000000 --- a/.gitea/workflows/apk.yml +++ /dev/null @@ -1,53 +0,0 @@ -# Construye la APK y la deja publicada como borrador en las releases. -# -# NO se dispara sola con cada commit: la APK son mas de 100 MB y el backend tarda -# en empaquetarse. Se lanza a mano desde la interfaz de gitea, o al crear un tag. -# -# El runner necesita: JDK 17 con compilador, el SDK de Android (plataforma 35 y -# build-tools), node, zip y unzip. Un runner pelado NO vale. -# -# Secretos que hay que dar de alta en el repositorio: -# OASIS_KEYSTORE_B64 el almacen de claves en base64 -# OASIS_KEYSTORE_PASS su contraseña -# OASIS_KEY_ALIAS el alias (por defecto "oasis") -# -# El token para publicar lo pone gitea solo en GITEA_TOKEN. - -name: APK - -on: - workflow_dispatch: - push: - tags: - - 'v*' - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # release.sh cuenta commits para el versionCode - - - name: Restaurar el almacen de claves - run: | - echo "${{ secrets.OASIS_KEYSTORE_B64 }}" | base64 -d > /tmp/oasis.jks - chmod 600 /tmp/oasis.jks - - - name: Construir, verificar y publicar - env: - JAVA_HOME: /usr/lib/jvm/temurin-17-jdk-amd64 - ANDROID_HOME: /opt/android-sdk - OASIS_KEYSTORE: /tmp/oasis.jks - OASIS_KEYSTORE_PASS: ${{ secrets.OASIS_KEYSTORE_PASS }} - OASIS_KEY_ALIAS: ${{ secrets.OASIS_KEY_ALIAS }} - OASIS_GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - OASIS_GITEA_URL: ${{ github.server_url }} - OASIS_GITEA_REPO: ${{ github.repository }} - run: | - cd android - ./scripts/release.sh --release --publish - - - name: Borrar el almacen de claves - if: always() - run: rm -f /tmp/oasis.jks diff --git a/README.md b/README.md index 42adab0..528aea2 100644 --- a/README.md +++ b/README.md @@ -1,184 +1,328 @@ -# OASIS_MOBILE — rama de desarrollo experimental +# Oasis -> ## ⚠ ESTO NO ES OASIS OFICIAL -> -> Esta es una **rama de desarrollo experimental** mantenida por el hacklab, **no -> afiliada** al proyecto original. -> -> ### El repositorio oficial de Oasis es el de epsylon: https://github.com/epsylon/oasis -> -> Si quieres **usar Oasis de verdad, instala el oficial**. Lo que hay aquí puede -> romperse, cambiar de forma incompatible o desaparecer sin aviso. No está auditado y -> no tiene garantía de ninguna clase. -> -> **Los fallos de esta rama se reportan aquí, nunca a epsylon.** +Oasis is a **libre, open-source, encrypted, peer-to-peer, distributed (not decentralized!) & federated**... project networking application +that helps you follow interesting content and discover new ones. -Versión base: **Oasis 0.9.5** · Rama: `PRUEBAS` · -[Documentación completa](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki) · -[Versión de escritorio](https://gitea.laenre.net/hacklab/OASIS_LINUX) + ![SNH](https://solarnethub.com/git/snh-oasis-logo3.jpg "SolarNET.HuB") -![Las novedades](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/00-novedades.png) +Oasis redefines what it means to be connected in the modern world, giving people +the ability to control their online presence and interactions without the need for centralized institutions. ---- +---------- -## Lo que añade +## Frontend: -| | Oasis oficial | Esta rama | -|---|---|---| -| **Salas efímeras** | — | Karvan: mensajes que solo viven en memoria y se autodestruyen | -| **Videollamadas** | — | Audio y vídeo por WebRTC, sin servidores de terceros | -| **Interfaz de móvil** | El menú de escritorio en una pantalla pequeña | Panal de hexágonos, filtros y barra de accesos rápidos | -| **Varias identidades** | Una por instalación | Varias en el mismo móvil, con aviso si dos comparten clave | -| **Respuesta citada** | — | Responder a un mensaje concreto dentro de un chat | -| **Envoltorio Android** | APK sin permisos de cámara ni micrófono | Proyecto Gradle propio: permisos, subida de ficheros, servicio que sobrevive | -| **Metadatos de las fotos** | Se quitan solo si está `sharp`; en Android **nunca** | Se quitan siempre: GPS, marca, modelo, número de serie | -| **Servidor STUN** | Uno de Google, por defecto | Ninguno. TURN propio si lo configuras | +Main features of the Oasis interface are: -Los nombres de las categorías, los módulos y el resto de la aplicación son los de -epsylon, sin tocar. + + Data manipulation is not permitted due to the use of BLOCKCHAIN technology. + + No browser JavaScript. Just pure HTML+CSS. A really secure frontend!. + + Use your favorite web browser to read and write messages to the people you care about. + + Strong cryptography in every single point of the network. + + You are the center of your own distributed network. Online or offline, it works anywhere that you are. + + Initial identities are randomnly generated (no username or password required). + + No personal profile generated (no questions about gender, age, location, etc …). + + Automatic exif stripping (such as GPS coordinates) on images for better privacy. + + No email or associated mobile phone required. + + Automatic updates with new functionalities. + + ![SNH](https://solarnethub.com/git/snh-oasis-settings.png "SolarNET.HuB") + +But it has others features that are also really interesting, for example: ---- + + Support for multiple languages. + + ![SNH](https://solarnethub.com/git/snh-oasis-languages.png "SolarNET.HuB") -## Qué es Oasis, en cuatro dibujos + + Modularity to set your own environment. + + ![SNH](https://solarnethub.com/git/snh-oasis-modules.png "SolarNET.HuB") + + + Support for multiple themes (including Mobile Theme). + + ![SNH](https://solarnethub.com/git/snh-clear-theme.png "SolarNET.HuB") + ![SNH](https://solarnethub.com/git/snh-purple-theme.png "SolarNET.HuB") + ![SNH](https://solarnethub.com/git/snh-matrix-theme.png "SolarNET.HuB") + +And some other nice modules, like for example: + + + A complex Reddit-styled forum system. + + ![SNH](https://solarnethub.com/git/snh-forum.png "SolarNET.HuB") + + + Or a collaborative -offline- maps system. + + ![SNH](https://solarnethub.com/git/snh-maps.jpeg "SolarNET.HuB") + +And much more, that we invite you to discover by yourself ;-) -Una red social **sin servidor**: publicas en tu propio equipo y los demás se traen una -copia. No hay cuenta, no hay contraseña y nadie puede borrarte. + ![SNH](https://solarnethub.com/git/snh-games.jpeg "SolarNET.HuB") + +---------- -![Qué es Oasis](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/01-red.png) +## Modules: -Los mensajes viajan **de vecino en vecino**, así que llegan a gente con la que nunca te -has conectado. Por eso funciona incluso sin internet, entre dos móviles en la misma wifi. +Oasis is TRULY MODULAR. Here's a list of what comes deployed with the "core". -![Cómo llega un mensaje](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/02-gossip.png) + + Agenda: Module to manage all your assigned items. + + AI: Module to talk with a LLM called '42'. + + AINav: Module for natural-language queries about the network's content. + + Audios: Module to discover and manage audios. + + Banking: Module to determine the real value of ECOIN and distribute a UBI using the common treasury. + + BlockExplorer: Module to navigate the blockchain. + + Bookmarks: Module to discover and manage bookmarks. + + Calendars: Module to discover and manage calendars. + + Chats: Module to discover and manage encrypted chats. + + Cipher: Module to encrypt and decrypt your text symmetrically (using a shared password). + + Courts: Module to resolve conflicts and emit veredicts. + + Documents: Module to discover and manage documents. + + Events: Module to discover and manage events. + + Favorites: Module to manage your favorite content. + + Fediverse: Manage your other fediverse accounts, including sending and receiving content. + + Feed: Module to discover and share short-texts (feeds). + + Forums: Module to discover and manage forums. + + Games: Module to play and share your scores in various mini-games. + + Governance: Module to discover and manage votes. + + Graphos: Module to explore the network as an interactive map of peers. + + Images: Module to discover and manage images. + + Invites: Module to manage and apply invite codes. + + Jobs: Module to discover and manage jobs. + + Legacy: Module to manage your secret (private key) quickly and securely. + + Latest: Module to receive the most recent posts and discussions. + + L.A.R.P.: Module for a live-action role-playing layer with 9 houses. + + Logs: Module to record (via AI assistant) your experiences. + + Maps: Module to manage and share offline maps. + + Market: Module to exchange goods or services. + + Melody: Module to generate and share the "sound" of your blockchain. + + Multiverse: Module to receive content from other federated peers. + + Opinions: Module to discover and vote on opinions. + + Pads: Module to manage collaborative encrypted text editors. + + Parliament: Module to elect governments and vote on laws. + + Pixelia: Module to draw on a collaborative grid. + + Projects: Module to explore, crowd-funding and manage projects. + + Popular: Module to receive posts that are trending, most viewed, or most commented on. + + Reports: Module to manage and track reports related to issues, bugs, abuses, and content warnings. + + Shops: Module to manage and discover shops. + + Summaries: Module to receive summaries of long discussions or posts. + + Tags: Module to discover and explore taxonomy patterns (tags). + + Tasks: Module to discover and manage tasks. + + Threads: Module to receive conversations grouped by topic or question. + + Topics: Module to receive discussion categories based on shared interests. + + Torrents: Module to explore and manage torrents. + + Transfers: Module to discover and manage smart-contracts (transfers). + + Trending: Module to explore the most popular content. + + Tribes: Module to explore or create tribes (groups). + + Videos: Module to discover and manage videos. + + Wallet: Module to manage your digital assets (ECOin). -Un **pub** es solo un nodo que no se apaga nunca. No es un servidor con tus datos: no -guarda tu identidad, no decide qué ves y no puede borrarte. +Both the codebase and the inhabitants can generate new modules. -![Qué es un pub](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/04-pub.png) +---------- -Todo lo que publicas se encadena y va **firmado con tu clave**. Nadie puede colar un -mensaje en tu nombre ni cambiar lo que dijiste. +## C-AI (collective artificial intelligence) -![Tu registro](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/14-registro.png) +Oasis contains its own AI model called "42". -> La explicación completa —los saltos, las tribus, el LARP, los módulos— está en -> **[Entender Oasis en diez minutos](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Entender)**. +The main idea behind this implementation is to enable distributed learning generated through the collective action of many individuals, with the goal of redistributing the necessary processing load, as well as the ecological footprint and corporate bias. ---- + ![SNH](https://solarnethub.com/git/oasis-ai-example2.png "SolarNET.HuB") -## Karvan: salas que se borran solas +Our AI is trained with content from the OASIS network and its purpose is to take action and obtain answers to individual, but also global, problems. -![Cómo funciona Karvan](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/11-karvan.png) + + https://wiki.solarnethub.com/socialnet/ai -Lo que se dice dentro **no toca el disco**, no entra en tu registro y no se replica a -nadie. Dos relojes corren a la vez —inactividad y tiempo absoluto— y la sala muere con -el que llegue antes. +---------- -

- - -

+## Parliament ---- +Oasis contains its own Parliament (Government system). -## Videollamadas sin terceros + ![SNH](https://solarnethub.com/git/oasis-parliament.png "SolarNET.HuB") + +## Courts -![Cómo funciona una llamada](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/12-llamadas.png) +Oasis contains its own Courts (Justice system). -El servidor solo sirve para que os encontréis: **la voz y el vídeo van directos** entre -los participantes, cifrados de extremo a extremo. Sin STUN de Google. + ![SNH](https://solarnethub.com/git/oasis-courts.png "SolarNET.HuB") + +## ECOin -

- - -

+Oasis contains its own cryptocurrency. With it, you can exchange items and services in the marketplace. -Esto **no es posible con la APK oficial**: no declara los permisos de cámara y su -navegador interno los deniega. Por eso esta rama trae su propio envoltorio Android. + ![SNH](https://solarnethub.com/git/oasis-tomatoes-example.png "SolarNET.HuB") ---- +You can also receive a -Universal Basic Income- if you contribute to the Tribes and their coordinated actions. -## La interfaz + + https://ecoin.03c8.net + +## Banking -![La interfaz por dentro](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/18-interfaz.png) +Oasis contains its own UBI (Universal Basic Income), distributed weekly using ECOin, and calculated by our AI through positive and efficient participation and trust. -

- - - -

+ ![SNH](https://solarnethub.com/git/oasis-banking.png "SolarNET.HuB") + +---------- -Casi cincuenta módulos no caben en la pantalla de un teléfono: se reparten en un panal -de categorías, dos filtros arriba y una barra de cuatro atajos que eliges tú. +## Carbon Footprinting ---- +Oasis contains its own carbon footprint meter. -## Empezar desde cero + ![SNH](https://solarnethub.com/git/snh-oasis-carbon-foorprinting.png "SolarNET.HuB") -![De cero a estar dentro](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/05-empezar.png) +All transmissions, stored files, and interactions are measured to understand the environmental impact of the network. -| | | -|---|---| -| **[oasis-project.pub](https://oasis-project.pub)** | Directorio de pubs, con invitaciones y estado de cada uno | -| **[0asis.net](https://0asis.net)** | El proyecto explicado desde el principio | -| **[wiki.solarnethub.com](https://wiki.solarnethub.com/socialnet/overview)** | La documentación oficial, módulo por módulo | + ![SNH](https://solarnethub.com/git/snh-oasis-carbon-foorprinting2.png "SolarNET.HuB") ---- +And also of each inhabitant. -## Instalar + ![SNH](https://solarnethub.com/git/snh-oasis-carbon-foorprinting3.png "SolarNET.HuB") -La APK **no está en ninguna tienda**: se descarga de las *releases* de este repositorio -y se instala a mano, con orígenes desconocidos activado. Se instala **junto al Oasis -oficial** sin pisarlo —el identificador es `net.laenre.oasis`—, así que puedes tener -los dos y comparar. +---------- -Necesita unos **350 MB libres**. El primer arranque tarda entre 15 y 60 segundos porque -descomprime el servidor. +## L.A.R.P. -> **Verifica la huella de la firma** antes de instalar una APK bajada de un servidor -> propio: `apksigner verify --print-certs` +Oasis contains a L.A.R.P. (real action role-playing) structured around 1+8 main houses. -### Compilar + ![SNH](https://solarnethub.com/git/oasis-larp-schema.jpg "SolarNET.HuB") -```sh -cd android -./scripts/prepare.sh ../../oasis_mobile # librerías nativas y backend, la primera vez -./scripts/release.sh # compila, verifica y calcula la huella -``` +The main objective is to empower the inhabitants to organize around specific proposals and generate federated governments with specific characteristics. -![Qué hay dentro de la APK](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/16-arquitectura-movil.png) + + https://wiki.solarnethub.com/socialnet/roleplaying#how_to_play + +Check "The Houses" to review which one fit better with your ambitions: ---- + + https://wiki.solarnethub.com/socialnet/roleplaying#the_houses -## Documentación +---------- -Todo lo de arriba, en detalle, está en la -**[wiki](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki)**: +## Fediverse -| | | -|---|---| -| [Entender Oasis](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Entender) | La red, el gossip, los saltos, los pubs, las tribus y el LARP | -| [Empezar desde cero](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Empezar) | Invitación, seguir gente, abrir tu blog o tu tienda | -| [Karvan](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Karvan) | Salas efímeras, paso a paso | -| [Llamadas](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Llamadas) | Cómo se llama y cómo montar un TURN | -| [La interfaz](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Interfaz) | Botón por botón | -| [Identidades](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Identidades) | Varias cuentas en el mismo móvil | -| [Compilar la APK](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Compilar) | Y cómo se publica una versión | -| [Estructura del código](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Estructura) | Dónde vive cada fichero y por qué ahí | -| [Si algo va mal](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Problemas) | Los fallos que han pasado de verdad | -| [Qué NO tiene](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/Limitaciones) | Lo que falta respecto al oficial, sin adornos | +Oasis bridges to the **Fediverse**. You can connect your fediverse accounts (ex: **Mastodon**) and use them for content from inside Oasis: read your home timeline, publish (text, images and video), reply, boost and favourite — without storing any third-party content in your SSB log (the feed is fetched live and shown ephemerally; only your credentials are kept locally). ---- + ![SNH](https://solarnethub.com/git/oasis-fediverse.png "SolarNET.HuB") -## El proyecto original +Connect your account from **Settings → Fediverse**, then open it from the **Fediverse** menu. Each network lives in its own space. -Esta rama existe **gracias a** y **por encima de** el trabajo de epsylon. El código, la -arquitectura, los módulos y la red son suyos. + + Mastodon connect guide: [docs/FEDIVERSE/MASTODON/connect.md](docs/FEDIVERSE/MASTODON/connect.md) -- **Código:** https://github.com/epsylon/oasis -- **Proyecto:** https://solarnethub.com -- **Wiki:** https://wiki.solarnethub.com/socialnet/overview +---------- + +## Invite codes (for PUBs and TRIBES): -## Licencia +Oasis is a TRUSTNET. This means you need an invitation code to enter the PUBs (managed by inhabitants or hacklabs). -AGPL-3.0, la misma que el proyecto original. +Similarly, TRIBES (groups in Oasis) require an entry code. + + ![SNH](https://solarnethub.com/git/snh-oasis-invites.png "SolarNET.HuB") + +While you can use it and connect to any nodes you want, it's a good idea to get an entry code to connect with the community. + +So you'll need to know someone, or participate in a collective action that distributes invitation codes, to see everything. + + + https://wiki.solarnethub.com/socialnet/snh#finding_inhabitants + +---------- + +## Architecture: + +Oasis uses a gossip protocol or epidemic protocol which is a procedure or process of computer peer-to peer communication +that is based on the way epidemics spread. + + ![SNH](https://solarnethub.com/git/snh-meshnet.png "SolarNET.HuB") + +This means that information is able to distribute across multiple machines, without requiring direct connections between them. + + ![SNH](https://solarnethub.com/git/gossip-graph1.png "SolarNET.HuB") + +Even though Alice and Dan lack a direct connection, they can still exchange feeds: + + ![SNH](https://solarnethub.com/git/gossip-graph2.png "SolarNET.HuB") + +This is because gossip creates “transitive” connections between computers. Dan's messages travel through Carla and the PUB +to reach Alice, and visa-versa. + +---------- + +## Backend: + +Oasis is based on a mesh network and self-hosted social media ecosystem called Secure Scuttlebutt (SSB). + +SSB uses a blockchain like append-only data structure and a fully decentralized P2P network. There are no servers or authorities +of any kind. Like a crypto transaction, SSB posts are censorship-resistant and are replicated to the entire network. + + ![SNH](https://solarnethub.com/git/ssb-participants-perspective.png "SolarNET.HuB") + +In SSB each user hosts their own content and the content of the peers they follow, which provides fault tolerance and +eventual consistency. + +---------- + +## Installing: + +Follow ['INSTALL.md'](docs/install/install.md) to build and install it on your device. + +---------- + +## Setup & Deploy: + +Visit ['Settings'](https://wiki.solarnethub.com/socialnet/snh#settings_minimal) to learn how to choose your language, set a theme & configure your avatar. + +---------- + +## SNH-Hub (for HackLabs): + +The public content of the ['PUB: "La Plaza"'](https://wiki.solarnethub.com/socialnet/snh-pub) can be visited from outside the [project network](https://wiki.solarnethub.com/socialnet/overview), through the [World Wide Web](https://en.wikipedia.org/wiki/World_Wide_Web) (aka [Clearnet](https://en.wikipedia.org/wiki/Clearnet_(networking))). + + ![SNH](https://solarnethub.com/git/snh-pub-feed.png "SolarNET.HuB") + +Just visit: https://pub.solarnethub.com/ + + ![SNH](https://solarnethub.com/git/snh-pub-laplaza.png "SolarNET.HuB") + +And also you can visit periodically the public statistic of the SNH-PUB: + + ![SNH](https://solarnethub.com/git/snh-pub-stats.png "SolarNET.HuB") + +See stats: https://laplaza.solarnethub.com/ + +---------- + +## Roadmap: + +Review ['Roadmap'](https://wiki.solarnethub.com/project/roadmap#the_project_network) to know about some required functionalities that can be implemented. + +---------- + +## Translations: + +Oasis supports multiple languages. One way to contribute is to translate the interface into your language so other people in your region can use it more intuitively. + + + https://wiki.solarnethub.com/socialnet/snh#choose_language + +---------- + +## Development: + +Oasis is completely coded in: node.js, HTML5 + CSS. + +Check ['Call 4 Hackers'](https://wiki.solarnethub.com/community/hackers) for contributing with developments. + +---------- + +## Links: + + + SNH Website: https://solarnethub.com + + Kräkens.Lab: https://krakenslab.com + + Documentation: https://wiki.solarnethub.com + + Research: https://wiki.solarnethub.com/docs/research + + Code of Conduct: https://wiki.solarnethub.com/docs/code_of_conduct + + The KIT: https://wiki.solarnethub.com/kit/overview + + Ecosystem: https://wiki.solarnethub.com/socialnet/ecosystem + + Project Network: https://wiki.solarnethub.com/socialnet/snh#the_project_network + + Oasis: https://wiki.solarnethub.com/socialnet/overview + + ECOin: https://wiki.solarnethub.com/ecoin/overview + + Role-playing (L.A.R.P): https://wiki.solarnethub.com/socialnet/roleplaying + + Warehouse: https://wiki.solarnethub.com/stock/submit_request + + THS: https://thehackerstyle.com + + PeerTube: https://video.hardlimit.com/c/thehackerstyle/videos + + Youtube: https://www.youtube.com/@thehackerstyle + + Twitch: https://twitch.tv/thehackerstyle diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index c98f46b..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# artefactos y binarios grandes: los prepara scripts/prepare.sh -app/src/main/jniLibs/ -app/src/main/assets/nodejs-project.zip -.gradle/ -build/ -app/build/ -local.properties diff --git a/android/README.md b/android/README.md deleted file mode 100644 index 7e93ac2..0000000 --- a/android/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# Wrapper Android propio - -Proyecto Gradle para construir la APK de la rama sin depender del APK oficial como -molde. - -## Por que existe - -El APK que se venia usando es el de epsylon re-firmado, y su `classes.dex` **no -implementa `WebChromeClient.onPermissionRequest`**. Sin ese metodo el WebView de -Android deniega por defecto toda peticion de `getUserMedia()`, de modo que las -videollamadas de Karvan no pueden funcionar por mucho que se parchee el manifest: -la lista de permisos es estatica y hay que declararla al compilar. - -Este wrapper resuelve las dos puertas: declara los permisos y los concede al -contenido web solo tras el consentimiento del usuario. - -## Permisos: declarados, no concedidos - -`CAMERA`, `RECORD_AUDIO` y `MODIFY_AUDIO_SETTINGS` figuran en el manifest porque -Android exige que la lista sea fija. Pero son permisos peligrosos: **se piden en -tiempo de ejecucion la primera vez que se pulsa Llamar**, no al instalar. Si nadie -llama, no se pide nada y la app no accede a la camara ni al microfono. - -`uses-feature ... required="false"` evita excluir de la tienda a dispositivos sin -camara o sin microfono. - -## Que reutiliza y que es nuevo - -Las tres librerias nativas se toman tal cual del APK existente: - -- `libnode.so` — nodejs-mobile **v18.20.4** oficial (OpenSSL 3.0.13+quic), un - artefacto publico; no hay que compilar Node. -- `libnative-lib.so` — 6,8 KB, un solo simbolo JNI: - `Java_com_solarnethub_oasis_OasisActivity_startNodeWithArguments`. -- `libc++_shared.so` - -Por eso el `namespace` del modulo es `com.solarnethub.oasis`: JNI resuelve por -nombre y la clase puente tiene que vivir en ese paquete. El `applicationId` si es -propio (`net.laenre.oasis`), asi que la app se instala junto a la oficial sin -pisarla. - -Si algun dia se quiere soltar el `.so` de epsylon, reimplementar esos 6,8 KB son -unas 40 lineas de C++ contra las cabeceras de nodejs-mobile, y ahi si haria falta -el NDK. - -## Mejoras sobre el APK actual - -- `targetSdk 35` (el actual se quedo en uno ya caducado para la tienda). -- Trafico en claro permitido **solo hacia loopback** via - `network_security_config`, en vez de `usesCleartextTraffic="true"` global. -- `allowBackup="false"`: `adb backup` deja de poder extraer `~/.ssb`. -- El backend corre en un servicio en primer plano, en el proceso `:node`, asi que - sobrevive en segundo plano — imprescindible para recibir llamadas. El APK actual - declara `FOREGROUND_SERVICE` pero no registra ningun servicio. -- Al descomprimir el backend se descartan las entradas con rutas sospechosas - (zip slip). - -## Construir - -```sh -# 1. dependencias del backend (una vez) -(cd ../src/server && npm install) - -# 2. librerias nativas + empaquetado del backend -./scripts/prepare.sh ../../oasis_mobile # o la ruta de un APK - -# 3. compilar -./gradlew assembleDebug # o assembleRelease -``` - -Para firmar la release, por entorno (la keystore nunca al repositorio): - -```sh -export OASIS_KEYSTORE=/ruta/oasis-alfa-key.jks -export OASIS_KEYSTORE_PASS=... -export OASIS_KEY_ALIAS=oasis -./gradlew assembleRelease -``` - -## Requisitos - -JDK 17 o superior **con compilador**. El OpenJDK del sistema puede ser solo -runtime; `gradle.properties` apunta al JDK que trae Android Studio. Cambia esa -linea si compilas en otra maquina. - -## Estado - -Compila y genera APK con los permisos y el `onPermissionRequest` en su sitio. -**Falta probarlo en un dispositivo real**: que arranque el backend, que la -interfaz cargue y que `getUserMedia` resuelva al pulsar Llamar. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts deleted file mode 100644 index 6ad85c7..0000000 --- a/android/app/build.gradle.kts +++ /dev/null @@ -1,61 +0,0 @@ -plugins { - id("com.android.application") - id("org.jetbrains.kotlin.android") -} - -android { - // El .so nativo exporta Java_com_solarnethub_oasis_OasisActivity_startNodeWithArguments, - // asi que la clase puente tiene que vivir en ese paquete para que el simbolo resuelva. - // El applicationId si es propio: la app se instala junto a la oficial sin pisarla. - namespace = "com.solarnethub.oasis" - compileSdk = 35 - - defaultConfig { - applicationId = "net.laenre.oasis" - minSdk = 24 - targetSdk = 35 - // el pipeline los pasa por entorno; a mano valen los de aqui - versionCode = (System.getenv("OASIS_VERSION_CODE") ?: "1").toInt() - versionName = System.getenv("OASIS_VERSION_NAME") ?: "0.9.5" - ndk { abiFilters += listOf("arm64-v8a") } - } - - signingConfigs { - create("release") { - val ks = System.getenv("OASIS_KEYSTORE") - if (ks != null) { - storeFile = file(ks) - storePassword = System.getenv("OASIS_KEYSTORE_PASS") - keyAlias = System.getenv("OASIS_KEY_ALIAS") ?: "oasis" - keyPassword = System.getenv("OASIS_KEY_PASS") ?: System.getenv("OASIS_KEYSTORE_PASS") - } - } - } - - buildTypes { - release { - isMinifyEnabled = false - if (System.getenv("OASIS_KEYSTORE") != null) signingConfig = signingConfigs.getByName("release") - } - } - - buildFeatures { buildConfig = true } - - packaging { - jniLibs { - // libnode.so son 49 MB: sin comprimir se carga por mmap y arranca mas rapido - useLegacyPackaging = false - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - kotlinOptions { jvmTarget = "17" } -} - -dependencies { - implementation("androidx.appcompat:appcompat:1.7.0") - implementation("androidx.core:core-ktx:1.13.1") -} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index edcd992..0000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/java/com/solarnethub/oasis/OasisActivity.kt b/android/app/src/main/java/com/solarnethub/oasis/OasisActivity.kt deleted file mode 100644 index 42dffc5..0000000 --- a/android/app/src/main/java/com/solarnethub/oasis/OasisActivity.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.solarnethub.oasis - -/** - * Puente con el runtime de Node. - * - * El nombre de la clase y del metodo no son libres: libnative-lib.so exporta - * Java_com_solarnethub_oasis_OasisActivity_startNodeWithArguments, y JNI resuelve - * por nombre. De ahi que el namespace del modulo sea com.solarnethub.oasis aunque - * el applicationId sea net.laenre.oasis. - * - * No es una Activity pese al nombre; solo declara el metodo nativo. - */ -object OasisActivity { - init { - System.loadLibrary("node") - System.loadLibrary("native-lib") - } - - /** Arranca node en el hilo actual. No retorna hasta que el runtime termina. */ - @JvmStatic - external fun startNodeWithArguments(arguments: Array): Int -} diff --git a/android/app/src/main/java/net/laenre/oasis/MainActivity.kt b/android/app/src/main/java/net/laenre/oasis/MainActivity.kt deleted file mode 100644 index 304d64f..0000000 --- a/android/app/src/main/java/net/laenre/oasis/MainActivity.kt +++ /dev/null @@ -1,243 +0,0 @@ -package net.laenre.oasis - -import android.Manifest -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Log -import android.webkit.PermissionRequest -import android.webkit.ValueCallback -import android.webkit.WebChromeClient -import android.webkit.WebResourceError -import android.webkit.WebResourceRequest -import android.webkit.WebView -import android.webkit.WebViewClient -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat -import com.solarnethub.oasis.BuildConfig - -class MainActivity : AppCompatActivity() { - - private lateinit var web: WebView - private var pending: PermissionRequest? = null - private var fileCallback: ValueCallback>? = null - - private val pickFiles = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { res -> - // Si se cancela hay que devolver null igualmente: si no, el queda - // bloqueado y no vuelve a abrir el selector nunca mas. - fileCallback?.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(res.resultCode, res.data)) - fileCallback = null - } - - private val askAndroid = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { granted -> - val req = pending - pending = null - if (req == null) return@registerForActivityResult - // Solo se concede al contenido web lo que el sistema nos ha concedido a nosotros. - val allowed = req.resources.filter { res -> - when (res) { - PermissionRequest.RESOURCE_VIDEO_CAPTURE -> granted[Manifest.permission.CAMERA] == true - PermissionRequest.RESOURCE_AUDIO_CAPTURE -> granted[Manifest.permission.RECORD_AUDIO] == true - else -> false - } - }.toTypedArray() - if (allowed.isEmpty()) req.deny() else req.grant(allowed) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (Build.VERSION.SDK_INT >= 33 && !has("android.permission.POST_NOTIFICATIONS")) { - askAndroid.launch(arrayOf("android.permission.POST_NOTIFICATIONS")) - } - startService(Intent(this, NodeService::class.java)) - - web = WebView(this) - setContentView(web) - WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) - - web.settings.apply { - javaScriptEnabled = true - domStorageEnabled = true - mediaPlaybackRequiresUserGesture = false // permite reproducir el audio remoto de la llamada - allowFileAccess = false - allowContentAccess = false - } - - // La navegacion se queda dentro del backend local; lo de fuera va al navegador. - web.webViewClient = object : WebViewClient() { - override fun shouldOverrideUrlLoading(v: WebView?, r: WebResourceRequest?): Boolean { - val u = r?.url ?: return false - if (isLocal(u)) return false - startActivity(Intent(Intent.ACTION_VIEW, u)) - return true - } - - /** - * Red de seguridad: si una pagina del backend no carga, se vuelve a esperar - * y se reintenta, en vez de dejar el ERR_CONNECTION_REFUSED de Chromium en - * pantalla. Pasa cuando el backend todavia no escucha (arranque lento), y - * tambien cuando se ha reiniciado —al cambiar de identidad, o porque Android - * mato el proceso del servicio estando en segundo plano—. - */ - override fun onPageFinished(v: WebView?, url: String?) { - // se recuerda para poder volver aqui si luego hay que reintentar - if (url != null && url.startsWith("http://127.0.0.1:3000")) lastGoodUrl = url - } - - override fun onReceivedError(v: WebView?, req: WebResourceRequest?, err: WebResourceError?) { - if (req?.isForMainFrame != true) return // un css o una imagen no cuentan - val u = req.url ?: return - if (!isLocal(u)) return // paginas de fuera, ni tocarlas - Log.w("OasisNode", "la pagina no ha cargado (${'$'}u); reintentando") - loaded = false - waitAndLoad() - } - } - - // Sin este WebChromeClient.onPermissionRequest, el WebView DENIEGA por defecto - // toda peticion de getUserMedia y no hay videollamada posible, aunque los - // permisos esten declarados en el manifest. Es la puerta que faltaba. - web.webChromeClient = object : WebChromeClient() { - override fun onShowFileChooser( - view: WebView?, - callback: ValueCallback>?, - params: FileChooserParams? - ): Boolean { - fileCallback?.onReceiveValue(null) // descartar uno anterior sin resolver - fileCallback = callback - return try { - pickFiles.launch(params?.createIntent()) - true - } catch (e: Exception) { - fileCallback = null - false - } - } - - override fun onPermissionRequest(request: PermissionRequest) { - runOnUiThread { - if (!isLocal(request.origin)) { request.deny(); return@runOnUiThread } - val needed = mutableListOf() - for (res in request.resources) { - when (res) { - PermissionRequest.RESOURCE_VIDEO_CAPTURE -> - if (!has(Manifest.permission.CAMERA)) needed += Manifest.permission.CAMERA - PermissionRequest.RESOURCE_AUDIO_CAPTURE -> - if (!has(Manifest.permission.RECORD_AUDIO)) needed += Manifest.permission.RECORD_AUDIO - } - } - if (needed.isEmpty()) { - request.grant(request.resources) - } else { - // Aqui es donde el usuario ve el dialogo del sistema: al pulsar Llamar, - // no al instalar la app. - pending = request - askAndroid.launch(needed.toTypedArray()) - } - } - } - } - - waitAndLoad() - } - - private fun has(p: String) = - ContextCompat.checkSelfPermission(this, p) == PackageManager.PERMISSION_GRANTED - - private fun isLocal(u: Uri?): Boolean { - val h = u?.host ?: return false - return (h == "127.0.0.1" || h == "localhost") && u.port == 3000 - } - - private fun showStatus(attempt: Int) { - val s = attempt / 2 - web.loadDataWithBaseURL(null, """ - - -

Oasis

-

Preparando la aplicación…
El primer arranque descomprime el - servidor y puede tardar un minuto. No hace falta hacer nada.

${s}s

- """.trimIndent(), "text/html", "utf-8", null) - } - - /** - * Carga la interfaz reintentando hasta que el backend responda. - * - * Se hace con el propio WebView en vez de sondear con HttpURLConnection: es el - * canal que de verdad va a servir la app, y evita depender de que la peticion de - * comprobacion pase los mismos filtros (el backend, por ejemplo, responde 400 a HEAD). - */ - private var loadAttempt = 0 - private var loaded = false - @Volatile private var waiting = false // hay un hilo esperando al backend - private var lastGoodUrl: String? = null // ultima pagina que cargo bien - - /** - * Espera a que el backend acepte conexiones y entonces carga la interfaz. - * - * La comprobacion es un socket TCP, no una peticion HTTP: no depende del metodo - * (el backend responde 400 a HEAD), ni de redirecciones (/ redirige a /activity), - * ni de la politica de trafico en claro. Si el puerto acepta, esta listo. - * - * La pantalla de estado se pinta ANTES de empezar a esperar, nunca entre medias: - * cargarla mientras el WebView trae la pagina buena cancelaria esa carga. - */ - /** Ruta a abrir: la del intent si viene con una, o la raiz. */ - private fun targetUrl(): String { - val d = intent?.data - if (d != null && isLocal(d)) return d.toString() - val path = intent?.getStringExtra("path") - if (!path.isNullOrBlank() && path.startsWith("/")) return "http://127.0.0.1:3000${'$'}path" - return "http://127.0.0.1:3000/" - } - - override fun onNewIntent(newIntent: Intent) { - super.onNewIntent(newIntent) - intent = newIntent - if (loaded) web.loadUrl(targetUrl()) - } - - private fun waitAndLoad() { - // onReceivedError puede dispararse varias veces seguidas (la pagina y sus - // recursos); sin esto se acumularian hilos esperando al mismo puerto - if (waiting) return - waiting = true - showStatus(0) - Log.i("OasisNode", "esperando al backend en 127.0.0.1:3000") - Thread { - var i = 0 - while (i < 600 && !loaded) { // hasta 5 minutos - val ok = try { - java.net.Socket().use { it.connect(java.net.InetSocketAddress("127.0.0.1", 3000), 1000); true } - } catch (e: Exception) { - if (i % 10 == 0) Log.w("OasisNode", "socket #" + i + ": " + e.javaClass.simpleName + ": " + e.message) - false - } - if (ok) { - Handler(Looper.getMainLooper()).post { - loaded = true - waiting = false - Log.i("OasisNode", "backend listo tras " + (i / 2) + "s, cargando interfaz") - web.loadUrl(lastGoodUrl ?: targetUrl()) - } - return@Thread - } - if (i % 8 == 0) Handler(Looper.getMainLooper()).post { if (!loaded) showStatus(i) } - Thread.sleep(500); i++ - } - waiting = false - }.start() - } - - @Deprecated("Deprecated in Java") - override fun onBackPressed() { - if (web.canGoBack()) web.goBack() else super.onBackPressed() - } -} diff --git a/android/app/src/main/java/net/laenre/oasis/NodeService.kt b/android/app/src/main/java/net/laenre/oasis/NodeService.kt deleted file mode 100644 index 9bd6f07..0000000 --- a/android/app/src/main/java/net/laenre/oasis/NodeService.kt +++ /dev/null @@ -1,117 +0,0 @@ -package net.laenre.oasis - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.IBinder -import android.system.Os -import android.util.Log -import com.solarnethub.oasis.OasisActivity -import com.solarnethub.oasis.R -import java.io.File -import java.io.FileOutputStream -import java.util.zip.ZipInputStream - -/** - * Servicio en primer plano que sostiene el backend Node. - * - * Vive en el proceso :node por dos razones: nodejs-mobile solo permite arrancar el - * runtime una vez por proceso —cambiar de identidad exige matar el proceso, no vale - * reiniciar dentro—, y asi el backend sobrevive cuando la app pasa a segundo plano, - * que es imprescindible para recibir llamadas. - */ -class NodeService : Service() { - - companion object { - private const val TAG = "OasisNode" - private const val CHANNEL = "oasis-node" - private const val NOTIF_ID = 1 - @Volatile private var started = false - } - - override fun onBind(intent: Intent?): IBinder? = null - - override fun onCreate() { - super.onCreate() - startForeground(NOTIF_ID, buildNotification()) - if (started) return - started = true - Thread({ runNode() }, "node-main").start() - } - - private fun buildNotification(): Notification { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val ch = NotificationChannel(CHANNEL, getString(R.string.node_channel), NotificationManager.IMPORTANCE_LOW) - ch.setShowBadge(false) - (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(ch) - } - val b = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.Builder(this, CHANNEL) - else @Suppress("DEPRECATION") Notification.Builder(this) - return b.setContentTitle(getString(R.string.app_name)) - .setContentText(getString(R.string.node_running)) - .setSmallIcon(android.R.drawable.stat_notify_sync) - .setOngoing(true) - .build() - } - - private fun runNode() { - try { - val root = File(filesDir, "nodejs-project") - extractProjectIfNeeded(root) - val main = File(root, "main.js") - if (!main.exists()) { Log.e(TAG, "no encuentro main.js en ${root.absolutePath}"); return } - - // main.js hace process.env.HOME || resolve(__dirname,'..','..'). En Android HOME - // suele venir como "/" —no escribible—, asi que se fija explicitamente al - // almacenamiento privado de la app: ahi cuelga ~/.ssb. - val home = filesDir.parentFile ?: filesDir - val tmp = File(home, "tmp").apply { mkdirs() } - Os.setenv("HOME", home.absolutePath, true) - Os.setenv("TMPDIR", tmp.absolutePath, true) - Log.i(TAG, "HOME=${home.absolutePath}") - - val rc = OasisActivity.startNodeWithArguments(arrayOf("node", main.absolutePath)) - Log.w(TAG, "el runtime de node ha terminado con codigo $rc") - } catch (e: Throwable) { - Log.e(TAG, "fallo arrancando node", e) - } - } - - /** Descomprime assets/nodejs-project.zip la primera vez y tras cada actualizacion. */ - private fun extractProjectIfNeeded(root: File) { - val stamp = File(root, ".version") - // Marca por tamano y fecha del asset, no por versionName: al reconstruir la APK - // sin subir la version, con versionName la app seguiria con el backend anterior. - // lastUpdateTime cambia en cada instalacion, tambien reinstalando la misma version. - // (openFd no vale: el asset va comprimido dentro del APK y no admite descriptor.) - val current = packageManager.getPackageInfo(packageName, 0).lastUpdateTime.toString() - if (stamp.exists() && stamp.readText() == current) return - if (root.exists()) root.deleteRecursively() - root.mkdirs() - assets.open("nodejs-project.zip").use { input -> - ZipInputStream(input).use { zip -> - var entry = zip.nextEntry - while (entry != null) { - val out = File(root, entry.name.removePrefix("nodejs-project/")) - // no confiar en las rutas del zip: evita escribir fuera del directorio (zip slip) - if (!out.canonicalPath.startsWith(root.canonicalPath + File.separator) && - out.canonicalPath != root.canonicalPath) { - Log.w(TAG, "entrada de zip descartada por ruta sospechosa: ${entry.name}") - } else if (entry.isDirectory) { - out.mkdirs() - } else { - out.parentFile?.mkdirs() - FileOutputStream(out).use { fos -> zip.copyTo(fos) } - } - zip.closeEntry() - entry = zip.nextEntry - } - } - } - stamp.writeText(current) - } -} diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 0de041c..0000000 --- a/android/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 5f349f7..0000000 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 5f349f7..0000000 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index c78bd37..0000000 --- a/android/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #121212 - diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml deleted file mode 100644 index 302c0e2..0000000 --- a/android/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Oasis - Servicio de Oasis - Oasis esta activo - diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml deleted file mode 100644 index d2a295f..0000000 --- a/android/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml deleted file mode 100644 index ca015e3..0000000 --- a/android/app/src/main/res/xml/network_security_config.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - 127.0.0.1 - localhost - - diff --git a/android/build.gradle.kts b/android/build.gradle.kts deleted file mode 100644 index b06934c..0000000 --- a/android/build.gradle.kts +++ /dev/null @@ -1,4 +0,0 @@ -plugins { - id("com.android.application") version "8.5.2" apply false - id("org.jetbrains.kotlin.android") version "1.9.24" apply false -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index c170168..0000000 --- a/android/gradle.properties +++ /dev/null @@ -1,8 +0,0 @@ -org.gradle.jvmargs=-Xmx2048m -android.useAndroidX=true -kotlin.code.style=official -android.nonTransitiveRClass=true - -# El OpenJDK del sistema es solo runtime (sin javac). Se usa el JDK que trae -# Android Studio. Si compilas en otra maquina, cambia esta ruta o instala un JDK 17+. -org.gradle.java.home=/opt/android-studio/jbr diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index a4b76b9..0000000 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index df97d72..0000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew deleted file mode 100755 index f5feea6..0000000 --- a/android/gradlew +++ /dev/null @@ -1,252 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat deleted file mode 100644 index 9b42019..0000000 --- a/android/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/scripts/prepare.sh b/android/scripts/prepare.sh deleted file mode 100755 index 768b97e..0000000 --- a/android/scripts/prepare.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -# Prepara lo que el proyecto Android no lleva en git: -# - las librerias nativas (libnode.so son 49 MB; no tiene sentido versionarlas) -# - assets/nodejs-project.zip con el backend Node y sus dependencias -# -# Uso: ./scripts/prepare.sh [ruta-a-un-apk-o-directorio-con-lib/arm64-v8a] -set -euo pipefail - -HERE="$(cd "$(dirname "$0")/.." && pwd)" -REPO="$(cd "$HERE/.." && pwd)" -JNI="$HERE/app/src/main/jniLibs/arm64-v8a" -ASSETS="$HERE/app/src/main/assets" -SRC="${1:-}" - -mkdir -p "$JNI" "$ASSETS" - -# --- 1) librerias nativas ----------------------------------------------------- -need_libs=(libnode.so libnative-lib.so libc++_shared.so) -missing=0 -for l in "${need_libs[@]}"; do [ -f "$JNI/$l" ] || missing=1; done - -if [ "$missing" = "1" ]; then - if [ -z "$SRC" ]; then - echo "Faltan las librerias nativas en $JNI" - echo "Pasa un APK o un directorio que tenga lib/arm64-v8a/, por ejemplo:" - echo " ./scripts/prepare.sh ../../oasis_mobile" - exit 1 - fi - if [ -d "$SRC/lib/arm64-v8a" ]; then - cp "$SRC/lib/arm64-v8a"/*.so "$JNI/" - elif [ -f "$SRC" ]; then - tmp="$(mktemp -d)"; unzip -qo "$SRC" 'lib/arm64-v8a/*' -d "$tmp" - cp "$tmp/lib/arm64-v8a"/*.so "$JNI/"; rm -rf "$tmp" - else - echo "No encuentro librerias en $SRC"; exit 1 - fi - echo "librerias nativas copiadas:"; ls -la "$JNI" | tail -n +2 | awk '{print " "$NF" ("$5" bytes)"}' -fi - -# --- 2) backend + dependencias --------------------------------------------- -# El node_modules del movil esta podado: son ~137 MB frente a los 1,4 GB de una -# instalacion de escritorio. En vez de reinstalarlo, se reutiliza el del APK base: -# comprobado que las 98 dependencias son identicas entre 0.9.1 y 0.9.5. -BASE_ZIP="" -if [ -n "$SRC" ] && [ -f "$SRC" ]; then - tmpz="$(mktemp -d)"; unzip -qo "$SRC" 'assets/nodejs-project.zip' -d "$tmpz" 2>/dev/null || true - [ -f "$tmpz/assets/nodejs-project.zip" ] && BASE_ZIP="$tmpz/assets/nodejs-project.zip" -fi - -work="$(mktemp -d)" -if [ -n "$BASE_ZIP" ]; then - ( cd "$work" && unzip -q "$BASE_ZIP" ) - mv "$work/nodejs-project/src/server/node_modules" "$work/_nm" - rm -rf "$work/nodejs-project/src" -elif [ -d "$REPO/src/server/node_modules" ]; then - mkdir -p "$work/nodejs-project" - cp -r "$REPO/src/server/node_modules" "$work/_nm" -else - echo "No hay node_modules. Pasa un APK base del que sacarlo, o instalalas:" - echo " (cd $REPO/src/server && npm install --omit=dev)" - exit 1 -fi - -mkdir -p "$work/nodejs-project" -cp -r "$REPO/src" "$work/nodejs-project/" -# backend.js lee README.md con readFileSync SIN try/catch (linea ~1690): -# si falta, el backend muere con ENOENT al arrancar. -cp "$REPO/main.js" "$REPO/package.json" "$REPO/README.md" "$work/nodejs-project/" 2>/dev/null || true -test -f "$work/nodejs-project/README.md" || { echo "FALTA README.md: el backend no arrancaria"; exit 1; } -rm -rf "$work/nodejs-project/src/server/node_modules" -mv "$work/_nm" "$work/nodejs-project/src/server/node_modules" - -# --- 3) empaquetar ------------------------------------------------------------ -# Con compresion: sin ella el backend son ~145 MB y la APK se iria por encima de 200. -rm -f "$ASSETS/nodejs-project.zip" -( cd "$work" && zip -q -r "$ASSETS/nodejs-project.zip" nodejs-project ) -rm -rf "$work" "${tmpz:-/nonexistent}" -echo "backend empaquetado: $(du -h "$ASSETS/nodejs-project.zip" | cut -f1) en assets/nodejs-project.zip" -echo -echo "Listo. Ahora: ./gradlew assembleDebug (o assembleRelease para firmar)" diff --git a/android/scripts/release.sh b/android/scripts/release.sh deleted file mode 100755 index 5fedb05..0000000 --- a/android/scripts/release.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env bash -# -# Construye la APK, la verifica y (si se le pasa un token) publica la release en -# gitea. Pensado para lanzarlo a mano o desde un runner; hace lo mismo en los dos -# sitios, que es lo que evita el clasico "en mi maquina si sale". -# -# Uso: -# ./scripts/release.sh # compila debug y verifica -# ./scripts/release.sh --release # compila release firmada -# ./scripts/release.sh --release --publish -# -# Variables: -# OASIS_KEYSTORE, OASIS_KEYSTORE_PASS, OASIS_KEY_ALIAS firma (obligatorias con --release) -# OASIS_GITEA_TOKEN publicar (obligatoria con --publish) -# OASIS_APK_BASE APK del que sacar node_modules la primera vez -# OASIS_GITEA_URL por defecto https://gitea.laenre.net -# OASIS_GITEA_REPO por defecto hacklab/OASIS_MOBILE -# ANDROID_HOME por defecto ~/Android/Sdk -# JAVA_HOME un JDK 17 o superior CON compilador -# -set -euo pipefail - -HERE="$(cd "$(dirname "$0")/.." && pwd)" -REPO="$(cd "$HERE/.." && pwd)" -cd "$HERE" - -MODE=debug -PUBLISH=0 -for arg in "$@"; do - case "$arg" in - --release) MODE=release ;; - --publish) PUBLISH=1 ;; - *) echo "opcion desconocida: $arg"; exit 2 ;; - esac -done - -GITEA_URL="${OASIS_GITEA_URL:-https://gitea.laenre.net}" -GITEA_REPO="${OASIS_GITEA_REPO:-hacklab/OASIS_MOBILE}" -SDK="${ANDROID_HOME:-$HOME/Android/Sdk}" - -say() { printf '\n== %s\n' "$*"; } -die() { printf '\n!! %s\n' "$*" >&2; exit 1; } - -# --- 1) version ------------------------------------------------------------- -# El nombre sale de package.json, que es de donde lo saca tambien el backend, para -# que la version que muestra la aplicacion y la de la APK no puedan discrepar. -VERSION_NAME="${OASIS_VERSION_NAME:-$(node -p "require('$REPO/package.json').version" 2>/dev/null || echo 0.0.0)}" -# El codigo tiene que subir en cada publicacion o Android rechaza la instalacion: -# el numero de commits sirve y no hay que llevarlo a mano. -VERSION_CODE="${OASIS_VERSION_CODE:-$(git -C "$REPO" rev-list --count HEAD 2>/dev/null || echo 1)}" -TAG="v${VERSION_NAME}-${VERSION_CODE}" -export OASIS_VERSION_NAME="$VERSION_NAME" OASIS_VERSION_CODE="$VERSION_CODE" -say "version ${VERSION_NAME} (codigo ${VERSION_CODE}), modo ${MODE}" - -# --- 2) comprobaciones previas --------------------------------------------- -[ -n "${JAVA_HOME:-}" ] || die "define JAVA_HOME apuntando a un JDK con compilador" -[ -x "${JAVA_HOME}/bin/javac" ] || die "en ${JAVA_HOME} no hay javac: es un runtime, no un JDK" -[ -d "$SDK" ] || die "no encuentro el SDK de Android en $SDK (define ANDROID_HOME)" - -if [ "$MODE" = release ]; then - [ -n "${OASIS_KEYSTORE:-}" ] || die "--release necesita OASIS_KEYSTORE" - [ -f "$OASIS_KEYSTORE" ] || die "no existe el almacen de claves: $OASIS_KEYSTORE" - [ -n "${OASIS_KEYSTORE_PASS:-}" ] || die "--release necesita OASIS_KEYSTORE_PASS" -fi -if [ "$PUBLISH" = 1 ]; then - [ -n "${OASIS_GITEA_TOKEN:-}" ] || die "--publish necesita OASIS_GITEA_TOKEN" - [ "$MODE" = release ] || die "no se publica una APK de depuracion" -fi - -# --- 3) backend empaquetado ------------------------------------------------- -# prepare.sh es idempotente pero tarda; se rehace siempre porque el backend cambia -# en cada commit y una APK con el backend viejo es peor que no tener APK. -# La primera vez hace falta un APK base del que sacar node_modules: el del movil -# esta podado y sin binarios nativos, y un npm install normal no lo reproduce. -say "empaquetando el backend" -./scripts/prepare.sh ${OASIS_APK_BASE:+"$OASIS_APK_BASE"} \ - || die "prepare.sh ha fallado (define OASIS_APK_BASE con un APK del que sacar node_modules)" - -ZIP=app/src/main/assets/nodejs-project.zip -[ -f "$ZIP" ] || die "falta $ZIP" -ZIP_MB=$(( $(stat -c%s "$ZIP") / 1024 / 1024 )) -[ "$ZIP_MB" -ge 20 ] || die "el backend empaquetado son $ZIP_MB MB: se ha quedado a medias" -say "backend: ${ZIP_MB} MB" - -# --- 4) compilar ------------------------------------------------------------ -say "compilando" -if [ "$MODE" = release ]; then - ./gradlew --quiet assembleRelease - APK=$(ls app/build/outputs/apk/release/*.apk | head -1) -else - ./gradlew --quiet assembleDebug - APK=$(ls app/build/outputs/apk/debug/*.apk | head -1) -fi -[ -f "$APK" ] || die "no se ha generado ninguna APK" -say "APK: $APK ($(( $(stat -c%s "$APK") / 1024 / 1024 )) MB)" - -# --- 5) verificar ----------------------------------------------------------- -# Estas tres comprobaciones son las que han cazado fallos de verdad: una APK que -# compila e instala puede no llevar el backend dentro, o haber perdido los permisos. -BT=$(ls -d "$SDK"/build-tools/* 2>/dev/null | sort -V | tail -1) -[ -n "$BT" ] || die "no encuentro build-tools en $SDK" - -say "verificando" -# ojo con pipefail: grep -q cierra la tuberia y el productor muere con SIGPIPE, -# asi que se vuelca a fichero antes de mirar -LISTING=$(mktemp); PERMS=$(mktemp) -trap 'rm -f "$LISTING" "$PERMS"' EXIT -unzip -l "$APK" > "$LISTING" -"$BT/aapt2" dump permissions "$APK" > "$PERMS" 2>/dev/null || true - -grep -q "assets/nodejs-project.zip" "$LISTING" || die "la APK no lleva el backend dentro" -grep -q "android.permission.CAMERA" "$PERMS" || die "la APK no declara CAMERA: no podria hacer videollamadas" -grep -q "android.permission.RECORD_AUDIO" "$PERMS" || die "la APK no declara RECORD_AUDIO" -grep -q "libnode.so" "$LISTING" || die "la APK no lleva el runtime de node" - -if [ "$MODE" = release ]; then - "$BT/apksigner" verify --print-certs "$APK" > /tmp/oasis-signer.txt 2>&1 \ - || die "la APK no esta firmada correctamente" - FINGERPRINT=$(grep -m1 "SHA-256 digest" /tmp/oasis-signer.txt | awk '{print $NF}') - say "huella del certificado: $FINGERPRINT" -fi - -SHA=$(sha256sum "$APK" | cut -d' ' -f1) -say "sha256 de la APK: $SHA" - -# --- 6) publicar ------------------------------------------------------------ -if [ "$PUBLISH" != 1 ]; then - say "listo (sin publicar)" - exit 0 -fi - -say "publicando $TAG en $GITEA_REPO" -API="$GITEA_URL/api/v1/repos/$GITEA_REPO" -AUTH="Authorization: token $OASIS_GITEA_TOKEN" - -NOTES=$(printf 'Rama experimental del hacklab sobre el Oasis de epsylon.\n\nNO es Oasis oficial: https://github.com/epsylon/oasis\n\nVersion base: %s (codigo %s)\n\nsha256 de la APK: %s\nHuella del certificado: %s\n\nComprueba la huella antes de instalar:\n\n apksigner verify --print-certs oasis.apk\n' \ - "$VERSION_NAME" "$VERSION_CODE" "$SHA" "${FINGERPRINT:-sin firmar}") - -RELEASE_ID=$(curl -sf -X POST "$API/releases" -H "$AUTH" -H "Content-Type: application/json" \ - -d "$(node -e ' - const [tag, name, body] = process.argv.slice(1); - process.stdout.write(JSON.stringify({ tag_name: tag, name, body, draft: true, prerelease: true })); - ' "$TAG" "Oasis Mobile $TAG" "$NOTES")" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id' 2>/dev/null) \ - || die "no se ha podido crear la release (token sin permiso, o el tag ya existe)" - -curl -sf -X POST "$API/releases/$RELEASE_ID/assets?name=oasis-$TAG.apk" \ - -H "$AUTH" -F "attachment=@$APK" >/dev/null \ - || die "la release se ha creado pero no se ha podido subir la APK" - -say "publicada como borrador: $GITEA_URL/$GITEA_REPO/releases" -say "revisala y quita el borrador a mano; no se publica sola a proposito" diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts deleted file mode 100644 index 17fd342..0000000 --- a/android/settings.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { google(); mavenCentral(); gradlePluginPortal() } -} -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { google(); mavenCentral() } -} -rootProject.name = "OasisMobile" -include(":app") diff --git a/main.js b/main.js index 7cfeff0..c3d1c01 100644 --- a/main.js +++ b/main.js @@ -67,9 +67,153 @@ console.log('[Oasis] Launching Oasis Social Network Utopia...'); console.log('[Oasis] Backend path:', backendPath); console.log('[Oasis] HOME:', process.env.HOME); -try { +const http = require('http'); +const keylock = require('./src/models/keylock'); +const configPath = path.join(process.env.HOME, '.ssb'); + +let backendStarted = false; +function startBackend() { + if (backendStarted) return; + backendStarted = true; + try { require(backendPath); console.log('[Oasis] Backend started on port 3000'); -} catch (error) { + } catch (error) { console.error('[Oasis] Error starting backend:', error); + } } + +function unlockAndStart(keys) { + global.__OASIS_KEYS__ = keys; + for (const s of sockets) { try { s.destroy(); } catch (_) {} } + try { lockServer.close(() => startBackend()); } catch (_) { startBackend(); } +} + +const MODE = keylock.isConfigured(configPath) ? 'unlock' : 'setup'; + +const LOCK_LANGS = ['en','es','fr','eu','de','it','pt','zh','ar','hi','ru']; +const LOCK_I18N = { + en: { name:"English", t_setup:"Create your password", t_unlock:"Unlock Oasis", s_setup:"It encrypts your identity and keys on this device. If you forget it, you lose the account (no recovery).", s_unlock:"Enter your password to decrypt your keys and start Oasis.", ph:"Password", ph2:"Repeat password", b_setup:"Create and enter", b_unlock:"Enter", e_min:"Minimum 6 characters", e_mism:"Passwords do not match", e_wrong:"Incorrect password", e_pre:"Error: ", m_unlock:"Unlocking…" }, + es: { name:"Español", t_setup:"Crea tu contraseña", t_unlock:"Desbloquea Oasis", s_setup:"Cifrará tu identidad y tus claves en este dispositivo. Si la olvidas, pierdes la cuenta (no hay recuperación).", s_unlock:"Introduce tu contraseña para descifrar tus claves y arrancar Oasis.", ph:"Contraseña", ph2:"Repite la contraseña", b_setup:"Crear y entrar", b_unlock:"Entrar", e_min:"Mínimo 6 caracteres", e_mism:"Las contraseñas no coinciden", e_wrong:"Contraseña incorrecta", e_pre:"Error: ", m_unlock:"Desbloqueando…" }, + fr: { name:"Français", t_setup:"Créez votre mot de passe", t_unlock:"Déverrouiller Oasis", s_setup:"Il chiffre votre identité et vos clés sur cet appareil. Si vous l'oubliez, vous perdez le compte (aucune récupération).", s_unlock:"Saisissez votre mot de passe pour déchiffrer vos clés et démarrer Oasis.", ph:"Mot de passe", ph2:"Répétez le mot de passe", b_setup:"Créer et entrer", b_unlock:"Entrer", e_min:"6 caractères minimum", e_mism:"Les mots de passe ne correspondent pas", e_wrong:"Mot de passe incorrect", e_pre:"Erreur : ", m_unlock:"Déverrouillage…" }, + eu: { name:"Euskara", t_setup:"Sortu zure pasahitza", t_unlock:"Desblokeatu Oasis", s_setup:"Zure identitatea eta gakoak gailu honetan zifratzen ditu. Ahazten baduzu, kontua galtzen duzu (ez dago berreskuratzerik).", s_unlock:"Sartu zure pasahitza gakoak deszifratu eta Oasis abiarazteko.", ph:"Pasahitza", ph2:"Errepikatu pasahitza", b_setup:"Sortu eta sartu", b_unlock:"Sartu", e_min:"Gutxienez 6 karaktere", e_mism:"Pasahitzak ez datoz bat", e_wrong:"Pasahitz okerra", e_pre:"Errorea: ", m_unlock:"Desblokeatzen…" }, + de: { name:"Deutsch", t_setup:"Erstelle dein Passwort", t_unlock:"Oasis entsperren", s_setup:"Es verschlüsselt deine Identität und Schlüssel auf diesem Gerät. Wenn du es vergisst, verlierst du das Konto (keine Wiederherstellung).", s_unlock:"Gib dein Passwort ein, um deine Schlüssel zu entschlüsseln und Oasis zu starten.", ph:"Passwort", ph2:"Passwort wiederholen", b_setup:"Erstellen und eintreten", b_unlock:"Eintreten", e_min:"Mindestens 6 Zeichen", e_mism:"Passwörter stimmen nicht überein", e_wrong:"Falsches Passwort", e_pre:"Fehler: ", m_unlock:"Entsperren…" }, + it: { name:"Italiano", t_setup:"Crea la tua password", t_unlock:"Sblocca Oasis", s_setup:"Cifra la tua identità e le tue chiavi su questo dispositivo. Se la dimentichi, perdi l'account (nessun recupero).", s_unlock:"Inserisci la password per decifrare le chiavi e avviare Oasis.", ph:"Password", ph2:"Ripeti la password", b_setup:"Crea ed entra", b_unlock:"Entra", e_min:"Minimo 6 caratteri", e_mism:"Le password non coincidono", e_wrong:"Password errata", e_pre:"Errore: ", m_unlock:"Sblocco…" }, + pt: { name:"Português", t_setup:"Crie a sua palavra-passe", t_unlock:"Desbloquear o Oasis", s_setup:"Cifra a sua identidade e chaves neste dispositivo. Se a esquecer, perde a conta (sem recuperação).", s_unlock:"Introduza a sua palavra-passe para decifrar as chaves e iniciar o Oasis.", ph:"Palavra-passe", ph2:"Repita a palavra-passe", b_setup:"Criar e entrar", b_unlock:"Entrar", e_min:"Mínimo 6 caracteres", e_mism:"As palavras-passe não coincidem", e_wrong:"Palavra-passe incorreta", e_pre:"Erro: ", m_unlock:"A desbloquear…" }, + zh: { name:"中文", t_setup:"创建你的密码", t_unlock:"解锁 Oasis", s_setup:"它会在此设备上加密你的身份和密钥。如果忘记,你将失去账户(无法恢复)。", s_unlock:"输入密码以解密你的密钥并启动 Oasis。", ph:"密码", ph2:"重复密码", b_setup:"创建并进入", b_unlock:"进入", e_min:"至少 6 个字符", e_mism:"密码不一致", e_wrong:"密码错误", e_pre:"错误:", m_unlock:"正在解锁…" }, + ar: { name:"العربية", t_setup:"أنشئ كلمة المرور", t_unlock:"فتح Oasis", s_setup:"يشفّر هويتك ومفاتيحك على هذا الجهاز. إذا نسيتها، تفقد الحساب (لا استعادة).", s_unlock:"أدخل كلمة المرور لفك تشفير مفاتيحك وتشغيل Oasis.", ph:"كلمة المرور", ph2:"أعد كلمة المرور", b_setup:"إنشاء ودخول", b_unlock:"دخول", e_min:"6 أحرف على الأقل", e_mism:"كلمتا المرور غير متطابقتين", e_wrong:"كلمة مرور غير صحيحة", e_pre:"خطأ: ", m_unlock:"جارٍ الفتح…" }, + hi: { name:"हिन्दी", t_setup:"अपना पासवर्ड बनाएं", t_unlock:"Oasis अनलॉक करें", s_setup:"यह इस डिवाइस पर आपकी पहचान और कुंजियाँ एन्क्रिप्ट करता है। भूल गए तो खाता खो देंगे (कोई रिकवरी नहीं)।", s_unlock:"अपनी कुंजियाँ डिक्रिप्ट करने और Oasis शुरू करने के लिए पासवर्ड दर्ज करें।", ph:"पासवर्ड", ph2:"पासवर्ड दोहराएँ", b_setup:"बनाएँ और प्रवेश करें", b_unlock:"प्रवेश करें", e_min:"कम से कम 6 अक्षर", e_mism:"पासवर्ड मेल नहीं खाते", e_wrong:"गलत पासवर्ड", e_pre:"त्रुटि: ", m_unlock:"अनलॉक हो रहा है…" }, + ru: { name:"Русский", t_setup:"Создайте пароль", t_unlock:"Разблокировать Oasis", s_setup:"Он шифрует вашу личность и ключи на этом устройстве. Если забудете, потеряете аккаунт (без восстановления).", s_unlock:"Введите пароль, чтобы расшифровать ключи и запустить Oasis.", ph:"Пароль", ph2:"Повторите пароль", b_setup:"Создать и войти", b_unlock:"Войти", e_min:"Минимум 6 символов", e_mism:"Пароли не совпадают", e_wrong:"Неверный пароль", e_pre:"Ошибка: ", m_unlock:"Разблокировка…" }, +}; + +function lockPage(mode) { + const confirmField = mode === 'setup' + ? '' + : ''; + return '' + + '' + + 'Oasis' + + '' + + '' + + '

' + + '
' + + confirmField + + '' + + '
' + + ''; +} + +function readBody(req) { + return new Promise((resolve) => { + let b = ''; + req.on('data', (d) => { b += d; if (b.length > 1e5) req.destroy(); }); + req.on('end', () => { try { resolve(JSON.parse(b || '{}')); } catch (_) { resolve({}); } }); + req.on('error', () => resolve({})); + }); +} + +const sockets = new Set(); +const lockServer = http.createServer(async (req, res) => { + try { + if (req.method === 'POST' && req.url === '/__keylock/setup') { + const { password } = await readBody(req); + if (!password || String(password).length < 6) { res.writeHead(400, { 'Connection': 'close' }); return res.end('{"ok":false}'); } + keylock.setPassword(configPath, password); + const keys = keylock.loadOrCreateSecret(configPath); + res.writeHead(200, { 'Content-Type': 'application/json', 'Connection': 'close' }); + res.end('{"ok":true}'); + return unlockAndStart(keys); + } + if (req.method === 'POST' && req.url === '/__keylock/unlock') { + const { password } = await readBody(req); + const kek = password ? keylock.verifyPassword(configPath, password) : null; + if (!kek) { res.writeHead(401, { 'Connection': 'close' }); return res.end('{"ok":false}'); } + const keys = keylock.loadOrCreateSecret(configPath); + res.writeHead(200, { 'Content-Type': 'application/json', 'Connection': 'close' }); + res.end('{"ok":true}'); + return unlockAndStart(keys); + } + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Connection': 'close' }); + res.end(lockPage(MODE)); + } catch (e) { + res.writeHead(500, { 'Connection': 'close' }); + res.end('{"ok":false,"error":"' + String(e && e.message).replace(/"/g, '') + '"}'); + } +}); +lockServer.on('connection', (s) => { sockets.add(s); s.on('close', () => sockets.delete(s)); }); +lockServer.listen(3000, '127.0.0.1', () => { + console.log('[Oasis] keylock gate escuchando en 127.0.0.1:3000 (modo: ' + MODE + ')'); +}); diff --git a/package.json b/package.json index 78dff81..00bb761 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@krakenslab/oasis", - "version": "0.9.5", + "version": "0.8.7", "description": "Oasis - Social Networking Utopia", "repository": { "type": "git", @@ -156,4 +156,4 @@ "node": "^10.0.0 || >=12.0.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" -} \ No newline at end of file +} diff --git a/src/backend/accounts.js b/src/backend/accounts.js deleted file mode 100644 index 29330f9..0000000 --- a/src/backend/accounts.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Identidades locales. - * - * Cada identidad vive en su propio directorio ~/., con su secret, su base - * de datos y sus blobs. Cual esta activa lo decide ssb_config.js al arrancar: - * primero OASIS_ACCOUNT, y si no, el fichero que escribe setActive(). - * - * Aqui no se cambia de identidad en caliente: medio Oasis captura la referencia al - * sbot al cargarse, asi que cambiarla con el proceso vivo dejaria modulos hablando - * con la identidad anterior. El cambio se aplica al reiniciar. - */ - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); - -// mismo criterio que ssb_config.js: este nombre acaba en rutas de fichero -const ACCOUNT_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/; -const ACTIVE_FILE = path.join(__dirname, '..', 'configs', 'active-account'); - -const isValid = (name) => typeof name === 'string' && ACCOUNT_RE.test(name); - -/** Cuenta anotada como activa, o null si no hay ninguna anotada. */ -const readActive = () => { - try { - const raw = fs.readFileSync(ACTIVE_FILE, 'utf8').trim(); - return isValid(raw) ? raw : null; - } catch (_) { return null; } -}; - -/** Anota cual sera la cuenta activa en el proximo arranque. */ -const setActive = (name) => { - if (!isValid(name)) throw new Error('nombre de cuenta no valido'); - fs.mkdirSync(path.dirname(ACTIVE_FILE), { recursive: true }); - fs.writeFileSync(ACTIVE_FILE, name, { mode: 0o600 }); - return name; -}; - -/** Directorio de una cuenta. */ -const dirOf = (name) => path.join(os.homedir(), `.${name}`); - -/** - * Identidades que existen en el disco: directorios ~/. con un secret dentro. - * El secret es lo que hace que un directorio sea una identidad y no otra cosa. - */ -const list = () => { - let entries = []; - try { - entries = fs.readdirSync(os.homedir(), { withFileTypes: true }); - } catch (_) { return []; } - - return entries - .filter((e) => e.isDirectory() && e.name.startsWith('.')) - .map((e) => e.name.slice(1)) - .filter(isValid) - .filter((name) => { - try { return fs.statSync(path.join(dirOf(name), 'secret')).isFile(); } - catch (_) { return false; } - }) - .sort(); -}; - -/** Identidad publica (@...) de una cuenta, leyendo su secret. Null si no se puede. */ -const feedIdOf = (name) => { - if (!isValid(name)) return null; - try { - const raw = fs.readFileSync(path.join(dirOf(name), 'secret'), 'utf8'); - const clean = raw.split('\n').filter((l) => !l.trim().startsWith('#')).join('\n'); - const id = JSON.parse(clean).id; - return typeof id === 'string' ? id : null; - } catch (_) { return null; } -}; - -module.exports = { isValid, list, readActive, setActive, feedIdOf, dirOf, ACTIVE_FILE }; diff --git a/src/backend/backend.js b/src/backend/backend.js index 0f1e06c..44492f0 100644 --- a/src/backend/backend.js +++ b/src/backend/backend.js @@ -52,7 +52,7 @@ const writeAddrMap = (map) => { fs.mkdirSync(path.dirname(ADDR_PATH), { recursiv //parliament model let electionInFlight = null; const ensureTerm = async () => { - const cur = await parliamentModel.getPublishedTerm().catch(() => null); + const cur = await parliamentModel.getCurrentTerm().catch(() => null); if (cur) return cur; if (electionInFlight) return electionInFlight; electionInFlight = parliamentModel.resolveElection().catch(() => null).finally(() => { electionInFlight = null; }); @@ -148,13 +148,6 @@ const sendErrorPage = (ctx, message, { title, status } = {}) => { ctx.body = errorView({ title, message, backHref }); }; -const exceedsSsbLimit = (content, extraOverhead = 0) => { - try { return Buffer.byteLength(JSON.stringify(content), 'utf8') + 512 + extraOverhead > 8192; } - catch (_) { return false; } -}; - -const isSsbTooLargeError = (e) => e && /8192|must not be larger/i.test(String(e && e.message)); - const safeRefererRedirect = (ctx, fallback = '/') => { const ref = ctx.request.header.referer; if (!ref) { ctx.redirect(fallback); return; } @@ -367,10 +360,6 @@ const refreshInboxCount = async (messagesOpt) => { sharedState.setInboxCount(filtered.filter(isToUser).length); }; -const refreshMentionsCount = async () => { - try { sharedState.setMentionsCount(await mentionsModel.countUnseen()); } catch (_) {} -}; - const PM_CRYPTER_MAX = 4600; const buildInboxMessages = async () => { @@ -398,7 +387,7 @@ const buildInboxMessages = async () => { } return messages; }; -const contentFavorites = require("./content_favorites.js"); +const mediaFavorites = require("./media-favorites.js"); const customStyleFile = path.join(envPaths("oasis", { suffix: "" }).config, "/custom-style.css"); let haveCustomStyle = false; try { fs.readFileSync(customStyleFile, "utf8"); haveCustomStyle = true; } catch (e) { if (e.code !== "ENOENT") { console.log(`Problem loading ${customStyleFile}`); throw e; } } @@ -582,42 +571,20 @@ const { about, blob, friend, meta, post, vote, spreads, lifetime } = models({ cooler, isPublic: config.public, }); -const { handleBlobUpload, handleBlobUploads, serveBlob, FileTooLargeError } = require('../backend/blobHandler.js'); -const { mergeGallery } = require('../models/media_gallery'); +const { handleBlobUpload, serveBlob, FileTooLargeError } = require('../backend/blobHandler.js'); const extractBlobId = (md) => md ? (md.match(/\((&[^)]+)\)/)?.[1] ?? null) : null; const exportmodeModel = require('../models/exportmode_model'); const panicmodeModel = require('../models/panicmode_model'); const cipherModel = require('../models/cipher_model'); -const pmPolicy = require('./pm_policy'); const legacyModel = require('../models/legacy_model'); -const devModel = require('../models/dev_model'); const walletModel = require('../models/wallet_model') const pmModel = require('../models/pm_model')({ cooler, isPublic: config.public }); -const fileshareModel = require('../models/fileshare_model')({ cooler }); -const FILESHARE_MAX_SIZE = (getConfig().fileShare && Number(getConfig().fileShare.maxSize)) || (1024 * 1024 * 1024); -const FILESHARE_TTL_MS = (((getConfig().fileShare && Number(getConfig().fileShare.ttlDays)) || 30)) * 24 * 60 * 60 * 1000; -const runFileshareCleanup = async () => { - try { - const ssbClient = await cooler.open(); - const me = ssbClient.id; - const msgs = await pmModel.listAllPrivate(); - const mine = (msgs || []) - .filter(m => m && m.value && m.value.author === me && m.value.content && m.value.content.fileShare) - .map(m => ({ pointer: m.value.content.fileShare, sentAt: m.value.content.sentAt })); - if (mine.length) await fileshareModel.pruneExpired(mine, FILESHARE_TTL_MS, Date.now()); - } catch (_) {} -}; -const fsCleanupTimer = setTimeout(() => { runFileshareCleanup(); }, 120000); -if (fsCleanupTimer.unref) fsCleanupTimer.unref(); -const fsCleanupInterval = setInterval(() => { runFileshareCleanup(); }, 12 * 60 * 60 * 1000); -if (fsCleanupInterval.unref) fsCleanupInterval.unref(); const bookmarksModel = require("../models/bookmarking_model")({ cooler, isPublic: config.public }); const opinionsModel = require('../models/opinions_model')({ cooler, isPublic: config.public }); const tasksModel = require('../models/tasks_model')({ cooler, isPublic: config.public, pmModel }); const votesModel = require('../models/votes_model')({ cooler, isPublic: config.public }); const ssbConfig = require('../server/ssb_config'); const tribeCrypto = require('../models/crypto')(ssbConfig.path, 'tribes'); -const onboardingModel = require('../models/onboarding_model')({ cooler, ssbPath: ssbConfig.path }); const chatCrypto = require('../models/crypto')(ssbConfig.path, 'chats'); const padCrypto = require('../models/crypto')(ssbConfig.path, 'pads'); const mapCrypto = require('../models/crypto')(ssbConfig.path, 'maps'); @@ -645,27 +612,20 @@ const padsModel = require('../models/pads_model')({ cooler, cipherModel, tribeCr const tagsModel = require('../models/tags_model')({ cooler, isPublic: config.public, padsModel, tribesModel }); const tribesContentModel = require('../models/tribes_content_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel }); const searchModel = require('../models/search_model')({ cooler, isPublic: config.public, padsModel, tribeCrypto, tribesModel }); -const industryModel = require("../models/industry_model")({ cooler, isPublic: config.public }); -const activityModel = require('../models/activity_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel, padsModel, industryModel }); +const activityModel = require('../models/activity_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel, padsModel }); const pixeliaModel = require('../models/pixelia_model')({ cooler, isPublic: config.public }); const melodyModel = require('../models/melody_model')({ cooler }); const marketModel = require('../models/market_model')({ cooler, isPublic: config.public, tribeCrypto }); const forumModel = require('../models/forum_model')({ cooler, isPublic: config.public, tribeCrypto, forumCrypto }); -const blogModel = require('../models/blog_model')({ cooler, isPublic: config.public }); -const dataModel = require('../models/data_model')({ cooler }); -const workflowsModel = require('../models/workflows_model'); -const mentionsModel = require('../models/mentions_model')({ cooler }); const jobsModel = require('../models/jobs_model')({ cooler, isPublic: config.public, tribeCrypto }); -const housingModel = require('../models/housing_model')({ cooler }); const shopsModel = require('../models/shops_model')({ cooler, isPublic: config.public, tribeCrypto }); const chatsModel = require('../models/chats_model')({ cooler, tribeCrypto, chatCrypto, tribesModel }); -const pollsModel = require('../models/polls_model')({ cooler, isPublic: config.public, tribeCrypto, chatsModel }); const projectsModel = require("../models/projects_model")({ cooler, isPublic: config.public }); -const agendaModel = require("../models/agenda_model")({ cooler, isPublic: config.public, calendarsModel, eventsModel, tasksModel, marketModel, jobsModel, projectsModel, industryModel, housingModel }); +const agendaModel = require("../models/agenda_model")({ cooler, isPublic: config.public, calendarsModel, eventsModel, tasksModel, marketModel, jobsModel, projectsModel }); const mapsModel = require("../models/maps_model")({ cooler, isPublic: config.public, tribeCrypto, mapCrypto, tribesModel }); const gamesModel = require('../models/games_model')({ cooler }); const bankingModel = require("../models/banking_model")({ services: { cooler }, isPublic: config.public }); -const favoritesModel = require("../models/favorites_model")({ services: { cooler }, audiosModel, bookmarksModel, documentsModel, imagesModel, videosModel, mapsModel, padsModel, chatsModel, calendarsModel, torrentsModel, marketModel, shopsModel, eventsModel, tasksModel, reportsModel, votesModel, jobsModel, housingModel, projectsModel, transfersModel, forumModel, blogsModel: blogModel, pollsModel }); +const favoritesModel = require("../models/favorites_model")({ services: { cooler }, audiosModel, bookmarksModel, documentsModel, imagesModel, videosModel, mapsModel, padsModel, chatsModel, calendarsModel, torrentsModel, marketModel, shopsModel }); const logsModel = require("../models/logs_model")({ cooler }); const parliamentModel = require('../models/parliament_model')({ cooler, services: { tribes: tribesModel, votes: votesModel, inhabitants: inhabitantsModel, banking: bankingModel } }); const fediverseModel = require('../models/fediverse_model')({ isPublic: config.public }); @@ -820,40 +780,8 @@ const applyListFilters = async (items, ctx, opts = {}) => { } out = filtered; } - out = await markFavorites(out, ctx); return out; }; - -const applyTextSearch = (items, q, fields) => { - const needle = String(q || '').trim().toLowerCase(); - if (!needle || !Array.isArray(items)) return items; - return items.filter(it => fields.some(f => { - const v = it && it[f]; - if (Array.isArray(v)) return v.some(x => String(x || '').toLowerCase().includes(needle)); - return String(v == null ? '' : v).toLowerCase().includes(needle); - })); -}; - -const withFavorite = async (item, kind) => { - if (!item || typeof item !== 'object') return item; - try { - const fav = await contentFavorites.getFavoriteSet(kind); - return { ...item, isFavorite: fav.has(String(item.rootId || item.id || item.key)) }; - } catch (_) { return item; } -}; - -const markFavorites = async (items, ctx) => { - if (!Array.isArray(items) || !items.length || !ctx) return items; - const kind = String(ctx.path || '').split('/').filter(Boolean)[0]; - if (!kind || !contentModCheck[kind]) return items; - let fav; - try { fav = await contentFavorites.getFavoriteSet(kind); } catch (_) { return items; } - if (!fav || typeof fav.has !== 'function') return items; - return items.map(it => (it && typeof it === 'object' && it.isFavorite === undefined) - ? { ...it, isFavorite: fav.has(String(it.rootId || it.id || it.key)) } - : it); -}; - const enrichItemLifetime = async (item, opts = {}) => { if (!item) return item; try { @@ -913,245 +841,30 @@ const resolveMapUrl = async (mapUrl) => { } catch (_) { return null; } }; -const contentResolvers = { +const mediaResolvers = { images: id => imagesModel.resolveRootId(id), audios: id => audiosModel.resolveRootId(id), videos: id => videosModel.resolveRootId(id), documents: id => documentsModel.resolveRootId(id), bookmarks: id => bookmarksModel.resolveRootId(id), shops: id => shopsModel.resolveRootId(id), - shopProducts: id => shopsModel.resolveRootId(id), chats: id => chatsModel.resolveRootId(id), maps: id => mapsModel.resolveRootId(id), pads: id => padsModel.resolveRootId(id), calendars: id => calendarsModel.resolveRootId(id), - torrents: id => torrentsModel.resolveRootId(id), - events: async id => id, - blogs: async id => id, - logs: async id => id, - polls: async id => pollsModel.resolveRootId(id).catch(() => id), - forum: async id => id, - tasks: async id => id, - reports: async id => id, - votes: async id => id, - jobs: async id => jobsModel.resolveRootId(id).catch(() => id), - housing: async id => housingModel.resolveRootId(id).catch(() => id), - projects: async id => id, - transfers: async id => id, - market: async id => id + torrents: id => torrentsModel.resolveRootId(id) }; -const contentModCheck = { images: 'imagesMod', audios: 'audiosMod', videos: 'videosMod', documents: 'documentsMod', bookmarks: 'bookmarksMod', market: 'marketMod', jobs: 'jobsMod', projects: 'projectsMod', shops: 'shopsMod', shopProducts: 'shopsMod', chats: 'chatsMod', maps: 'mapsMod', pads: 'padsMod', calendars: 'calendarsMod', torrents: 'torrentsMod', events: 'eventsMod', forum: 'forumMod', blogs: 'blogsMod', logs: 'logsMod', polls: 'pollsMod', tasks: 'tasksMod', reports: 'reportsMod', votes: 'votesMod', housing: 'housingMod', transfers: 'transfersMod' }; +const mediaModCheck = { images: 'imagesMod', audios: 'audiosMod', videos: 'videosMod', documents: 'documentsMod', bookmarks: 'bookmarksMod', market: 'marketMod', jobs: 'jobsMod', projects: 'projectsMod', shops: 'shopsMod', chats: 'chatsMod', maps: 'mapsMod', pads: 'padsMod', calendars: 'calendarsMod', torrents: 'torrentsMod' }; const favAction = async (ctx, kind, action) => { - if (!checkMod(ctx, contentModCheck[kind])) { ctx.redirect('/modules'); return; } + if (!checkMod(ctx, mediaModCheck[kind])) { ctx.redirect('/modules'); return; } try { - const rootId = await contentResolvers[kind](ctx.params.id); - if (rootId) await contentFavorites[action + 'Favorite'](kind, rootId); + const rootId = await mediaResolvers[kind](ctx.params.id); + if (rootId) await mediaFavorites[action + 'Favorite'](kind, rootId); } catch (_) {} ctx.redirect(safeReturnTo(ctx, `/${kind}`, [`/${kind}`])); }; -const voteFormRedirect = (mode, id, err, body = {}) => { - const params = new URLSearchParams(); - params.set('error', err && err.code === 'VOTE_DEADLINE_MIN' ? 'deadline' : 'generic'); - if (body.question) params.set('question', String(body.question).slice(0, 300)); - if (body.deadline) params.set('deadline', String(body.deadline).slice(0, 40)); - if (body.tags) params.set('tags', String(body.tags).slice(0, 300)); - return mode === 'edit' && id - ? `/votes/edit/${encodeURIComponent(id)}?${params.toString()}` - : `/votes?filter=create&${params.toString()}`; -}; - -const voteFormState = (ctx) => ({ - error: typeof ctx.query.error === 'string' ? ctx.query.error : '', - minVoteDays: votesModel.MIN_VOTE_DAYS, - draft: { - question: typeof ctx.query.question === 'string' ? ctx.query.question : '', - deadline: typeof ctx.query.deadline === 'string' ? ctx.query.deadline : '', - tags: typeof ctx.query.tags === 'string' ? ctx.query.tags : '' - } -}); - -const notifyHousingRequesters = async (item, reason) => { - if (!item || String(item.author) !== String(getViewerId())) return; - const requesters = Array.isArray(item.requests) ? item.requests.filter(id => id && id !== item.author) : []; - if (!requesters.length) return; - const label = reason === 'deleted' ? 'has been removed' : 'is no longer available'; - for (const requester of requesters) { - try { - await pmModel.sendMessage([requester], 'HOUSING_UNAVAILABLE', `The place [${item.title || 'a place'}](/housing/${encodeURIComponent(item.id)}) you requested ${label}`); - } catch (_) {} - } -}; - -const readInterval = (b) => { - const single = String((b && b.interval) || '').toLowerCase(); - if (single) { - return { - intervalWeekly: single === 'weekly', - intervalMonthly: single === 'monthly', - intervalYearly: single === 'yearly' - }; - } - return { - intervalWeekly: [].concat(b.intervalWeekly).includes('1'), - intervalMonthly: [].concat(b.intervalMonthly).includes('1'), - intervalYearly: [].concat(b.intervalYearly).includes('1') - }; -}; - -const readGalleryUpload = async (ctx) => { - const b = ctx.request.body || {}; - const uploaded = await handleBlobUploads(ctx, 'images'); - const clip = ctx.request.files?.video ? await handleBlobUpload(ctx, 'video') : null; - const removeIndex = b.removePhoto !== undefined && b.removePhoto !== '' ? parseInt(b.removePhoto, 10) : -1; - const action = String(b.action || ''); - const keep = [].concat(b.keepImages || []).filter(Boolean); - return { - uploaded, - clip, - keep, - removeIndex, - action, - isMediaAction: action === 'addPhoto' || action === 'addVideo' || action === 'removeVideo' || removeIndex >= 0 - }; -}; - -const galleryPatch = (media, current) => { - const patch = {}; - if (media.uploaded.length || media.removeIndex >= 0) patch.images = mergeGallery(current, media.uploaded, media.removeIndex); - if (media.clip) patch.video = media.clip; - else if (media.action === 'removeVideo') patch.video = ''; - return patch; -}; - -const formatFileSize = (n) => { - const x = Number(n) || 0; - if (x < 1024) return x + ' B'; - const u = ['KB', 'MB', 'GB', 'TB']; - let v = x / 1024, i = 0; - while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; } - return v.toFixed(v >= 100 || i === 0 ? 0 : 1) + ' ' + u[i]; -}; - -const PDF_KINDS = { - reports: { mod: 'reportsMod', load: async (id) => ({ item: await reportsModel.getReportById(id) }) }, - votes: { mod: 'votesMod', load: async (id) => ({ item: await votesModel.getVoteById(id) }) }, - events: { - mod: 'eventsMod', - load: async (id, viewerId) => { - const event = await eventsModel.getEventById(id); - if (!event) return null; - const attendees = Array.isArray(event.attendees) ? event.attendees : []; - const isPrivate = String(event.isPublic || '').toUpperCase() === 'PRIVATE'; - if (isPrivate && String(event.organizer) !== String(viewerId) && !attendees.includes(viewerId)) return null; - return { item: event }; - } - }, - tasks: { - mod: 'tasksMod', - load: async (id, viewerId) => { - const task = await tasksModel.getTaskById(id); - if (!task) return null; - const assignees = Array.isArray(task.assignees) ? task.assignees : []; - const isPrivate = String(task.isPublic || '').toUpperCase() === 'PRIVATE'; - if (isPrivate && String(task.author) !== String(viewerId) && !assignees.includes(viewerId)) return null; - return { item: task }; - } - }, - calendars: { - mod: 'calendarsMod', - load: async (id, viewerId) => { - const cal = await calendarsModel.getCalendarById(id); - if (!cal) return null; - if (cal.tribeId) { - const tribe = await tribesModel.getTribeById(cal.tribeId).catch(() => null); - if (!tribe || !tribe.members.includes(viewerId)) return null; - } else { - const participants = Array.isArray(cal.participants) ? cal.participants : []; - const isOpen = String(cal.status || '').toUpperCase() === 'OPEN'; - if (!isOpen && cal.author !== viewerId && !participants.includes(viewerId)) return null; - } - if (String(cal.status || '').toUpperCase() === 'CLOSED' && cal.author !== viewerId) return null; - const dates = await calendarsModel.getDatesForCalendar(cal.rootId); - const notesByDate = {}; - for (const d of dates) notesByDate[d.key] = await calendarsModel.getNotesForDate(cal.rootId, d.key); - return { item: cal, extra: { dates, notesByDate } }; - } - }, - cv: { mod: null, load: async () => ({ item: await cvModel.getCVByUserId() }) } -}; - -const loadPdfDoc = async (ctx, kind, id) => { - const cfg = PDF_KINDS[kind]; - if (!cfg) return null; - if (cfg.mod && !checkMod(ctx, cfg.mod)) return null; - try { - return await cfg.load(id, getViewerId()); - } catch (_) { - return null; - } -}; - -const sendContentPdf = async (ctx, kind, id) => { - const doc = await loadPdfDoc(ctx, kind, id); - if (!doc || !doc.item) { ctx.redirect(`/${kind === 'cv' ? 'cv' : kind}`); return; } - const pdf = buildContentPdf(kind, doc.item, doc.extra || {}, getViewerId()); - ctx.set('Content-Type', 'application/pdf'); - ctx.set('Content-Disposition', `attachment; filename="${pdfFilename(kind, doc.item)}"`); - ctx.body = pdf; -}; - -const sharePdfBuffer = async (ctx, pdf, filename, subject) => { - let pointer; - try { - pointer = await fileshareModel.createShareFromBuffer({ buffer: pdf, filename, mime: 'application/pdf' }); - } catch (_) { - ctx.redirect('/pm?fileerror=failed#fileshare'); - return; - } - const subjectText = String(subject || filename).slice(0, 150); - ctx.body = await pmView('', subjectText, '', false, '', false, null, false, '', { - recipient: '', subject: subjectText, - manifestBlobId: pointer.manifestBlobId, keyHex: pointer.key, - filename: pointer.filename, mime: pointer.mime, size: pointer.size, - sizeLabel: formatFileSize(pointer.size), crypter: false, sharedKey: '' - }); -}; - -const sharePdfAsPm = async (ctx, kind, id) => { - const doc = await loadPdfDoc(ctx, kind, id); - if (!doc || !doc.item) { ctx.redirect(`/${kind}`); return; } - const pdf = buildContentPdf(kind, doc.item, doc.extra || {}, null); - const subject = doc.item.title || doc.item.question || doc.item.name || ''; - await sharePdfBuffer(ctx, pdf, pdfFilename(kind, doc.item), subject); -}; - -const MODULE_HOME_PATHS = new Set([ - 'images', 'audios', 'videos', 'documents', 'bookmarks', 'torrents', 'maps', - 'events', 'tasks', 'reports', 'votes', 'market', 'jobs', 'housing', 'projects', - 'industry', 'shops', 'transfers', 'pads', 'chats', 'calendars', 'forum', - 'tribes', 'feed', 'logs', 'opinions', 'trending', 'agenda' -]); - -const moduleHomeFor = (ctx) => { - if (!ctx || ctx.method !== 'GET') return null; - const segment = String(ctx.path || '').split('/').filter(Boolean)[0]; - return segment && MODULE_HOME_PATHS.has(segment) ? `/${segment}` : null; -}; - -const isMissingContentError = (err) => { - if (!err) return false; - if (err.status === 400 || err.status === 404 || err.name === 'BadRequestError') return true; - const msg = String(err.message || '').toLowerCase(); - return msg.includes('failed to decode') || - msg.includes('not found') || - msg.includes('malformed') || - msg.includes('invalid') || - msg.includes('cannot be decrypted') || - msg.includes('undecodable'); -}; - const commentAction = async (ctx, kind, idParam) => { - const modKey = contentModCheck[kind]; + const modKey = mediaModCheck[kind]; if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; } const itemId = ctx.params[idParam]; let text = stripDangerousTags((ctx.request.body.text || '').trim()); @@ -1165,14 +878,14 @@ const commentAction = async (ctx, kind, idParam) => { const opinionModels = { images: imagesModel, audios: audiosModel, videos: videosModel, documents: documentsModel, bookmarks: bookmarksModel, torrents: torrentsModel }; const deleteModels = { images: imagesModel, audios: audiosModel, videos: videosModel, documents: documentsModel, bookmarks: bookmarksModel, torrents: torrentsModel }; const opinionAction = async (ctx, kind, idParam) => { - const modKey = contentModCheck[kind]; + const modKey = mediaModCheck[kind]; if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; } await opinionModels[kind].createOpinion(ctx.params[idParam], ctx.params.category); try { activityModel.invalidateCache(); } catch (_) {} ctx.redirect(safeReturnTo(ctx, `/${kind}`, [`/${kind}`])); }; const deleteAction = async (ctx, kind, deleteFn = 'delete' + kind.charAt(0).toUpperCase() + kind.slice(1, -1) + 'ById') => { - const modKey = contentModCheck[kind]; + const modKey = mediaModCheck[kind]; if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; } await deleteModels[kind][deleteFn](ctx.params.id); ctx.redirect(safeReturnTo(ctx, `/${kind}?filter=mine`, [`/${kind}`])); @@ -1180,7 +893,7 @@ const deleteAction = async (ctx, kind, deleteFn = 'delete' + kind.charAt(0).toUp const mediaCreateModels = { audios: audiosModel, videos: videosModel }; const mediaCreateAction = async (ctx, kind) => { - const modKey = contentModCheck[kind]; + const modKey = mediaModCheck[kind]; if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; } const blob = await handleBlobUpload(ctx, kind.slice(0, -1)); const { tags, title, description, mapUrl } = ctx.request.body; @@ -1188,7 +901,7 @@ const mediaCreateAction = async (ctx, kind) => { ctx.redirect(safeReturnTo(ctx, `/${kind}?filter=all`, [`/${kind}`])); }; const mediaUpdateAction = async (ctx, kind) => { - const modKey = contentModCheck[kind]; + const modKey = mediaModCheck[kind]; if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; } const { tags, title, description, mapUrl } = ctx.request.body; const singular = kind.slice(0, -1); @@ -1259,6 +972,12 @@ const getOasisVersion = async (feedId) => { if (OASIS_VERSION && mine !== OASIS_VERSION) { ssb.publish({ type: 'oasisVersion', version: OASIS_VERSION, updatedAt: new Date().toISOString() }, () => {}); } + try { + const img = await about.image(ssb.id); + global.__OASIS_SELF_AVATAR__ = (typeof img === 'string' && img[0] === '&') + ? getAvatarUrl(img) + : '/assets/images/default-avatar.png'; + } catch (_) {} } catch (_) {} })(); async function renderBlobMarkdown(text, mentions = {}, myFeedId, myUsername) { @@ -1570,18 +1289,7 @@ const koaBodyMiddleware = koaBody({ maxFileSize: maxSize, hash: 'sha256', }, - parsedMethods: ['POST'], -}); -const koaBodyFileshare = koaBody({ - multipart: true, - formidable: { - uploadDir: blobsPath, - keepExtensions: true, - maxFieldsSize: maxSize, - maxFileSize: FILESHARE_MAX_SIZE, - hash: false, - }, - parsedMethods: ['POST'], + parsedMethods: ['POST'], }); const resolveCommentComponents = async function (ctx) { let parentId; @@ -1615,7 +1323,7 @@ const resolveCommentComponents = async function (ctx) { } return { messages, myFeedId, parentMessage, contentWarning }; }; -const { authorView, previewCommentView, commentView, editProfileView, likesView, threadView, hashtagView, privateView, previewSubtopicView, subtopicView, imageSearchView, setLanguage, tribeAccessDeniedView, inviteRequiredView, clearnetInhabitantView, clearnetBlogView } = require("../views/main_views"); +const { authorView, previewCommentView, commentView, editProfileView, extendedView, latestView, likesView, threadView, hashtagView, mentionsView, popularView, previewView, privateView, publishCustomView, publishView, previewSubtopicView, subtopicView, imageSearchView, setLanguage, topicsView, summaryView, threadsView, tribeAccessDeniedView, inviteRequiredView, clearnetInhabitantView, clearnetBlogView } = require("../views/main_views"); const { activityView } = require("../views/activity_view"); const { cvView, createCVView } = require("../views/cv_view"); const { indexingView } = require("../views/indexing_view"); @@ -1642,8 +1350,6 @@ const { voteView } = require("../views/vote_view"); const { bookmarkView, singleBookmarkView } = require("../views/bookmark_view"); const { feedView, feedCreateView, singleFeedView } = require("../views/feed_view"); const { legacyView } = require("../views/legacy_view"); -const { devTreeView, devFileView, devSearchView, devMapView } = require("../views/dev_view"); -const { welcomeView } = require("../views/welcome_view"); const { opinionsView } = require("../views/opinions_view"); const { peersView } = require("../views/peers_view"); const { graphosView } = require("../views/graphos_view"); @@ -1652,32 +1358,25 @@ const { searchView } = require("../views/search_view"); const { transferView, singleTransferView } = require("../views/transfer_view"); const { cipherView } = require("../views/cipher_view"); const { imageView, singleImageView } = require("../views/image_view"); -const { mapsView, singleMapView , renderMapInvitePage } = require("../views/maps_view"); +const { mapsView, singleMapView } = require("../views/maps_view"); const { settingsView } = require("../views/settings_view"); const { fediverseView, fediverseThreadView, fediverseOverviewView, fediversePreviewView } = require("../views/fediverse_view"); const { trendingView } = require("../views/trending_view"); const { marketView, singleMarketView } = require("../views/market_view"); const { aiView } = require("../views/AI_view"); const { forumView, singleForumView, renderForumInvitePage } = require("../views/forum_view"); -const { blogView, singleBlogView } = require("../views/blog_view"); -const { dataView } = require("../views/data_view"); -const { pollsView, singlePollView } = require("../views/polls_view"); -const { mentionsView } = require("../views/mentions_view"); const { renderBlockchainView, renderSingleBlockView } = require("../views/blockchain_view"); const { jobsView, singleJobsView, renderJobForm, clearnetJobView } = require("../views/jobs_view"); -const { housingView, singleHousingView } = require("../views/housing_view"); const { shopsView, singleShopView, singleProductView, editProductView, shopOrdersView, myPurchasesView, clearnetShopView, renderShopInvitePage } = require("../views/shops_view"); const { chatsView, singleChatView, renderChatInvitePage } = require("../views/chats_view"); const { padsView, singlePadView, renderPadInvitePage } = require("../views/pads_view"); const { calendarsView, singleCalendarView, renderCalendarInvitePage } = require("../views/calendars_view"); const { projectsView, singleProjectView, clearnetProjectView } = require("../views/projects_view") -const { industryView, singleFacilityView, singleBuildView, singleBlueprintView, blueprintEditView, buildEditView } = require("../views/industry_view") const { renderBankingView, renderSingleAllocationView, renderEpochView } = require("../views/banking_views") const { favoritesView } = require("../views/favorites_view"); const { logsView } = require("../views/logs_view"); const { buildLogsPdf } = require("./logsPdf"); const { buildSmartContractPdf } = require("./smartContractPdf"); -const { buildContentPdf, pdfFilename } = require("./contentPdf"); const { parliamentView } = require("../views/parliament_view"); const { courtsView, courtsCaseView } = require('../views/courts_view'); let sharp; @@ -1770,10 +1469,6 @@ router ctx.body = ainavHomeView({ recentTags }); return; } - if (currentConfig.ux?.current === "chats") { - ctx.redirect("/chats"); - return; - } const homePage = currentConfig.homePage || "activity"; ctx.redirect(`/${homePage}`); }) @@ -1805,8 +1500,15 @@ router try { stats.userEcoinTax = await bankingModel.getUserEcoinTax(getViewerId()); } catch (_) { stats.userEcoinTax = 0; } ctx.body = statsView(stats, filter); }) + .get("/public/popular/:period", async (ctx) => { + if (!checkMod(ctx, 'popularMod')) return ctx.redirect('/modules'); + const i18n = require("../client/assets/translations/i18n"), lang = ctx.cookies.get('language') || getConfig().language || 'en', t = i18n[lang] || i18n['en']; + const messages = sanitizeMessages(await post.popular({ period: ctx.params.period })); + const spreadMap = await spreads.forMessages((messages || []).map(m => m && m.key)).catch(() => new Map()); + ctx.body = await popularView({ messages, prefix: nav(div({ class: "filters" }, ul(['day','week','month','year'].map(p => li(form({ method: "GET", action: `/public/popular/${p}` }, button({ type: "submit", class: "filter-btn" }, t[p]))))))), spreadMap }); + }) .get("/modules", async (ctx) => { - const modules = ['blogs', 'polls', 'karvan', 'fediverse', 'invites', 'wallet', 'legacy', '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', 'parliament', 'courts']; + const modules = ['popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'fediverse', 'invites', 'wallet', 'legacy', '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', 'jobs', 'projects', 'shops', 'banking', 'parliament', 'courts']; const cfg = getConfig().modules; ctx.body = modulesView(modules.reduce((acc, m) => { acc[`${m}Mod`] = cfg[`${m}Mod`]; return acc; }, {})); }) @@ -2002,6 +1704,41 @@ router } ctx.body = renderSingleBlockView(block, filter, userId, search, viewMode, restricted); }) + .get("/public/latest", async (ctx) => { + if (!checkMod(ctx, 'latestMod')) { ctx.redirect('/modules'); return; } + const messages = sanitizeMessages(await post.latest()); + const spreadMap = await spreads.forMessages((messages || []).map(m => m && m.key)).catch(() => new Map()); + ctx.body = await latestView({ messages, spreadMap }); + }) + .get("/public/latest/extended", async (ctx) => { + if (!checkMod(ctx, 'extendedMod')) { ctx.redirect('/modules'); return; } + const messages = sanitizeMessages(await post.latestExtended()); + const spreadMap = await spreads.forMessages((messages || []).map(m => m && m.key)).catch(() => new Map()); + ctx.body = await extendedView({ messages, spreadMap }); + }) + .get("/public/latest/topics", async (ctx) => { + if (!checkMod(ctx, 'topicsMod')) { ctx.redirect('/modules'); return; } + const messages = sanitizeMessages(await post.latestTopics()); + const channels = await post.channels(); + const list = channels.map((c) => { + return li(a({ href: `/hashtag/${c}` }, `#${c}`)); + }); + const prefix = nav(ul(list)); + const spreadMap = await spreads.forMessages((messages || []).map(m => m && m.key)).catch(() => new Map()); + ctx.body = await topicsView({ messages, prefix, spreadMap }); + }) + .get("/public/latest/summaries", async (ctx) => { + if (!checkMod(ctx, 'summariesMod')) { ctx.redirect('/modules'); return; } + const messages = sanitizeMessages(await post.latestSummaries()); + const spreadMap = await spreads.forMessages((messages || []).map(m => m && m.key)).catch(() => new Map()); + ctx.body = await summaryView({ messages, spreadMap }); + }) + .get("/public/latest/threads", async (ctx) => { + if (!checkMod(ctx, 'threadsMod')) { ctx.redirect('/modules'); return; } + const messages = sanitizeMessages(await post.latestThreads()); + const spreadMap = await spreads.forMessages((messages || []).map(m => m && m.key)).catch(() => new Map()); + ctx.body = await threadsView({ messages, spreadMap }); + }) .get('/author/:feed', async (ctx) => { const feedId = decodeURIComponent(ctx.params.feed || ''), gt = Number(ctx.request.query.gt || -1), lt = Number(ctx.request.query.lt || -1); if (lt > 0 && gt > 0 && gt >= lt) throw new Error('Given search range is empty'); @@ -2039,7 +1776,7 @@ router const { bucket: lastActivityBucket } = inhabitantsModel.bucketLastActivity(fullLastTs || null); const profileItems = await fetchProfileItems(feedId, rawPrefs); const profileFilterType = String(ctx.query.type || '').toLowerCase(); - const profileSpreadable = new Set(['post','audio','video','image','document','torrent','bookmark','event','calendar','task','votes','vote','market','shop','shopProduct','project','industry','industryBuild','industryBlueprint','transfer','housing','job','report','chat','chatMessage','pad','padEntry','forum','map']); + const profileSpreadable = new Set(['post','audio','video','image','document','torrent','bookmark','event','calendar','task','votes','vote','market','shop','shopProduct','project','transfer','job','report','chat','chatMessage','pad','padEntry','forum','map']); const profileSpreadKeys = (allActions || []).filter(a => a && a.id && typeof a.id === 'string' && a.id.startsWith('%') && /\.sha256$/.test(a.id) && profileSpreadable.has(a.type)).map(a => a.id); const spreadMap = await spreads.forMessages(profileSpreadKeys).catch(() => new Map()); await warmAuthorNames(allActions, sanitizedMsgs, profileItems); @@ -2061,7 +1798,6 @@ router const applySearchPrivacy = (msgs) => msgs.filter(msg => { const c = msg.value?.content; if (!c) return true; - if (c.type === 'pub') return false; if (Array.isArray(c.recps)) return false; if (c.type === 'post' && (c.private === true || c.recps)) return false; if (c.tribeId && anonTribeIds.has(c.tribeId)) return false; @@ -2127,7 +1863,7 @@ router const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const viewerPrefs = await about.visibilityPrefs(getViewerId()).catch(() => null); const items = await imagesModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, sort, viewerId: getViewerId() }); - const fav = await contentFavorites.getFavoriteSet('images'); + const fav = await mediaFavorites.getFavoriteSet('images'); let enriched = items.map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) })); if (filter === 'favorites') enriched = enriched.filter(x => x.isFavorite); enriched = await applyListFilters(enriched, ctx); @@ -2139,14 +1875,14 @@ router .get("/images/edit/:id", async (ctx) => { if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; } const img = await imagesModel.getImageById(ctx.params.id, getViewerId()); - const fav = await contentFavorites.getFavoriteSet('images'); + const fav = await mediaFavorites.getFavoriteSet('images'); ctx.body = await imageView([{ ...img, isFavorite: fav.has(String(img.rootId || img.key)) }], 'edit', img.key, { returnTo: ctx.query.returnTo || '' }); }) .get("/images/:imageId", async (ctx) => { if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; } const { imageId } = ctx.params; const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const img = await imagesModel.getImageById(imageId, getViewerId()); - const fav = await contentFavorites.getFavoriteSet('images'); + const fav = await mediaFavorites.getFavoriteSet('images'); const comments = await getVoteComments(img.key); const imgAuthorPrefs = await about.visibilityPrefs(img.author).catch(() => null); await enrichItemLifetime(img, { key: img.key }); @@ -2157,7 +1893,7 @@ router const { filter = 'all', q = '', lat, lng, zoom, tribeId, title, description, markerLabel, tags, mapType } = ctx.query; const uid = getViewerId(); const items = await mapsModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, viewerId: uid }); - const fav = await contentFavorites.getFavoriteSet('maps'); + const fav = await mediaFavorites.getFavoriteSet('maps'); let enriched = items.map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) })); if (filter === 'favorites') enriched = enriched.filter(x => x.isFavorite); const myTribeIds = await getUserTribeIds(uid); @@ -2178,7 +1914,7 @@ router try { mapItem = await mapsModel.getMapById(ctx.params.id, getViewerId()); } catch (_) { ctx.redirect('/maps?filter=all'); return; } if (!mapItem) { ctx.redirect('/maps?filter=all'); return; } if (mapItem.author !== getViewerId()) { ctx.redirect(`/maps/${encodeURIComponent(mapItem.key)}`); return; } - const fav = await contentFavorites.getFavoriteSet('maps'); + const fav = await mediaFavorites.getFavoriteSet('maps'); ctx.body = await mapsView([{ ...mapItem, isFavorite: fav.has(String(mapItem.rootId || mapItem.key)) }], 'edit', mapItem.key, { returnTo: ctx.query.returnTo || '' }); }) .get("/maps/:mapId", async (ctx) => { @@ -2194,7 +1930,7 @@ router return; } if (!mapItem) { ctx.redirect('/maps?filter=all'); return; } - const fav = await contentFavorites.getFavoriteSet('maps'); + const fav = await mediaFavorites.getFavoriteSet('maps'); let tribeMembers = []; let parentTribe = null; if (mapItem.tribeId) { @@ -2219,7 +1955,7 @@ router const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const viewerPrefs = await about.visibilityPrefs(getViewerId()).catch(() => null); const items = await audiosModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, sort, viewerId: getViewerId() }); - const fav = await contentFavorites.getFavoriteSet('audios'); + const fav = await mediaFavorites.getFavoriteSet('audios'); let enriched = items.map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) })); if (filter === 'favorites') enriched = enriched.filter(x => x.isFavorite); enriched = await applyListFilters(enriched, ctx); @@ -2231,14 +1967,14 @@ router .get("/audios/edit/:id", async (ctx) => { if (!checkMod(ctx, 'audiosMod')) { ctx.redirect('/modules'); return; } const audio = await audiosModel.getAudioById(ctx.params.id, getViewerId()); - const fav = await contentFavorites.getFavoriteSet('audios'); + const fav = await mediaFavorites.getFavoriteSet('audios'); ctx.body = await audioView([{ ...audio, isFavorite: fav.has(String(audio.rootId || audio.key)) }], 'edit', audio.key, { returnTo: ctx.query.returnTo || '' }); }) .get("/audios/:audioId", async (ctx) => { if (!checkMod(ctx, 'audiosMod')) { ctx.redirect('/modules'); return; } const { audioId } = ctx.params; const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const audio = await audiosModel.getAudioById(audioId, getViewerId()); - const fav = await contentFavorites.getFavoriteSet('audios'); + const fav = await mediaFavorites.getFavoriteSet('audios'); const comments = await getVoteComments(audio.key); const audioAuthorPrefs = await about.visibilityPrefs(audio.author).catch(() => null); await enrichItemLifetime(audio, { key: audio.key }); @@ -2249,7 +1985,7 @@ router const { filter = 'all', q = '', sort = 'recent', tribeId = '' } = ctx.query; const viewerPrefs = await about.visibilityPrefs(getViewerId()).catch(() => null); const items = await torrentsModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, sort, viewerId: getViewerId() }); - const fav = await contentFavorites.getFavoriteSet('torrents'); + const fav = await mediaFavorites.getFavoriteSet('torrents'); let enriched = items.filter(x => !x.tribeId).map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) })); if (filter === 'favorites') enriched = enriched.filter(x => x.isFavorite); enriched = await applyListFilters(enriched, ctx); @@ -2260,14 +1996,14 @@ router .get("/torrents/edit/:id", async (ctx) => { if (!checkMod(ctx, 'torrentsMod')) { ctx.redirect('/modules'); return; } const torrent = await torrentsModel.getTorrentById(ctx.params.id, getViewerId()); - const fav = await contentFavorites.getFavoriteSet('torrents'); + const fav = await mediaFavorites.getFavoriteSet('torrents'); ctx.body = await torrentsView([{ ...torrent, isFavorite: fav.has(String(torrent.rootId || torrent.key)) }], 'edit', torrent.key, { returnTo: ctx.query.returnTo || '' }); }) .get("/torrents/:torrentId", async (ctx) => { if (!checkMod(ctx, 'torrentsMod')) { ctx.redirect('/modules'); return; } const { torrentId } = ctx.params; const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const torrent = await torrentsModel.getTorrentById(torrentId, getViewerId()); - const fav = await contentFavorites.getFavoriteSet('torrents'); + const fav = await mediaFavorites.getFavoriteSet('torrents'); const comments = await getVoteComments(torrent.key); const torrentAuthorPrefs = await about.visibilityPrefs(torrent.author).catch(() => null); await enrichItemLifetime(torrent, { key: torrent.key }); @@ -2278,7 +2014,7 @@ router const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const viewerPrefs = await about.visibilityPrefs(getViewerId()).catch(() => null); const items = await videosModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, sort, viewerId: getViewerId() }); - const fav = await contentFavorites.getFavoriteSet('videos'); + const fav = await mediaFavorites.getFavoriteSet('videos'); let enriched = items.map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) })); if (filter === 'favorites') enriched = enriched.filter(x => x.isFavorite); enriched = await applyListFilters(enriched, ctx); @@ -2290,14 +2026,14 @@ router .get("/videos/edit/:id", async (ctx) => { if (!checkMod(ctx, 'videosMod')) { ctx.redirect('/modules'); return; } const video = await videosModel.getVideoById(ctx.params.id, getViewerId()); - const fav = await contentFavorites.getFavoriteSet('videos'); + const fav = await mediaFavorites.getFavoriteSet('videos'); ctx.body = await videoView([{ ...video, isFavorite: fav.has(String(video.rootId || video.key)) }], 'edit', video.key, { returnTo: ctx.query.returnTo || '' }); }) .get("/videos/:videoId", async (ctx) => { if (!checkMod(ctx, 'videosMod')) { ctx.redirect('/modules'); return; } const { videoId } = ctx.params; const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const video = await videosModel.getVideoById(videoId, getViewerId()); - const fav = await contentFavorites.getFavoriteSet('videos'); + const fav = await mediaFavorites.getFavoriteSet('videos'); const comments = await getVoteComments(video.key); const videoAuthorPrefs = await about.visibilityPrefs(video.author).catch(() => null); await enrichItemLifetime(video, { key: video.key }); @@ -2307,7 +2043,7 @@ router const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const viewerPrefs = await about.visibilityPrefs(getViewerId()).catch(() => null); const items = await documentsModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, sort }); - const fav = await contentFavorites.getFavoriteSet('documents'); + const fav = await mediaFavorites.getFavoriteSet('documents'); let enriched = items.map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) })); if (filter === 'favorites') enriched = enriched.filter(x => x.isFavorite); enriched = await applyListFilters(enriched, ctx); @@ -2318,13 +2054,13 @@ router }) .get("/documents/edit/:id", async (ctx) => { const doc = await documentsModel.getDocumentById(ctx.params.id); - const fav = await contentFavorites.getFavoriteSet('documents'); + const fav = await mediaFavorites.getFavoriteSet('documents'); ctx.body = await documentView([{ ...doc, isFavorite: fav.has(String(doc.rootId || doc.key)) }], 'edit', doc.key, { returnTo: ctx.query.returnTo || '' }); }) .get("/documents/:documentId", async (ctx) => { const { filter = "all", q = "", sort = "recent" } = ctx.query; const document = await documentsModel.getDocumentById(ctx.params.documentId); - const fav = await contentFavorites.getFavoriteSet('documents'); + const fav = await mediaFavorites.getFavoriteSet('documents'); Object.assign(document, { isFavorite: fav.has(String(document.rootId || document.key)) }); const comments = await getVoteComments(document.rootId || document.key); const docAuthorPrefs = await about.visibilityPrefs(document.author).catch(() => null); @@ -2348,17 +2084,16 @@ router ctx.body = await createCVView(cv, true) }) .get('/pm', async ctx => { - const { recipients = '', subject = '', quote = '', preview = '', fileerror = '' } = ctx.query; + const { recipients = '', subject = '', quote = '', preview = '' } = ctx.query; const quoted = quote ? quote.split('\n').map(l => '> ' + l).join('\n') + '\n\n' : ''; const showPreview = preview === '1'; - ctx.body = await pmView(recipients, subject, quoted, showPreview, '', false, null, false, String(fileerror || '')); + ctx.body = await pmView(recipients, subject, quoted, showPreview); }) .get('/inbox', async ctx => { if (!checkMod(ctx, 'inboxMod')) { ctx.redirect('/modules'); return; } const messages = await buildInboxMessages(); await refreshInboxCount(messages); - const notice = ctx.query.filestatus === 'unavailable' ? 'unavailable' : (ctx.query.filekey === 'bad' ? 'badkey' : ''); - ctx.body = await privateView({ messages }, ctx.query.filter || undefined, null, notice); + ctx.body = await privateView({ messages }, ctx.query.filter || undefined); }) .post('/inbox/decrypt', koaBody(), async ctx => { if (!checkMod(ctx, 'inboxMod')) { ctx.redirect('/modules'); return; } @@ -2376,21 +2111,18 @@ router ctx.body = await privateView({ messages }, returnFilter || 'all', decrypted); }) .get('/tags', async ctx => { - const filter = qf(ctx), search = String(ctx.query.search || '').trim(); - const tags = await tagsModel.listTags(filter, search); - ctx.body = await tagsView(tags, filter, search); + const filter = qf(ctx), tags = await tagsModel.listTags(filter); + ctx.body = await tagsView(tags, filter); }) .get('/reports', async ctx => { const filter = qf(ctx); - const q = String(ctx.query.q || '').trim(); let reports = await enrichWithComments(await reportsModel.listAll()); - reports = applyTextSearch(reports, q, ['title', 'description', 'category', 'tags']); reports = await applyListFilters(reports, ctx); try { reports = await lifetime.enrichAndFilter(reports); } catch (_) {} await enrichMsgSize(reports); const spreadMap = await spreads.forMessages((reports || []).map(x => x && (x.id || x.key))); await warmAuthorNames(reports); - ctx.body = await reportView(reports, filter, null, ctx.query.category || '', { spreadMap, q, prefillTitle: ctx.query.title || '', prefillDescription: ctx.query.description || '' }); + ctx.body = await reportView(reports, filter, null, ctx.query.category || '', { spreadMap }); }) .get('/reports/edit/:id', async ctx => { const report = await reportsModel.getReportById(ctx.params.id); @@ -2401,36 +2133,23 @@ router const comments = await getVoteComments(reportId); await enrichMsgSize([report]); await enrichItemLifetime(report); - ctx.body = await singleReportView(await withFavorite(withCount(report, comments), 'reports'), filter, comments, { spreads: await spreads.forMessage(report.id).catch(() => null) }); + ctx.body = await singleReportView(withCount(report, comments), filter, comments, { spreads: await spreads.forMessage(report.id).catch(() => null) }); }) .get('/trending', async (ctx) => { - const filter = qf(ctx, 'ALL'); - const q = String(ctx.query.q || '').trim(); + const filter = qf(ctx, 'TOP'); let { filtered = [] } = await trendingModel.listTrending(filter); filtered = await applyListFilters(filtered, ctx); - if (q) { - const needle = q.toLowerCase(); - filtered = filtered.filter(m => { - const c = (m && m.value && m.value.content) || {}; - return [c.title, c.name, c.question, c.description, c.text, c.concept, ...(Array.isArray(c.tags) ? c.tags : [])] - .some(v => String(v || '').toLowerCase().includes(needle)); - }); - } const spreadMap = new Map(); const results = await Promise.all(filtered.map(it => it && it.key ? spreads.forMessage(it.key).catch(() => null) : Promise.resolve(null))); filtered.forEach((it, i) => { if (it && it.key && results[i]) spreadMap.set(it.key, results[i]); }); await warmAuthorNames(filtered); - ctx.body = await trendingView(filtered, filter, trendingModel.categories, spreadMap, q); + ctx.body = await trendingView(filtered, filter, trendingModel.categories, spreadMap); }) .get('/agenda', async (ctx) => { const filter = qf(ctx); - const q = String(ctx.query.q || '').trim(); let data = await agendaModel.listAgenda(filter); - if (data && Array.isArray(data.items)) { - const visible = await applyListFilters(data.items, ctx); - data = { ...data, items: applyTextSearch(visible, q, ['title', 'description', 'name', 'concept', 'location', 'tags']) }; - } - ctx.body = await agendaView(data, filter, q); + if (Array.isArray(data)) data = await applyListFilters(data, ctx); + ctx.body = await agendaView(data, filter); }) .get("/hashtag/:hashtag", async (ctx) => { const { hashtag } = ctx.params; @@ -2452,7 +2171,7 @@ router ctx.body = renderPendingFollows(enriched); return; } - if (filter === 'CVs') { + if (['CVs', 'MATCHSKILLS'].includes(filter)) { Object.assign(query, { location: ctx.query.location || '', language: ctx.query.language || '', @@ -2586,7 +2305,7 @@ router .get('/inhabitant/:id', async (ctx) => { const id = ctx.params.id; const aboutModel = about; - const [aboutMsg, cv, feed, photo, bank, lastTs, visibilityPrefs, carbonGrams, larpHouseKey, deviceSource, relationship] = await Promise.all([ + const [aboutMsg, cv, feed, photo, bank, lastTs, visibilityPrefs, carbonGrams, larpHouseKey, deviceSource] = await Promise.all([ inhabitantsModel.getLatestAboutById(id), inhabitantsModel.getCVByUserId(id), inhabitantsModel.getFeedByUserId(id), @@ -2596,8 +2315,7 @@ router aboutModel.visibilityPrefs(id).catch(() => null), getCarbonGramsForFeed(id).catch(() => 0), larpModel.getUserHouse(id).catch(() => null), - aboutModel.deviceSource(id).catch(() => null), - friend.getRelationship(id).catch(() => null) + aboutModel.deviceSource(id).catch(() => null) ]); const larpHouse = larpHouseKey ? { key: larpHouseKey, ...larpModel.getHouse(larpHouseKey) } : null; const bucketInfo = inhabitantsModel.bucketLastActivity(lastTs || null); @@ -2609,7 +2327,7 @@ router const totalClaimed = bank?.totalClaimed || 0; const oasisVersion = (String(id) === String(currentUserId) ? OASIS_VERSION : null) || await getOasisVersion(id); await warmAuthorNames(feed); - ctx.body = await inhabitantsProfileView({ about: aboutMsg, cv, feed, photo, relationship, karmaScore, estimatedUBI, lastClaimedDate, totalClaimed, carbonGrams, larpHouse, lastActivityBucket: bucketInfo.bucket, viewedId: id, visibilityPrefs, deviceSource, stats, oasisVersion }, currentUserId, fediverseModel.hasAccount()); + ctx.body = await inhabitantsProfileView({ about: aboutMsg, cv, feed, photo, karmaScore, estimatedUBI, lastClaimedDate, totalClaimed, carbonGrams, larpHouse, lastActivityBucket: bucketInfo.bucket, viewedId: id, visibilityPrefs, deviceSource, stats, oasisVersion }, currentUserId, fediverseModel.hasAccount()); }) .get('/parliament', async (ctx) => { if (!checkMod(ctx, 'parliamentMod')) return ctx.redirect('/modules'); @@ -2631,11 +2349,7 @@ router inhabitantsModel.listInhabitants({ filter: 'all', includeInactive: true }) ]); const inhabitantsTotal = Array.isArray(inhabitantsAll) ? inhabitantsAll.length : 0; - let governmentCard = governmentCardRaw ? { ...governmentCardRaw, inhabitantsTotal, expired: false } : null; - if (!governmentCard) { - const latest = await parliamentModel.getLatestGovernmentCard().catch(() => null); - if (latest) governmentCard = { ...latest, inhabitantsTotal }; - } + const governmentCard = governmentCardRaw ? { ...governmentCardRaw, inhabitantsTotal } : null; const leader = pickLeader(candidatures || []); const getActorMeta = async (type, id) => (type === 'tribe' || type === 'inhabitant') ? parliamentModel.getActorMeta({ targetType: type, targetId: id }) : null; const leaderMeta = leader ? await getActorMeta(leader.targetType || leader.powerType || 'inhabitant', leader.targetId || leader.powerId) : null; @@ -2676,7 +2390,7 @@ router try { await courtsModel.sweepCases(); } catch (_) {} const filter = String(ctx.query.filter || 'cases').toLowerCase(), search = String(ctx.query.search || '').trim(); const currentUserId = await courtsModel.getCurrentUserId(); - const state = { filter, search, cases: [], myCases: [], trials: [], history: [], nominations: [], userId: currentUserId, prefill: { titleSuffix: ctx.query.titleSuffix || '', respondentId: ctx.query.respondentId || '', method: (ctx.query.method || '').toUpperCase(), titlePreset: ctx.query.titlePreset || '' } }; + const state = { filter, search, cases: [], myCases: [], trials: [], history: [], nominations: [], userId: currentUserId }; const searchFilter = (items) => !search ? items : items.filter(c => [c.title, c.description].some(s => String(s || '').toLowerCase().includes(search.toLowerCase()))); if (filter === 'cases') state.cases = searchFilter((await courtsModel.listCases('open')).map(c => ({ ...c, respondent: c.respondentId || c.respondent }))); if (filter === 'mycases' || filter === 'actions') { @@ -2770,10 +2484,6 @@ router const forums = await listByTribeAllChain(tribe.id, 'forum'); const replies = await listByTribeAllChain(tribe.id, 'forum-reply'); sectionData = [...forums, ...replies]; - } else if (section === 'polls') { - sectionData = checkMod(ctx, 'pollsMod') - ? await pollsModel.listAll('ALL', { tribeId: tribe.id }).catch(() => []) - : []; } else if (section === 'subtribes') { sectionData = await tribesModel.listSubTribes(tribe.id, uid); } else if (mediaSections[section]) { @@ -2798,48 +2508,14 @@ router ]); const tribeChainSet = new Set(tribeChain); const toStandalone = (type, url) => (item) => ({ contentType: type, id: item.rootId || item.key, title: item.title || '', author: item.author, createdAt: item.createdAt, directUrl: url(item) }); - const allTribePolls = checkMod(ctx, 'pollsMod') - ? (await Promise.all([...tribeChainSet].map(tid => pollsModel.listAll('ALL', { tribeId: tid }).catch(() => [])))).flat() - : []; const standaloneItems = [ - ...allTribePolls.filter(p => !p.undecryptable).map(p => ({ - contentType: 'poll', id: p.id, title: p.question, author: p.author, - createdAt: p.createdAt, directUrl: `/tribe/${encodeURIComponent(tribe.id)}?section=polls` - })), ...allPadsRaw.filter(p => tribeChainSet.has(p.tribeId)).map(toStandalone('pad', p => `/pads/${encodeURIComponent(p.rootId)}`)), ...allChatsRaw.filter(c => tribeChainSet.has(c.tribeId)).map(toStandalone('chat', c => `/chats/${encodeURIComponent(c.rootId || c.key)}`)), ...allCalsRaw.filter(c => tribeChainSet.has(c.tribeId)).map(toStandalone('calendar', c => `/calendars/${encodeURIComponent(c.rootId)}`)), ...allMapsRaw.filter(m => tribeChainSet.has(m.tribeId)).map(toStandalone('map', m => `/maps/${encodeURIComponent(m.key || m.id)}`)), ...allTorrentsRaw.filter(t => tribeChainSet.has(t.tribeId)).map(toStandalone('torrent', t => `/torrents/${encodeURIComponent(t.rootId || t.key)}`)) ]; - const subTribeNameById = new Map(subTribes.map(st => [st.id, st.title])); - const chatThreadItems = []; - for (const c of allChatsRaw) { - const inChain = tribeChainSet.has(c.tribeId); - const inSub = subTribeNameById.has(c.tribeId); - if (!inChain && !inSub) continue; - const root = c.rootId || c.key; - if (!root) continue; - let msgs = []; - try { msgs = await chatsModel.listMessages(root); } catch (_) { msgs = []; } - const withText = (msgs || []).filter(m => m && typeof m.text === 'string' && m.text.trim()); - if (!withText.length) continue; - withText.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)); - const last = withText[withText.length - 1]; - chatThreadItems.push({ - contentType: 'chatThread', - id: root, - title: c.title || '', - author: c.author, - createdAt: last.createdAt, - _ts: Date.parse(last.createdAt) || 0, - directUrl: `/chats/${encodeURIComponent(root)}`, - ...(inSub ? { tribeName: subTribeNameById.get(c.tribeId) } : {}), - description: c.description || '', - replies: withText.slice(-6).map(m => ({ author: m.author, text: m.text, createdAt: m.createdAt })) - }); - } - const combined = [...allContent, ...subContent, ...standaloneItems, ...chatThreadItems]; + const combined = [...allContent, ...subContent, ...standaloneItems]; const allInhabitants = await inhabitantsModel.listInhabitants({ filter: 'all', includeInactive: true }); const allMembers = [...new Set([...tribe.members, ...subTribes.flatMap(st => st.members || [])])]; const memberMap = new Map(allInhabitants.filter(u => allMembers.includes(u.id)).map(u => [u.id, u])); @@ -2972,21 +2648,12 @@ router const isCreator = tribe.author === uid; const isMember = Array.isArray(tribe.members) && tribe.members.includes(uid); if (isCreator) { try { await parliamentModel.tribe.ensureTerm(tribe.id); } catch (_) {} } - let [term, candidatures, rules, globalTermBase] = await Promise.all([ + const [term, candidatures, rules, globalTermBase] = await Promise.all([ parliamentModel.tribe.getCurrentTerm(tribe.id).catch(() => null), parliamentModel.tribe.listCandidatures(tribe.id).catch(() => []), parliamentModel.tribe.listRules(tribe.id).catch(() => []), parliamentModel.getCurrentTerm().catch(() => null) ]); - if (term && term.startAt) { - const tribeTermDays = parliamentModel.tribe.TERM_DAYS || 60; - let cStart = moment(term.startAt); - let cEnd = term.endAt ? moment(term.endAt) : cStart.clone().add(tribeTermDays, 'days'); - const now = moment(); - let rolled = false; - while (cEnd.isValid() && cEnd.isSameOrBefore(now)) { cStart = cEnd.clone(); cEnd = cEnd.clone().add(tribeTermDays, 'days'); rolled = true; } - if (rolled) term = { ...term, startAt: cStart.toISOString(), endAt: cEnd.toISOString(), method: 'ANARCHY', powerType: 'none', powerId: null, leaders: [], winnerVotes: 0, totalVotes: 0 }; - } const globalStart = globalTermBase?.startAt || null; const alreadyPublishedThisGlobalCycle = await parliamentModel.tribe.hasCandidatureInGlobalCycle(tribe.id, globalStart).catch(() => false); const leaders = Array.isArray(term?.leaders) ? term.leaders : []; @@ -3040,7 +2707,7 @@ router ctx.body = await tribeView(tribe, uid, query, section, sectionData); }) .get('/activity', async ctx => { - const filter = qf(ctx, 'all'), userId = getViewerId(); + const filter = qf(ctx, 'recent'), userId = getViewerId(); const q = String((ctx.query && ctx.query.q) || ''); try { await bankingModel.ensureSelfAddressPublished(); } catch (_) {} try { await bankingModel.getUserEngagementScore(userId); } catch (_) {} @@ -3084,7 +2751,7 @@ router } allActions = await applyListFilters(allActions, ctx); const spreadMap = new Map(); - const SPREADABLE = new Set(['post','audio','video','image','document','torrent','bookmark','event','calendar','task','votes','vote','market','shop','shopProduct','project','industry','industryBuild','industryBlueprint','transfer','housing','job','report','chat','chatMessage','pad','padEntry','forum','map']); + const SPREADABLE = new Set(['post','audio','video','image','document','torrent','bookmark','event','calendar','task','votes','vote','market','shop','shopProduct','project','transfer','job','report','chat','chatMessage','pad','padEntry','forum','map']); const targets = (allActions || []).filter(a => a && a.id && typeof a.id === 'string' && a.id.startsWith('%') && /\.sha256$/.test(a.id) && SPREADABLE.has(a.type)); const results = await Promise.all(targets.map(a => spreads.forMessage(a.id).catch(() => null))); targets.forEach((a, i) => { if (results[i]) spreadMap.set(a.id, results[i]); }); @@ -3114,8 +2781,7 @@ router } } } catch (_) {} - const favIndex = await contentFavorites.getFavoriteIndex().catch(() => new Map()); - ctx.body = activityView(allActions, filter, userId, q, { spreadMap, favIndex, returnTo: `/activity?filter=${encodeURIComponent(filter)}` }); + ctx.body = activityView(allActions, filter, userId, q, { spreadMap }); }) .get("/profile", async (ctx) => { const myFeedId = await meta.myFeedId(), gt = Number(ctx.request.query.gt || -1), lt = Number(ctx.request.query.lt || -1); @@ -3142,6 +2808,7 @@ router inhabitantsModel.getLastActivityTimestampByUserId(myFeedId).catch(() => null) ]); const larpHouse = larpHouseKey ? { key: larpHouseKey, ...larpModel.getHouse(larpHouseKey) } : null; + global.__OASIS_SELF_AVATAR__ = getAvatarUrl(image); const stats = await inhabitantsModel.getInhabitantStats(myFeedId, myFeedId).catch(() => ({})); const userActions = (allActions || []).filter(a => a && a.author === myFeedId && a.type !== 'tombstone' && a.type !== 'post'); const normTs = t => { const n = Number(t || 0); return !isFinite(n) || n <= 0 ? 0 : n < 1e12 ? n * 1000 : n; }; @@ -3152,7 +2819,7 @@ router const baseUrl = resolveExternalBaseUrl(ctx); const profileItems = await fetchProfileItems(myFeedId, rawPrefs); const profileFilterType = String(ctx.query.type || '').toLowerCase(); - const profileSpreadable = new Set(['post','audio','video','image','document','torrent','bookmark','event','calendar','task','votes','vote','market','shop','shopProduct','project','industry','industryBuild','industryBlueprint','transfer','housing','job','report','chat','chatMessage','pad','padEntry','forum','map']); + const profileSpreadable = new Set(['post','audio','video','image','document','torrent','bookmark','event','calendar','task','votes','vote','market','shop','shopProduct','project','transfer','job','report','chat','chatMessage','pad','padEntry','forum','map']); const profileSpreadKeys = (allActions || []).filter(a => a && a.id && typeof a.id === 'string' && a.id.startsWith('%') && /\.sha256$/.test(a.id) && profileSpreadable.has(a.type)).map(a => a.id); const spreadMap = await spreads.forMessages(profileSpreadKeys).catch(() => new Map()); ctx.body = await authorView({ feedId: myFeedId, oasisVersion: OASIS_VERSION || await getOasisVersion(myFeedId), messages: sanitizeMessages(messages), firstPost, lastPost, name, description, avatarUrl: getAvatarUrl(image), relationship: { me: true }, ecoAddress, karmaScore: bankData.karmaScore, estimatedUBI: bankData.estimatedUBI || 0, lastClaimedDate: bankData.lastClaimedDate || null, totalClaimed: bankData.totalClaimed || 0, carbonGrams, larpHouse, lastActivityBucket, visibilityPrefs, stats, baseUrl, userActions, allActions, profileItems, profileFilterType, gpgFingerprint, spreadMap, fediverseConfigured: fediverseModel.hasAccount() }); @@ -3326,6 +2993,9 @@ router } catch (e) { console.error('profile/clearnet-toggle:', e.message); } ctx.redirect('/profile'); }) + .get("/publish/custom", async (ctx) => { + ctx.body = await publishCustomView(); + }) .get("/json/:message", async (ctx) => { if (config.public) { throw new Error( @@ -3454,21 +3124,11 @@ router }, }).png().toBuffer(); }; - const avatarFallback = ctx.query.fallback === 'avatar'; - const fallbackImage = async () => { - ctx.set("Content-Type", "image/png"); - ctx.set("Cache-Control", "no-cache"); - if (avatarFallback) { - try { - return fs.readFileSync(path.join(__dirname, "../client/assets/images/default-avatar.png")); - } catch (_) {} - } - return fakeImage(); - }; try { - const buffer = avatarFallback ? await blob.getCached({ blobId }) : await blob.getResolved({ blobId }); + const buffer = await blob.getResolved({ blobId }); if (!buffer) { - ctx.body = await fallbackImage(); + ctx.set("Content-Type", "image/png"); + ctx.body = await fakeImage(); return; } const fileType = await FileType.fromBuffer(buffer); @@ -3483,7 +3143,8 @@ router ctx.body = buffer; } } catch (err) { - ctx.body = await fallbackImage(); + ctx.set("Content-Type", "image/png"); + ctx.body = await fakeImage(); } }) .get("/settings", async (ctx) => { @@ -3507,7 +3168,7 @@ router }); }; try { - const gossipPathLocal = path.join(require('../server/ssb_config').path, 'gossip.json'); + const gossipPathLocal = path.join(os.homedir(), '.ssb', 'gossip.json'); let gossip = []; try { gossip = JSON.parse(await promisesFs.readFile(gossipPathLocal, 'utf8')); } catch (_) {} if (Array.isArray(gossip)) { @@ -3715,7 +3376,7 @@ router if (tr && tr.openInviteCode) h.openInviteCode = tr.openInviteCode; } } catch (_) {} - ctx.body = larpListView({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost, q: String(ctx.query.q || '').trim() }); + ctx.body = larpListView({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost }); }) .get("/larp/test", async (ctx) => { if (!checkMod(ctx, 'larpMod')) return ctx.redirect('/modules'); @@ -3810,8 +3471,7 @@ router const houseKey = String((ctx.request.body && ctx.request.body.house) || '').toLowerCase(); if (houseKey !== 'academia') { ctx.redirect('/larp'); return; } try { await larpModel.publishJoin('academia'); } catch (_) {} - const larpReturnTo = String((ctx.request.body && ctx.request.body.returnTo) || ''); - ctx.redirect(larpReturnTo === '/welcome' ? '/welcome' : '/larp/academia'); + ctx.redirect('/larp/academia'); }) .post("/larp/leave", koaBody(), async (ctx) => { if (!checkMod(ctx, 'larpMod')) return ctx.redirect('/modules'); @@ -3836,11 +3496,8 @@ router const text = stripDangerousTags(String(b.text || '')); const myFeedId = await meta.myFeedId(); const myHouseKey = await larpModel.getUserHouse(myFeedId).catch(() => null); - if (myHouseKey !== houseKey) { ctx.redirect('/larp'); return; } - try { await larpModel.publishHousePost({ house: houseKey, text }); } - catch (e) { - if (isSsbTooLargeError(e)) { sendErrorPage(ctx, require('../views/main_views').i18n.publishTooLong || 'Your post is too long. Please shorten it.', { status: 400 }); return; } - } + if (myHouseKey !== houseKey || houseKey === 'academia') { ctx.redirect('/larp'); return; } + try { await larpModel.publishHousePost({ house: houseKey, text }); } catch (_) {} ctx.redirect(`/larp/${encodeURIComponent(houseKey)}`); }) .get("/invites", async (ctx) => { @@ -3854,34 +3511,58 @@ router ctx.body = await likesView({ messages, feed, name: await about.name(feed), spreadMap }); }) .get("/mentions", async (ctx) => { - const filter = String(ctx.query.filter || 'ALL'); - const q = String(ctx.query.q || '').trim(); - const all = await mentionsModel.listMentions('ALL', { q }); - const counts = await mentionsModel.countTypes(all); - const items = filter === 'ALL' ? all : all.filter(x => x.type === filter); - await warmAuthorNames(items); - try { mentionsModel.markSeen(all); } catch (_) {} - sharedState.setMentionsCount(0); - ctx.body = await mentionsView(items, filter, { counts, total: all.length, q }); + const { messages, myFeedId } = await post.mentionsMe(); + const tribeMentions = []; + try { + const allTribes = await tribesModel.listAll(); + const myTribes = allTribes.filter(t => t.members.includes(myFeedId)); + for (const t of myTribes) { + const items = await tribesContentModel.listByTribe(t.id, null).catch(() => []); + for (const item of items) { + const text = (item.description || '') + ' ' + (item.title || ''); + if (text.includes(myFeedId) || text.includes(myFeedId.slice(1))) { + tribeMentions.push({ + key: item.id, + value: { + author: item.author, + timestamp: Date.parse(item.createdAt) || item._ts || Date.now(), + content: { + type: 'tribe-content', + text: item.description || item.title || '', + tribeId: t.id, + tribeName: t.title, + contentType: item.contentType, + mentions: { _self: [{ link: myFeedId }] } + } + } + }); + } + } + } + } catch (_) {} + const combined = [...(Array.isArray(messages) ? messages : []), ...tribeMentions]; + for (const msg of combined) { + if (!msg.value) continue; + const authorId = msg.value.author; + if (authorId) { + if (!msg.value.meta) msg.value.meta = {}; + if (!msg.value.meta.author) msg.value.meta.author = {}; + if (!msg.value.meta.author.name) { + try { msg.value.meta.author.name = await about.name(authorId); } catch (_) {} + } + } + } + ctx.body = await mentionsView({ messages: combined, myFeedId }); }) .get('/opinions', async (ctx) => { - const filter = qf(ctx, 'ALL'); - const q = String(ctx.query.q || '').trim(); + const filter = qf(ctx, 'TOP'); let opinions = await opinionsModel.listOpinions(filter); if (Array.isArray(opinions)) opinions = await applyListFilters(opinions, ctx); - if (q && Array.isArray(opinions)) { - const needle = q.toLowerCase(); - opinions = opinions.filter(m => { - const c = (m && m.value && m.value.content) || {}; - return [c.title, c.name, c.question, c.description, c.text, c.concept, ...(Array.isArray(c.tags) ? c.tags : [])] - .some(v => String(v || '').toLowerCase().includes(needle)); - }); - } const spreadMap = new Map(); const list = Array.isArray(opinions) ? opinions : []; const results = await Promise.all(list.map(it => it && it.key ? spreads.forMessage(it.key).catch(() => null) : Promise.resolve(null))); list.forEach((it, i) => { if (it && it.key && results[i]) spreadMap.set(it.key, results[i]); }); - ctx.body = await opinionsView(opinions, filter, spreadMap, q); + ctx.body = await opinionsView(opinions, filter, spreadMap); }) .get("/feed", async (ctx) => { const filter = String(ctx.query.filter || "ALL").toUpperCase(); @@ -3902,9 +3583,8 @@ router const feed = await feedModel.getFeedById(ctx.params.feedId); if (!feed) { ctx.redirect('/feed'); return; } const comments = await feedModel.getComments(ctx.params.feedId).catch(() => []); - ctx.body = singleFeedView(feed, comments, { spreads: await spreads.forMessage(feed.key).catch(() => null) }); + ctx.body = singleFeedView(feed, comments); }) - .get("/multiverse", async (ctx) => { ctx.redirect('/fediverse'); }) .get("/fediverse", async (ctx) => { if (!checkMod(ctx, 'fediverseMod')) { ctx.redirect('/modules'); return; } const account = fediverseModel.getAccount(); @@ -4054,186 +3734,15 @@ router try { await post.publishFediverseHandle(''); } catch (_) {} ctx.redirect('/settings'); }) - .get('/data', async ctx => { - const filter = String(ctx.query.filter || 'ALL').toUpperCase(); - const q = String(ctx.query.q || '').trim(); - const [{ matches, total, hasProfile }, cohesion] = await Promise.all([ - dataModel.listMatches(filter, { q }), - dataModel.cohesion().catch(() => null) - ]); - await warmAuthorNames(matches); - ctx.body = await dataView({ filter, q, matches, total, hasProfile, cohesion }); - }) - .get('/polls', async ctx => { - if (!checkMod(ctx, 'pollsMod')) { ctx.redirect('/modules'); return; } - const filter = String(ctx.query.filter || 'ALL').toUpperCase(); - const q = String(ctx.query.q || '').trim(); - if (filter === 'CREATE') { ctx.body = await pollsView([], 'CREATE', { q }); return; } - if (filter === 'EDIT') { - const poll = await pollsModel.getPollById(String(ctx.query.id || ''), getViewerId()).catch(() => null); - if (!poll || poll.author !== getViewerId()) { ctx.redirect('/polls'); return; } - ctx.body = await pollsView([], 'EDIT', { poll }); - return; - } - const fav = await contentFavorites.getFavoriteSet('polls'); - let polls = await pollsModel.listAll(filter, { q, favorites: [...fav] }); - polls = polls.map(p => ({ ...p, isFavorite: fav.has(String(p.id)) })); - polls = await applyListFilters(polls, ctx); - try { polls = await lifetime.enrichAndFilter(polls, { getKey: (x) => x.id }); } catch (_) {} - const spreadMap = await spreads.forMessages(polls.map(p => p.id)).catch(() => new Map()); - await warmAuthorNames(polls); - ctx.body = await pollsView(polls, filter, { q, spreadMap }); - }) - .get('/polls/:pollId', async ctx => { - if (!checkMod(ctx, 'pollsMod')) { ctx.redirect('/modules'); return; } - const poll = await pollsModel.getPollById(ctx.params.pollId, getViewerId()); - if (poll.chatId) { ctx.redirect(`/chats/${encodeURIComponent(poll.chatId)}`); return; } - const comments = await getVoteComments(poll.id); - const fav = await contentFavorites.getFavoriteSet('polls'); - await enrichItemLifetime(poll, { key: poll.id }); - await warmAuthorNames([poll], comments); - ctx.body = await singlePollView( - { ...poll, isFavorite: fav.has(String(poll.id)) }, - comments, - { - spreads: await spreads.forMessage(poll.id).catch(() => null), - filter: String(ctx.query.filter || 'ALL').toUpperCase(), - q: String(ctx.query.q || '').trim() - } - ); - }) - .post('/polls/create', koaBody(), async ctx => { - if (!checkMod(ctx, 'pollsMod')) { ctx.redirect('/modules'); return; } - const b = ctx.request.body || {}; - try { - await pollsModel.createPoll({ - question: stripDangerousTags(b.question), - options: stripDangerousTags(String(b.options || '')).split('\n'), - anonymous: [].concat(b.anonymous).includes('1'), - multiple: [].concat(b.multiple).includes('1'), - deadline: b.deadline, - tags: b.tags - }); - } catch (err) { sendErrorPage(ctx, err.message || String(err), { status: 400 }); return; } - try { activityModel.invalidateCache(); } catch (_) {} - ctx.redirect('/polls?filter=MINE'); - }) - .post('/polls/update/:id', koaBody(), async ctx => { - if (!checkMod(ctx, 'pollsMod')) { ctx.redirect('/modules'); return; } - const b = ctx.request.body || {}; - try { - await pollsModel.updatePoll(ctx.params.id, { - question: stripDangerousTags(b.question), - options: stripDangerousTags(String(b.options || '')).split('\n'), - anonymous: [].concat(b.anonymous).includes('1'), - multiple: [].concat(b.multiple).includes('1'), - deadline: b.deadline, - tags: b.tags - }); - } catch (err) { sendErrorPage(ctx, err.message || String(err), { status: 400 }); return; } - ctx.redirect(`/polls/${encodeURIComponent(ctx.params.id)}`); - }) - .post('/polls/vote/:id', koaBody(), async ctx => { - if (!checkMod(ctx, 'pollsMod')) { ctx.redirect('/modules'); return; } - const choices = [].concat(ctx.request.body.choices || []).filter(Boolean); - try { await pollsModel.vote(ctx.params.id, choices); } - catch (err) { sendErrorPage(ctx, err.message || String(err), { status: 400 }); return; } - try { activityModel.invalidateCache(); } catch (_) {} - ctx.redirect(safeReturnTo(ctx, `/polls/${encodeURIComponent(ctx.params.id)}`, ['/polls', '/chats', '/tribe'])); - }) - .post('/polls/close/:id', koaBody(), async ctx => { - if (!checkMod(ctx, 'pollsMod')) { ctx.redirect('/modules'); return; } - try { await pollsModel.closePoll(ctx.params.id); } catch (_) {} - ctx.redirect(safeReturnTo(ctx, `/polls/${encodeURIComponent(ctx.params.id)}`, ['/polls', '/chats', '/tribe'])); - }) - .post('/polls/delete/:id', koaBody(), async ctx => { - if (!checkMod(ctx, 'pollsMod')) { ctx.redirect('/modules'); return; } - let poll = null; - try { poll = await pollsModel.getPollById(ctx.params.id, getViewerId()); } catch (_) {} - try { await pollsModel.deletePoll(ctx.params.id); } catch (_) {} - if (poll && poll.chatId) { ctx.redirect(`/chats/${encodeURIComponent(poll.chatId)}`); return; } - ctx.redirect('/polls?filter=MINE'); - }) - .post('/polls/opinions/:pollId/:category', koaBody(), async ctx => { - if (!checkMod(ctx, 'pollsMod')) { ctx.redirect('/modules'); return; } - try { await pollsModel.createOpinion(ctx.params.pollId, ctx.params.category); } catch (_) {} - try { activityModel.invalidateCache(); } catch (_) {} - ctx.redirect(safeReturnTo(ctx, `/polls/${encodeURIComponent(ctx.params.pollId)}`, ['/polls'])); - }) - .post('/polls/:pollId/comments', koaBodyMiddleware, async ctx => commentAction(ctx, 'polls', 'pollId')) - .post('/polls/favorites/add/:id', koaBody(), async ctx => favAction(ctx, 'polls', 'add')) - .post('/polls/favorites/remove/:id', koaBody(), async ctx => favAction(ctx, 'polls', 'remove')) - .get('/blogs', async ctx => { - if (!checkMod(ctx, 'blogsMod')) { ctx.redirect('/modules'); return; } - const filter = String(ctx.query.filter || 'ALL').toUpperCase(); - const q = String(ctx.query.q || '').trim(); - if (filter === 'CREATE') { ctx.body = await blogView([], 'CREATE', { q }); return; } - const fav = await contentFavorites.getFavoriteSet('blogs'); - let blogs = await blogModel.listAll(filter, { q, favorites: [...fav] }); - blogs = await applyListFilters(blogs, ctx); - const spreadMap = await spreads.forMessages((blogs || []).map(b => b && b.id)).catch(() => new Map()); - await warmAuthorNames(blogs); - ctx.body = await blogView(blogs, filter, { q, spreadMap }); - }) - .get('/blogs/:blogId', async ctx => { - if (!checkMod(ctx, 'blogsMod')) { ctx.redirect('/modules'); return; } - let blog = await blogModel.getBlogById(ctx.params.blogId).catch(() => null); - if (!blog) { - const target = await blogModel.blogHrefFor(ctx.params.blogId).catch(() => null); - if (target && !target.startsWith(`/blogs/${encodeURIComponent(ctx.params.blogId)}`)) { ctx.redirect(target); return; } - ctx.redirect('/blogs'); - return; - } - const comments = await getVoteComments(blog.id); - const fav = await contentFavorites.getFavoriteSet('blogs'); - await warmAuthorNames([blog], comments); - ctx.body = await singleBlogView( - { ...blog, isFavorite: fav.has(String(blog.id)) }, - comments, - { spreads: await spreads.forMessage(blog.id).catch(() => null) } - ); - }) - .post('/blogs/create', koaBody({ multipart: true, urlencoded: true, formidable: { multiples: true, maxFileSize: maxSize } }), async ctx => { - if (!checkMod(ctx, 'blogsMod')) { ctx.redirect('/modules'); return; } - const b = ctx.request.body; - let text = stripDangerousTags((b.text || '').toString().trim()); - const subject = stripDangerousTags((b.subject || '').toString().trim()); - const blobMarkdown = await handleBlobUploads(ctx, 'blob', 9); - if (blobMarkdown.length) text += blobMarkdown.join(''); - const allowComments = [].concat(b.allowComments).includes('1'); - let mentions = []; - try { mentions = await extractMentions(text); } catch (_) { mentions = []; } - try { - await blogModel.createBlog({ text, subject, mentions, allowComments }); - } catch (err) { sendErrorPage(ctx, err.message || String(err), { status: 400 }); return; } - try { activityModel.invalidateCache(); } catch (_) {} - ctx.redirect('/blogs?filter=MINE'); - }) - .post('/blogs/opinions/:blogId/:category', koaBody(), async ctx => { - if (!checkMod(ctx, 'blogsMod')) { ctx.redirect('/modules'); return; } - try { await blogModel.createOpinion(ctx.params.blogId, ctx.params.category); } catch (_) {} - try { activityModel.invalidateCache(); } catch (_) {} - ctx.redirect(safeReturnTo(ctx, `/blogs/${encodeURIComponent(ctx.params.blogId)}`, ['/blogs'])); - }) - .post('/blogs/:blogId/comments', koaBodyMiddleware, async ctx => { - if (!checkMod(ctx, 'blogsMod')) { ctx.redirect('/modules'); return; } - const blog = await blogModel.getBlogById(ctx.params.blogId).catch(() => null); - if (!blog || !blog.allowComments) { ctx.redirect(`/blogs/${encodeURIComponent(ctx.params.blogId)}`); return; } - return commentAction(ctx, 'blogs', 'blogId'); - }) - .post('/blogs/favorites/add/:id', koaBody(), async ctx => favAction(ctx, 'blogs', 'add')) - .post('/blogs/favorites/remove/:id', koaBody(), async ctx => favAction(ctx, 'blogs', 'remove')) .get('/forum', async ctx => { if (!checkMod(ctx, 'forumMod')) { ctx.redirect('/modules'); return; } - const filter = qf(ctx, 'all'); - const q = String(ctx.query.q || '').trim(); + const filter = qf(ctx, 'hot'); let forums = await forumModel.listAll(filter); - forums = applyTextSearch(forums, q, ['title', 'text', 'category']); forums = await applyListFilters(forums, ctx); try { forums = await lifetime.enrichAndFilter(forums); } catch (_) {} const spreadMap = await spreads.forMessages((forums || []).map(x => x && (x.key || x.id))); await warmAuthorNames(forums); - ctx.body = await forumView(forums, filter, { spreadMap, q }); + ctx.body = await forumView(forums, filter, { spreadMap }); }) .get('/forum/:forumId', async ctx => { const msg = await forumModel.getMessageById(ctx.params.forumId), isReply = Boolean(msg.root), forumId = isReply ? msg.root : ctx.params.forumId; @@ -4242,116 +3751,16 @@ router try { const oi = await forumModel.getOpenInvite(forumId).catch(() => null); if (oi && forumObj) forumObj.openInviteCode = oi.code; } catch (_) {} const forumMsgs = await forumModel.getMessagesByForumId(forumId); await warmAuthorNames(forumObj, forumMsgs); - ctx.body = await singleForumView(await withFavorite(forumObj, 'forum'), forumMsgs, ctx.query.filter, isReply ? ctx.params.forumId : null, { spreads: spreadInfo }); - }) - .get('/welcome', async (ctx) => { - const lang = ctx.cookies.get('language') || getConfig().language || 'en'; - const available = { - language: true, - profile: true, - federation: checkMod(ctx, 'invitesMod'), - larp: checkMod(ctx, 'larpMod'), - ux: true, - backup: checkMod(ctx, 'legacyMod'), - greeting: checkMod(ctx, 'feedMod') - }; - try { - const status = await onboardingModel.status(available); - ctx.body = await welcomeView(status, lang, status.profile); - } catch (error) { sendErrorPage(ctx, error.message || String(error), { status: 500 }); } - }) - .post('/welcome/profile', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { - const body = ctx.request.body || {}; - const imageFile = ctx.request.files?.image; - const mime = imageFile?.mimetype || imageFile?.type || ''; - const image = mime.startsWith('image/') && imageFile?.filepath - ? await promisesFs.readFile(imageFile.filepath).catch(() => undefined) - : undefined; - try { - await post.publishProfileEdit({ - name: stripDangerousTags(String(body.name || '')).trim(), - description: stripDangerousTags(String(body.description || '')).trim(), - image - }); - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }); return; } - ctx.redirect('/welcome'); - }) - .post('/welcome/greeting', koaBody(), async (ctx) => { - const text = stripDangerousTags(String((ctx.request.body || {}).text || '')); - try { - const mentions = await extractMentions(text); - await feedModel.createFeed(text, mentions); - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }); return; } - ctx.redirect('/welcome'); - }) - .post('/welcome/ux', koaBody(), async (ctx) => { - const cfg = getConfig(); - const v = String((ctx.request.body || {}).ux || '').trim().toLowerCase(); - const aiNavEnabled = cfg.modules && cfg.modules.aiNavMod === 'on'; - const chatsEnabled = cfg.modules && cfg.modules.chatsMod === 'on'; - const next = (v === 'ainav' && aiNavEnabled) ? 'ainav' : (v === 'chats' && chatsEnabled) ? 'chats' : 'blocks'; - cfg.ux = { ...(cfg.ux && typeof cfg.ux === 'object' ? cfg.ux : {}), current: next }; - saveConfig(cfg); - try { onboardingModel.markStep('ux'); } catch (_) {} - ctx.redirect('/welcome'); - }) - .post('/welcome/dismiss', koaBody(), async (ctx) => { - try { onboardingModel.dismiss(); } catch (_) {} - safeRefererRedirect(ctx, '/'); - }) - .post('/ai/suggestion/dismiss', koaBody(), async (ctx) => { - const current = sharedState.getBestMatch ? sharedState.getBestMatch() : null; - if (current && current.href) sharedState.setDismissedSuggestion(current.href); - safeRefererRedirect(ctx, '/'); + ctx.body = await singleForumView(forumObj, forumMsgs, ctx.query.filter, isReply ? ctx.params.forumId : null, { spreads: spreadInfo }); }) .get('/legacy', async (ctx) => { if (!checkMod(ctx, 'legacyMod')) return ctx.redirect('/modules'); try { ctx.body = await legacyView(); } catch (error) { sendErrorPage(ctx, error.message); } }) - .get('/dev', async (ctx) => { - if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } - if (!checkMod(ctx, 'devMod')) { ctx.redirect('/modules'); return; } - try { - const group = String(ctx.query.group || ''); - const tree = group ? devModel.listGroup(group) : devModel.listTree(ctx.query.path || ''); - ctx.body = await devTreeView(tree, devModel.projectStats()); - } catch (_) { ctx.redirect('/dev'); } - }) - .get('/dev/file', async (ctx) => { - if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } - if (!checkMod(ctx, 'devMod')) { ctx.redirect('/modules'); return; } - try { - ctx.body = await devFileView(devModel.readFile(ctx.query.path || '', { from: ctx.query.from })); - } catch (_) { ctx.redirect('/dev'); } - }) - .get('/dev/raw', async (ctx) => { - if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } - if (!checkMod(ctx, 'devMod')) { ctx.redirect('/modules'); return; } - try { - const file = devModel.rawFile(ctx.query.path || ''); - ctx.set('Content-Type', 'text/plain; charset=utf-8'); - ctx.body = file.content; - } catch (_) { ctx.redirect('/dev'); } - }) - .get('/dev/search', async (ctx) => { - if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } - if (!checkMod(ctx, 'devMod')) { ctx.redirect('/modules'); return; } - const ext = String(ctx.query.ext || ''); - try { - ctx.body = await devSearchView(devModel.searchCode(ctx.query.q || '', { ext }), ext); - } catch (_) { ctx.redirect('/dev'); } - }) - .get('/dev/map', async (ctx) => { - if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } - if (!checkMod(ctx, 'devMod')) { ctx.redirect('/modules'); return; } - try { - ctx.body = await devMapView(devModel.moduleMap()); - } catch (_) { ctx.redirect('/dev'); } - }) .get('/bookmarks', async (ctx) => { if (!checkMod(ctx, 'bookmarksMod')) return ctx.redirect('/modules'); const filter = qf(ctx), q = ctx.query.q || '', sort = ctx.query.sort || 'recent', viewerId = getViewerId(); - const favs = await contentFavorites.getFavoriteSet("bookmarks"); + const favs = await mediaFavorites.getFavoriteSet("bookmarks"); let bookmarks = (await bookmarksModel.listAll({ viewerId, filter: filter === "favorites" ? "all" : filter, q, sort })).map(b => ({ ...b, isFavorite: favs.has(String(b.rootId || b.id)) })); if (filter === "favorites") bookmarks = bookmarks.filter(b => b.isFavorite); bookmarks = await applyListFilters(bookmarks, ctx); @@ -4362,27 +3771,25 @@ router }) .get("/bookmarks/edit/:id", async (ctx) => { if (!checkMod(ctx, 'bookmarksMod')) return ctx.redirect('/modules'); - const bookmark = await bookmarksModel.getBookmarkById(ctx.params.id, getViewerId()), favs = await contentFavorites.getFavoriteSet("bookmarks"); + const bookmark = await bookmarksModel.getBookmarkById(ctx.params.id, getViewerId()), favs = await mediaFavorites.getFavoriteSet("bookmarks"); ctx.body = await bookmarkView([{ ...bookmark, isFavorite: favs.has(String(bookmark.rootId || bookmark.id)) }], "edit", bookmark.id, { returnTo: ctx.query.returnTo || "" }); }) .get('/bookmarks/:bookmarkId', async (ctx) => { if (!checkMod(ctx, 'bookmarksMod')) return ctx.redirect('/modules'); - const filter = qf(ctx), q = ctx.query.q || '', sort = ctx.query.sort || 'recent', favs = await contentFavorites.getFavoriteSet("bookmarks"); + const filter = qf(ctx), q = ctx.query.q || '', sort = ctx.query.sort || 'recent', favs = await mediaFavorites.getFavoriteSet("bookmarks"); const bookmark = await bookmarksModel.getBookmarkById(ctx.params.bookmarkId), root = bookmark.rootId || bookmark.id, comments = await getVoteComments(root); await enrichItemLifetime(bookmark); ctx.body = await singleBookmarkView({ ...bookmark, commentCount: comments.length, isFavorite: favs.has(String(root)) }, filter, comments, { q, sort, returnTo: safeReturnTo(ctx, `/bookmarks?filter=${encodeURIComponent(filter)}`, ['/bookmarks']), spreads: await spreads.forMessage(bookmark.id) }); }) .get('/tasks', async ctx => { const filter = qf(ctx); - const q = String(ctx.query.q || '').trim(); let tasks = await enrichWithComments(await tasksModel.listAll()); - tasks = applyTextSearch(tasks, q, ['title', 'description', 'location', 'tags']); tasks = await applyListFilters(tasks, ctx); try { tasks = await lifetime.enrichAndFilter(tasks); } catch (_) {} await enrichMsgSize(tasks); const spreadMap = await spreads.forMessages((tasks || []).map(x => x && (x.id || x.key))); await warmAuthorNames(tasks); - ctx.body = await taskView(tasks, filter, null, ctx.query.returnTo, { spreadMap, q, prefillTitle: ctx.query.title || '', prefillDescription: ctx.query.description || '' }); + ctx.body = await taskView(tasks, filter, null, ctx.query.returnTo, { spreadMap }); }) .get('/tasks/edit/:id', async ctx => { const id = ctx.params.id; @@ -4394,21 +3801,19 @@ router const comments = await getVoteComments(taskId); await enrichMsgSize([task]); await enrichItemLifetime(task); - ctx.body = await singleTaskView(await withFavorite(withCount(task, comments), 'tasks'), filter, comments, { spreads: await spreads.forMessage(task.id).catch(() => null) }); + ctx.body = await singleTaskView(withCount(task, comments), filter, comments, { spreads: await spreads.forMessage(task.id).catch(() => null) }); }) .get('/events', async (ctx) => { if (!checkMod(ctx, 'eventsMod')) { ctx.redirect('/modules'); return; } const filter = qf(ctx); - const q = String(ctx.query.q || '').trim(); let events = await enrichWithComments(await eventsModel.listAll(null, filter)); - events = applyTextSearch(events, q, ['title', 'description', 'location', 'tags']); events = await applyListFilters(events, ctx); try { events = await lifetime.enrichAndFilter(events); } catch (_) {} await enrichMsgSize(events); const viewerPrefs = await about.visibilityPrefs(getViewerId()).catch(() => null); const spreadMap = await spreads.forMessages((events || []).map(x => x && (x.id || x.key))); await warmAuthorNames(events); - ctx.body = await eventView(events, filter, null, ctx.query.returnTo, { viewerPrefs, spreadMap, q }); + ctx.body = await eventView(events, filter, null, ctx.query.returnTo, { viewerPrefs, spreadMap }); }) .get('/events/edit/:id', async (ctx) => { if (!checkMod(ctx, 'eventsMod')) { ctx.redirect('/modules'); return; } @@ -4429,7 +3834,7 @@ router try { const oi = await eventsModel.getOpenInvite(eventId).catch(() => null); if (oi) event.openInviteCode = oi.code; } catch (_) {} const evAuthorPrefs2 = await about.visibilityPrefs(event.organizer).catch(() => null); await warmAuthorNames(event, comments); - ctx.body = await singleEventView(await withFavorite(withCount(event, comments), 'events'), filter, comments, { mapData, baseUrl: resolveExternalBaseUrl(ctx), authorPrefs: evAuthorPrefs2, linkedCalendarId, spreads: await spreads.forMessage(event.id).catch(() => null) }); + ctx.body = await singleEventView(withCount(event, comments), filter, comments, { mapData, baseUrl: resolveExternalBaseUrl(ctx), authorPrefs: evAuthorPrefs2, linkedCalendarId, spreads: await spreads.forMessage(event.id).catch(() => null) }); }) .get('/c/events/:eventId', async (ctx) => { let event; @@ -4450,28 +3855,26 @@ router }) .get('/votes', async ctx => { const filter = qf(ctx); - const q = String(ctx.query.q || '').trim(); let voteList = await enrichWithComments(await votesModel.listAll(filter)); voteList = await applyListFilters(voteList, ctx); - voteList = applyTextSearch(voteList, q, ['question', 'tags']); try { voteList = await lifetime.enrichAndFilter(voteList, { getAuthor: (x) => x.createdBy }); } catch (_) {} await enrichMsgSize(voteList); const spreadMap = await spreads.forMessages((voteList || []).map(x => x && (x.id || x.key))); await warmAuthorNames(voteList); - ctx.body = await voteView(voteList, filter, null, [], filter, { spreadMap, q, ...voteFormState(ctx) }); + ctx.body = await voteView(voteList, filter, null, [], filter, { spreadMap }); }) .get('/votes/edit/:id', async ctx => { const id = ctx.params.id; const activeFilter = (ctx.query.filter || 'mine'); const voteData = await votesModel.getVoteById(id); - ctx.body = await voteView([voteData], 'edit', id, [], activeFilter, voteFormState(ctx)); + ctx.body = await voteView([voteData], 'edit', id, [], activeFilter); }) .get('/votes/:voteId', async ctx => { const { voteId } = ctx.params, filter = qf(ctx), voteData = await votesModel.getVoteById(voteId); const comments = await getVoteComments(voteId); await enrichMsgSize([voteData]); await enrichItemLifetime(voteData, { author: voteData.createdBy }); - ctx.body = await voteView([await withFavorite(withCount(voteData, comments), 'votes')], 'detail', voteId, comments, filter, { spreads: await spreads.forMessage(voteId).catch(() => null) }); + ctx.body = await voteView([withCount(voteData, comments)], 'detail', voteId, comments, filter, { spreads: await spreads.forMessage(voteId).catch(() => null) }); }) .get("/market", async (ctx) => { if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; } @@ -4486,7 +3889,7 @@ router } const spreadMap = await spreads.forMessages((marketItems || []).map(x => x && (x.id || x.key))); await warmAuthorNames(marketItems); - ctx.body = await marketView(marketItems, filter, null, { q, minPrice, maxPrice, sort, spreadMap, industry: ctx.query.industry || "", title: ctx.query.title || "", description: ctx.query.description || "", price: ctx.query.price || "", tags: ctx.query.tags || "", stock: ctx.query.stock || "" }); + ctx.body = await marketView(marketItems, filter, null, { q, minPrice, maxPrice, sort, spreadMap }); }) .get("/market/edit/:id", async (ctx) => { if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; } @@ -4518,7 +3921,7 @@ router return `/market${params.length ? `?${params.join("&")}` : ""}` })() await enrichItemLifetime(item, { author: item.seller, createdAt: item.updatedAt || item.createdAt }) - ctx.body = await singleMarketView(await withFavorite(withCount(item, comments), 'market'), filter, comments, { q, minPrice, maxPrice, sort, returnTo, mapData, zoom, spreads: await spreads.forMessage(item.id).catch(() => null) }) + ctx.body = await singleMarketView(withCount(item, comments), filter, comments, { q, minPrice, maxPrice, sort, returnTo, mapData, zoom, spreads: await spreads.forMessage(item.id).catch(() => null) }) }) .get('/jobs', async (ctx) => { if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; } @@ -4530,11 +3933,6 @@ router minSalary: ctx.query.minSalary ?? '', maxSalary: ctx.query.maxSalary ?? '', sort: ctx.query.sort || 'recent', - industry: ctx.query.industry || '', - title: ctx.query.title || '', - description: ctx.query.description || '', - tasks: ctx.query.tasks || '', - salary: ctx.query.salary || '', viewerPrefs } if (filter === 'CREATE') { @@ -4606,63 +4004,7 @@ router } const jobAuthorPrefs2 = await about.visibilityPrefs(job.author).catch(() => null); await enrichItemLifetime(job) - ctx.body = await singleJobsView(await withFavorite(withCount(job, comments), 'jobs'), filter, comments, { ...params, mapData, candidates, baseUrl: resolveExternalBaseUrl(ctx), authorPrefs: jobAuthorPrefs2, spreads: await spreads.forMessage(job.id).catch(() => null) }) - }) - .get('/housing', async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - const filter = String(ctx.query.filter || 'ALL').toUpperCase() - const query = { - search: ctx.query.search || '', - minPrice: ctx.query.minPrice ?? '', - maxPrice: ctx.query.maxPrice ?? '', - place: ctx.query.place || '', - sort: ctx.query.sort || 'recent' - } - query.maxImages = housingModel.MAX_IMAGES - if (filter === 'CREATE') { ctx.body = await housingView([], 'CREATE', query); return } - const viewerId = getViewerId() - let items = await housingModel.listHousing(filter, viewerId, query) - await enrichWithComments(items) - items = await applyListFilters(items, ctx) - if (filter !== 'MINE') { - try { items = await lifetime.enrichAndFilter(items); } catch (_) {} - } - await enrichMsgSize(items) - const spreadMap = await spreads.forMessages((items || []).map(x => x && (x.id || x.key))); - await warmAuthorNames(items); - ctx.body = await housingView(items, filter, { ...query, spreadMap }) - }) - .get('/housing/edit/:id', async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - let item; - try { item = await housingModel.getHousingById(ctx.params.id, getViewerId()); } catch (_) { ctx.redirect('/housing?filter=MINE'); return; } - if (!item || String(item.author) !== String(getViewerId())) { ctx.redirect('/housing?filter=MINE'); return; } - ctx.body = await housingView([item], 'EDIT', { maxImages: housingModel.MAX_IMAGES }) - }) - .get('/housing/:housingId', async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - let housingId = ctx.params.housingId - if (housingId && housingId.startsWith('%25')) { - try { housingId = decodeURIComponent(housingId); } catch (_) {} - } - const filter = String(ctx.query.filter || 'ALL').toUpperCase() - const params = { - search: ctx.query.search || '', - minPrice: ctx.query.minPrice ?? '', - maxPrice: ctx.query.maxPrice ?? '', - place: ctx.query.place || '', - sort: ctx.query.sort || 'recent', - returnTo: safeReturnTo(ctx, `/housing?filter=${encodeURIComponent(filter)}`, ['/housing']) - } - let item; - try { item = await housingModel.getHousingById(housingId, getViewerId()); } catch (e) { - sendErrorPage(ctx, `Housing not found or invalid id: ${housingId}`, { status: 404 }); return; - } - if (!item) { sendErrorPage(ctx, `Housing not found: ${housingId}`, { status: 404 }); return; } - const [comments, mapData] = await Promise.all([getVoteComments(housingId), resolveMapUrl(item.mapUrl)]) - await enrichMsgSize([item]) - await enrichItemLifetime(item) - ctx.body = await singleHousingView(await withFavorite(withCount(item, comments), 'housing'), filter, comments, { ...params, mapData, spreads: await spreads.forMessage(item.id).catch(() => null) }) + ctx.body = await singleJobsView(withCount(job, comments), filter, comments, { ...params, mapData, candidates, baseUrl: resolveExternalBaseUrl(ctx), authorPrefs: jobAuthorPrefs2, spreads: await spreads.forMessage(job.id).catch(() => null) }) }) .get('/c/jobs/:jobId', async (ctx) => { let job; @@ -4698,7 +4040,7 @@ router return; } const items = await shopsModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, sort, viewerId: getViewerId() }); - const fav = await contentFavorites.getFavoriteSet('shops'); + const fav = await mediaFavorites.getFavoriteSet('shops'); let enriched = items.map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) })); if (filter === 'favorites') enriched = enriched.filter(x => x.isFavorite); enriched = await applyListFilters(enriched, ctx); @@ -4715,7 +4057,7 @@ router if (!checkMod(ctx, 'shopsMod')) { ctx.redirect('/modules'); return; } const shop = await shopsModel.getShopById(ctx.params.id); if (!shop) { ctx.redirect('/shops'); return; } - const fav = await contentFavorites.getFavoriteSet('shops'); + const fav = await mediaFavorites.getFavoriteSet('shops'); ctx.body = await shopsView([{ ...shop, isFavorite: fav.has(String(shop.rootId || shop.key)) }], 'edit', shop, { returnTo: ctx.query.returnTo || '' }); }) .get("/shops/product/edit/:id", async (ctx) => { @@ -4733,8 +4075,7 @@ router const myPurchases = await shopsModel.listMyPurchases().catch(() => []); const productRootId = product.rootId || product.key; const canRate = myPurchases.some(o => o.productId === productRootId && String(o.status || '').toUpperCase() === 'RECEIVED'); - const productFav = await contentFavorites.getFavoriteSet('shopProducts'); - ctx.body = await singleProductView({ ...withCount(product, comments), isFavorite: productFav.has(String(product.rootId || product.key)) }, shop, comments, { shopId: product.shopId, canRate, returnTo: safeReturnTo(ctx, `/shops/${encodeURIComponent(product.shopId)}`, ['/shops']), spreads: await spreads.forMessage(product.key).catch(() => null) }); + ctx.body = await singleProductView(withCount(product, comments), shop, comments, { shopId: product.shopId, canRate, returnTo: safeReturnTo(ctx, `/shops/${encodeURIComponent(product.shopId)}`, ['/shops']) }); }) .get("/c/audios/:id", async (ctx) => { let item; try { item = await audiosModel.getAudioById(ctx.params.id); } catch (_) {} @@ -5027,7 +4368,7 @@ router const shop = await shopsModel.getShopById(ctx.params.shopId); if (!shop) { ctx.redirect('/shops'); return; } if (shop.encrypted && shop.undecryptable && shop.author !== getViewerId()) { ctx.redirect('/invites#invites-shops'); return; } - const fav = await contentFavorites.getFavoriteSet('shops'); + const fav = await mediaFavorites.getFavoriteSet('shops'); const [products, comments, mapData] = await Promise.all([shopsModel.listProducts(shop.rootId || shop.key), getVoteComments(shop.key), resolveMapUrl(shop.mapUrl)]); const baseUrl = resolveExternalBaseUrl(ctx); const authorPrefs = await about.visibilityPrefs(shop.author).catch(() => null); @@ -5072,7 +4413,7 @@ router } const modelFilter = filter === "favorites" ? "all" : filter; const items = await chatsModel.listAll({ filter: modelFilter, q, viewerId }); - const fav = await contentFavorites.getFavoriteSet('chats'); + const fav = await mediaFavorites.getFavoriteSet('chats'); const myTribeIds = await getUserTribeIds(viewerId); const enriched = items.filter(x => !x.tribeId).map(x => ({ ...x, isFavorite: fav.has(String(x.rootId || x.key)) })); let finalList = filter === "favorites" ? enriched.filter(x => x.isFavorite) : enriched; @@ -5080,14 +4421,7 @@ router try { finalList = await lifetime.enrichAndFilter(finalList, { getKey: (x) => x.rootId || x.key }); } catch (_) {} const spreadMap = await spreads.forMessages((finalList || []).map(x => x && (x.key || x.id))); await warmAuthorNames(finalList); - const uxChatsMode = getConfig().ux?.current === 'chats'; - if (uxChatsMode && filter === 'all' && !q && finalList.length) { - const actTs = (c) => Math.max(Number(c.lastMsgAt || 0), Date.parse(c.updatedAt || '') || 0, Date.parse(c.createdAt || '') || 0); - const first = finalList.slice().sort((a, b) => actTs(b) - actTs(a))[0]; - ctx.redirect(`/chats/${encodeURIComponent(first.rootId || first.key)}`); - return; - } - ctx.body = await chatsView(finalList, filter, null, { q, spreadMap, workspace: uxChatsMode }); + ctx.body = await chatsView(finalList, filter, null, { q, spreadMap }); }) .get("/chats/edit/:id", async (ctx) => { if (!checkMod(ctx, 'chatsMod')) { ctx.redirect('/modules'); return; } @@ -5115,23 +4449,10 @@ router const isOpen = String(chat.status || '').toUpperCase() === 'OPEN'; if (!isOpen && chat.author !== uid && !members.includes(uid)) { ctx.redirect('/chats?filter=all'); return; } } - const fav = await contentFavorites.getFavoriteSet('chats'); + const fav = await mediaFavorites.getFavoriteSet('chats'); const messages = await chatsModel.listMessages(chat.rootId || chat.key); const isTribeMember = !!parentTribe; - const pollsEnabled = checkMod(ctx, 'pollsMod'); - const chatPolls = pollsEnabled - ? await pollsModel.listAll('ALL', { chatId: chat.rootId || chat.key }).catch(() => []) - : []; - const uxChats = getConfig().ux?.current === 'chats'; - // la rama usa la misma lista para el selector de chats en movil - const forkMobileChats = process.env.OASIS_MOBILE === '1'; - let allChats = []; - if (uxChats || forkMobileChats) { - try { - allChats = (await chatsModel.listAll({ filter: 'all', q: '', viewerId: uid })).filter(x => !x.tribeId); - } catch (_) { allChats = []; } - } - ctx.body = await singleChatView({ ...chat, isFavorite: fav.has(String(chat.rootId || chat.key)), isTribeMember }, filter, messages, { q, replyTo: String(ctx.query.replyTo || ''), chatList: forkMobileChats ? allChats : [], polls: chatPolls, pollsEnabled, returnTo: safeReturnTo(ctx, `/chats?filter=${encodeURIComponent(filter)}`, ['/chats']), spreads: await spreads.forMessage(chat.key).catch(() => null), workspace: uxChats, allChats }); + ctx.body = await singleChatView({ ...chat, isFavorite: fav.has(String(chat.rootId || chat.key)), isTribeMember }, filter, messages, { q, returnTo: safeReturnTo(ctx, `/chats?filter=${encodeURIComponent(filter)}`, ['/chats']) }); }) .get("/pads", async (ctx) => { if (!checkMod(ctx, 'padsMod')) { ctx.redirect('/modules'); return; } @@ -5148,7 +4469,7 @@ router const q = String(ctx.query.q || "").trim(); const tribeId = ctx.query.tribeId || ""; const pads = await padsModel.listAll({ filter, viewerId: uid }); - const fav = await contentFavorites.getFavoriteSet('pads'); + const fav = await mediaFavorites.getFavoriteSet('pads'); let enriched = pads.filter(p => !p.tribeId).map(p => ({ ...p, isFavorite: fav.has(String(p.rootId)) })); enriched = await applyListFilters(enriched, ctx); try { enriched = await lifetime.enrichAndFilter(enriched, { getKey: (x) => x.rootId || x.key }); } catch (_) {} @@ -5175,7 +4496,7 @@ router const isOpen = String(pad.status || '').toUpperCase() === 'OPEN'; if (!isOpen && pad.author !== uid && !members.includes(uid)) { ctx.redirect('/pads?filter=all'); return; } } - const fav = await contentFavorites.getFavoriteSet('pads'); + const fav = await mediaFavorites.getFavoriteSet('pads'); const entries = await padsModel.getEntries(pad.rootId); const versionKey = ctx.query.version || null; const selectedVersion = versionKey @@ -5201,7 +4522,7 @@ router const tribeId = ctx.query.tribeId || ""; const modelFilter = filter === "favorites" ? "all" : filter; const calendars = await calendarsModel.listAll({ filter: modelFilter, viewerId: uid }); - const fav = await contentFavorites.getFavoriteSet('calendars'); + const fav = await mediaFavorites.getFavoriteSet('calendars'); const myTribeIds = await getUserTribeIds(uid); const enriched = calendars.filter(c => !c.tribeId).map(c => ({ ...c, isFavorite: fav.has(String(c.rootId)) })); let finalList = filter === "favorites" ? enriched.filter(c => c.isFavorite) : enriched; @@ -5236,7 +4557,7 @@ router for (const d of dates) { notesByDate[d.key] = await calendarsModel.getNotesForDate(cal.rootId, d.key); } - const fav = await contentFavorites.getFavoriteSet('calendars'); + const fav = await mediaFavorites.getFavoriteSet('calendars'); const month = String(ctx.query.month || "").trim() || null; const day = String(ctx.query.day || "").trim() || null; await enrichItemLifetime(cal, { key: cal.rootId }); @@ -5247,24 +4568,18 @@ router const filter = String(ctx.query.filter || "ALL").toUpperCase() const viewerPrefs = await about.visibilityPrefs(getViewerId()).catch(() => null); if (filter === "CREATE") { - const prefill = { - title: String(ctx.query.title || ""), - description: String(ctx.query.description || ""), - goal: String(ctx.query.goal || "") - } - ctx.body = await projectsView([], "CREATE", null, { viewerPrefs, prefill }) + ctx.body = await projectsView([], "CREATE", null, { viewerPrefs }) return } - const q = String(ctx.query.q || '').trim() const modelFilter = filter === "BACKERS" ? "ALL" : filter - let projects = await projectsModel.listProjects(modelFilter, { q }) + let projects = await projectsModel.listProjects(modelFilter) await enrichWithComments(projects) projects = await applyListFilters(projects, ctx) try { projects = await lifetime.enrichAndFilter(projects); } catch (_) {} await enrichMsgSize(projects) const spreadMap = await spreads.forMessages((projects || []).map(x => x && (x.id || x.key))); await warmAuthorNames(projects); - ctx.body = await projectsView(projects, filter, null, { viewerPrefs, spreadMap, q }) + ctx.body = await projectsView(projects, filter, null, { viewerPrefs, spreadMap }) }) .get("/projects/edit/:id", async (ctx) => { if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; } @@ -5281,7 +4596,7 @@ router const [comments, mapData] = await Promise.all([getVoteComments(projectId), resolveMapUrl(project.mapUrl)]) await enrichMsgSize([project]) await enrichItemLifetime(project, { key: project.id || project.key }) - ctx.body = await singleProjectView(await withFavorite(withCount(project, comments), 'projects'), filter, comments, { mapData, zoom, baseUrl: resolveExternalBaseUrl(ctx), spreads: await spreads.forMessage(project.id).catch(() => null) }) + ctx.body = await singleProjectView(withCount(project, comments), filter, comments, { mapData, zoom, baseUrl: resolveExternalBaseUrl(ctx), spreads: await spreads.forMessage(project.id).catch(() => null) }) }) .get("/c/projects/:projectId", async (ctx) => { let project; @@ -5300,242 +4615,6 @@ router ctx.type = 'text/html'; ctx.body = await clearnetProjectView(project); }) - .get("/industry", async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - const filter = String(ctx.query.filter || "ALL").toUpperCase() - if (filter === "CREATE" || filter === "RULES") { ctx.body = await industryView([], filter); return } - if (filter === "BLUEPRINTS" || filter === "BUILDS") { - const q = String(ctx.query.search || "").trim().toLowerCase() - let items = filter === "BLUEPRINTS" - ? await industryModel.listAllBlueprints().catch(() => []) - : await industryModel.listAllBuilds().catch(() => []) - if (q) items = items.filter(x => [x.name, x.title, x.description, x.notes, x.facilityName].some(v => String(v || "").toLowerCase().includes(q))) - const spreadMap = await spreads.forMessages((items || []).map(x => x && (x.id || x.key))).catch(() => new Map()) - ctx.body = await industryView(items, filter, { spreadMap }) - return - } - const search = String(ctx.query.search || "").trim() - const sector = String(ctx.query.sector || "").trim().toLowerCase() - let facilities = await industryModel.listFacilities(filter) - if (sector) facilities = facilities.filter(x => x.sector === sector) - if (search) { - const q = search.toLowerCase() - facilities = facilities.filter(x => [x.name, x.description, x.sector].some(v => String(v || "").toLowerCase().includes(q))) - } - try { facilities = await lifetime.enrichAndFilter(facilities); } catch (_) {} - await enrichMsgSize(facilities) - const spreadMap = await spreads.forMessages((facilities || []).map(x => x && (x.id || x.key))); - await warmAuthorNames(facilities); - ctx.body = await industryView(facilities, filter, { spreadMap, search, sector }) - }) - .get("/industry/edit/:id", async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - const fc = await industryModel.getFacilityById(ctx.params.id) - ctx.body = await industryView([fc], "EDIT") - }) - .get("/industry/build/:id", async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const build = await industryModel.getBuild(ctx.params.id) - ctx.body = await singleBuildView(build, { kind: ctx.query.kind, spreads: await spreads.forMessage(build.id).catch(() => null) }) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 404 }) } - }) - .get("/industry/builds/:id/edit", async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const build = await industryModel.getBuild(ctx.params.id) - const fc = await industryModel.getFacilityById(build.facilityId) - ctx.body = await buildEditView(build, fc) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 404 }) } - }) - .get("/industry/blueprint/:id", async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const bp = await industryModel.getBlueprint(ctx.params.id) - ctx.body = await singleBlueprintView(bp, { spreads: await spreads.forMessage(bp.id).catch(() => null) }) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 404 }) } - }) - .get("/industry/blueprints/:id/edit", async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const bp = await industryModel.getBlueprint(ctx.params.id) - const fc = await industryModel.getFacilityById(bp.facility) - ctx.body = await blueprintEditView(bp, fc) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 404 }) } - }) - .get("/industry/:id", async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - const filter = String(ctx.query.filter || "ALL").toUpperCase() - const facility = await industryModel.getFacilityById(ctx.params.id) - const zoom = parseInt(ctx.query.zoom) || 2; - const [mapData, blueprints, builds, allJobs] = await Promise.all([ - resolveMapUrl(facility.mapUrl), - industryModel.listBlueprints(ctx.params.id).catch(() => []), - industryModel.listBuilds(ctx.params.id, 'ALL').catch(() => []), - checkMod(ctx, 'jobsMod') ? jobsModel.listJobs('ALL', getViewerId()).catch(() => []) : Promise.resolve([]) - ]) - const facilityRef = facility.root || facility.id - const facilityJobs = (allJobs || []).filter(j => j && j.industry === facilityRef) - await enrichMsgSize([facility]) - await enrichItemLifetime(facility, { key: facility.id || facility.key }) - const childSpreadMap = await spreads.forMessages([...(blueprints || []), ...(builds || [])].map(x => x && (x.id || x.key))).catch(() => new Map()) - ctx.body = await singleFacilityView(facility, filter, { mapData, zoom, blueprints, builds, facilityJobs, childSpreadMap, spreads: await spreads.forMessage(facility.id).catch(() => null) }) - }) - .post("/industry/create", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const image = ctx.request.files?.image ? await handleBlobUpload(ctx, "image") : null - const res = await industryModel.createFacility({ ...(ctx.request.body || {}), image }) - ctx.redirect(`/industry/${encodeURIComponent(res.key)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const image = ctx.request.files?.image ? await handleBlobUpload(ctx, "image") : undefined - await industryModel.updateFacility(ctx.params.id, { ...(ctx.request.body || {}), ...(image !== undefined ? { image } : {}) }) - ctx.redirect(`/industry/${encodeURIComponent(ctx.params.id)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/delete/:id", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { await industryModel.deleteFacility(ctx.params.id); ctx.redirect('/industry?filter=MINE') } - catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/join/:id", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { await industryModel.joinFacility(ctx.params.id); safeRefererRedirect(ctx, `/industry/${encodeURIComponent(ctx.params.id)}`) } - catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/apply/:id", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const before = await industryModel.getFacilityById(ctx.params.id).catch(() => null) - await industryModel.joinFacility(ctx.params.id) - if (before && before.membershipPolicy === 'vote') { - const me = getViewerId() - for (const m of (before.members || [])) { if (m !== me) { try { await pmModel.sendMessage([m], "INDUSTRY_APPLICATION", `A new habitant requested to join [${before.name || 'a facility'}](/industry/${encodeURIComponent(ctx.params.id)})`); } catch (_) {} } } - } - safeRefererRedirect(ctx, `/industry/${encodeURIComponent(ctx.params.id)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/leave/:id", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { await industryModel.leaveFacility(ctx.params.id); safeRefererRedirect(ctx, `/industry/${encodeURIComponent(ctx.params.id)}`) } - catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/invite/:id", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - const invitee = ctx.request.body.invitee - try { - await industryModel.inviteToFacility(ctx.params.id, invitee) - try { const fc = await industryModel.getFacilityById(ctx.params.id); await pmModel.sendMessage([invitee], "INDUSTRY_INVITED", `You have been invited to join [${fc.name || 'a facility'}](/industry/${encodeURIComponent(ctx.params.id)})`); } catch (_) {} - safeRefererRedirect(ctx, `/industry/${encodeURIComponent(ctx.params.id)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/govern/:id", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - const subject = ctx.request.body.subject, ref = ctx.request.body.ref - try { - const before = await industryModel.getFacilityById(ctx.params.id).catch(() => null) - await industryModel.voteGovernance(ctx.params.id, subject, ref, ctx.request.body.choice) - const after = await industryModel.getFacilityById(ctx.params.id).catch(() => null) - const me = getViewerId() - if (subject === 'admit' && ref && before && after && !before.members.includes(ref) && after.members.includes(ref)) { - try { await pmModel.sendMessage([ref], "INDUSTRY_ADMITTED", `You have been admitted to [${after.name || 'a facility'}](/industry/${encodeURIComponent(ctx.params.id)})`); } catch (_) {} - } - if (subject === 'dissolve' && before && after && before.status !== 'DISSOLVED' && after.status === 'DISSOLVED') { - for (const m of (after.members || [])) { if (m !== me) { try { await pmModel.sendMessage([m], "INDUSTRY_DISSOLVED", `The facility [${after.name || 'a facility'}](/industry/${encodeURIComponent(ctx.params.id)}) has been dissolved by collective vote`); } catch (_) {} } } - } - safeRefererRedirect(ctx, `/industry/${encodeURIComponent(ctx.params.id)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post('/industry/opinions/:id/:category', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { await industryModel.createOpinion(ctx.params.id, ctx.params.category); } - catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }); return; } - safeRefererRedirect(ctx, `/industry/${encodeURIComponent(ctx.params.id)}`) - }) - .post("/industry/:fid/blueprints", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const image = ctx.request.files?.image ? await handleBlobUpload(ctx, "image") : null - await industryModel.createBlueprint(ctx.params.fid, { ...(ctx.request.body || {}), image }) - safeRefererRedirect(ctx, `/industry/${encodeURIComponent(ctx.params.fid)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/blueprints/:id/update", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const image = ctx.request.files?.image ? await handleBlobUpload(ctx, "image") : undefined - await industryModel.updateBlueprint(ctx.params.id, { ...(ctx.request.body || {}), ...(image !== undefined ? { image } : {}) }) - const bp = await industryModel.getBlueprint(ctx.params.id).catch(() => null) - ctx.redirect(bp ? `/industry/${encodeURIComponent(bp.facility)}` : '/industry') - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/blueprints/:id/delete", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - const bp = await industryModel.getBlueprint(ctx.params.id).catch(() => null) - try { await industryModel.deleteBlueprint(ctx.params.id) } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }); return } - ctx.redirect(bp && bp.facilityId ? `/industry/${encodeURIComponent(bp.facilityId)}` : '/industry?filter=BLUEPRINTS') - }) - .post("/industry/:fid/builds", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const image = ctx.request.files?.image ? await handleBlobUpload(ctx, "image") : null - const res = await industryModel.createBuild(ctx.params.fid, { ...(ctx.request.body || {}), image }) - ctx.redirect(`/industry/build/${encodeURIComponent(res.key)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/builds/:id/update", koaBody(), async (ctx) => { - if (!checkMod(ctx, "industryMod")) { ctx.redirect("/modules"); return; } - try { - await industryModel.updateBuild(ctx.params.id, ctx.request.body || {}) - safeRefererRedirect(ctx, `/industry/build/${encodeURIComponent(ctx.params.id)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/builds/:id/delete", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { await industryModel.deleteBuild(ctx.params.id); ctx.redirect('/industry?filter=BUILDS') } - catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/builds/:id/vote", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const before = await industryModel.getBuild(ctx.params.id).catch(() => null) - await industryModel.voteBuild(ctx.params.id, ctx.request.body.choice) - const after = await industryModel.getBuild(ctx.params.id).catch(() => null) - if (before && after && before.status !== 'APPROVED' && after.status === 'APPROVED' && after.proposer !== getViewerId()) { - try { await pmModel.sendMessage([after.proposer], "INDUSTRY_BUILD_APPROVED", `Your build [${after.title || 'a build'}](/industry/build/${encodeURIComponent(ctx.params.id)}) has been approved`); } catch (_) {} - } - safeRefererRedirect(ctx, `/industry/build/${encodeURIComponent(ctx.params.id)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/builds/:id/status", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { await industryModel.setBuildStatus(ctx.params.id, ctx.request.body.status); safeRefererRedirect(ctx, `/industry/build/${encodeURIComponent(ctx.params.id)}`) } - catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/builds/:id/contribute", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - const b = ctx.request.body || {} - try { - await industryModel.contribute(ctx.params.id, b) - safeRefererRedirect(ctx, `/industry/build/${encodeURIComponent(ctx.params.id)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) - .post("/industry/builds/:id/distribute", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } - try { - const viewer = getViewerId() - const res = await industryModel.distributeBuild(ctx.params.id, { outputValue: ctx.request.body.outputValue }) - for (const alloc of res.plan.allocations) { - if (!alloc || !alloc.to || alloc.to === viewer || !(alloc.amount > 0)) continue - try { await pmModel.sendMessage([alloc.to], "INDUSTRY_DISTRIBUTED", `Build [${res.build.title || 'a build'}](/industry/build/${encodeURIComponent(ctx.params.id)}) allocated you ${alloc.amount.toFixed(2)} ECO`) } catch (_) {} - } - safeRefererRedirect(ctx, `/industry/build/${encodeURIComponent(ctx.params.id)}`) - } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }) } - }) .get("/banking", async (ctx) => { if (!checkMod(ctx, 'bankingMod')) { ctx.redirect('/modules'); return; } const userId = getViewerId(); @@ -5656,10 +4735,8 @@ router ctx.body = renderEpochView(epoch, allocations); }) .get("/favorites", async (ctx) => { - const filter = qf(ctx), q = String(ctx.query.q || '').trim(); - const data = await favoritesModel.listAll({ filter }); - const items = applyTextSearch(data.items, q, ['title', 'name', 'description', 'category', 'url', 'tags']); - ctx.body = await favoritesView(items, filter, data.counts, q); + const filter = qf(ctx), data = await favoritesModel.listAll({ filter }); + ctx.body = await favoritesView(data.items, filter, data.counts); }) .get("/logs", async (ctx) => { if (!checkMod(ctx, 'logsMod')) { ctx.redirect('/modules'); return; } @@ -5670,6 +4747,13 @@ router ctx.body = logsView([], 'today', mode, { view: 'create', aiModOn }); return; } + if (view === 'edit') { + const id = String(ctx.query.id || ''); + const entry = id ? await logsModel.getLogById(id) : null; + if (!entry) { ctx.redirect('/logs'); return; } + ctx.body = logsView([], 'today', entry.mode, { view: 'edit', aiModOn, entry }); + return; + } const filter = qf(ctx, 'today'); const q = String(ctx.query.q || '').trim().toLowerCase(); const typeQ = String(ctx.query.type || '').trim().toLowerCase(); @@ -5686,8 +4770,7 @@ router }) .get("/logs/view/:id", async (ctx) => { if (!checkMod(ctx, 'logsMod')) { ctx.redirect('/modules'); return; } - const entries = await logsModel.listLogs('always'); - const entry = entries.find(e => e.key === ctx.params.id); + const entry = await logsModel.getLogById(ctx.params.id); if (!entry) { ctx.redirect('/logs'); return; } const aiModOn = logsModel.isAImodOn(); ctx.body = logsView([], 'today', entry.mode, { view: 'detail', aiModOn, entry }); @@ -5702,17 +4785,11 @@ router } catch (_) {} ctx.redirect('/logs'); }) - .get("/logs/edit/:id", async (ctx) => { - if (!checkMod(ctx, 'logsMod')) { ctx.redirect('/modules'); return; } - const entry = await logsModel.getLogById(ctx.params.id); - if (!entry) { ctx.redirect('/logs'); return; } - ctx.body = logsView([], 'today', entry.mode, { view: 'edit', aiModOn: logsModel.isAImodOn(), entry }); - }) - .post("/logs/update/:id", koaBody(), async (ctx) => { + .post("/logs/edit/:id", koaBody(), async (ctx) => { if (!checkMod(ctx, 'logsMod')) { ctx.redirect('/modules'); return; } const b = ctx.request.body || {}; try { await logsModel.updateLog(ctx.params.id, { label: b.label || '', text: b.text || '' }); } catch (_) {} - ctx.redirect(`/logs/view/${encodeURIComponent(ctx.params.id)}`); + ctx.redirect('/logs'); }) .post("/logs/delete/:id", koaBody(), async (ctx) => { if (!checkMod(ctx, 'logsMod')) { ctx.redirect('/modules'); return; } @@ -5746,10 +4823,6 @@ router }) .get("/thread/:message", async (ctx) => { const { message } = ctx.params; - if (checkMod(ctx, 'blogsMod')) { - const target = await blogModel.blogHrefFor(message).catch(() => null); - if (target) { ctx.redirect(target); return; } - } const thread = async (message) => { const messages = await post.fromThread(message); const spreadMap = await spreads.forMessages((messages || []).map(m => m && m.key)).catch(() => new Map()); @@ -5767,7 +4840,7 @@ router ctx.body = await subtopicView({ messages, myFeedId, spreadMap }); }) .get("/publish", async (ctx) => { - ctx.redirect('/blogs?filter=CREATE'); + ctx.body = await publishView(); }) .get("/comment/:message", async (ctx) => { const { messages, myFeedId, parentMessage } = @@ -5874,46 +4947,6 @@ router const tr = await transfersModel.getTransferById(ctx.params.id, getViewerId()); ctx.body = await transferView([tr], 'edit', ctx.params.id, {}); }) - .post('/transfers/:id/share', koaBody(), async ctx => { - if (!checkMod(ctx, 'transfersMod')) { ctx.redirect('/modules'); return; } - const transfer = await transfersModel.getTransferById(ctx.params.id, getViewerId()).catch(() => null); - if (!transfer) { ctx.redirect('/transfers'); return; } - const pdf = buildSmartContractPdf({ transfer, block: null, viewerId: null }); - const slug = String(transfer.concept || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40); - await sharePdfBuffer(ctx, pdf, `oasis-smart-contract-${slug || 'transfer'}.pdf`, transfer.concept || ''); - }) - .get('/reports/:id/pdf', async ctx => sendContentPdf(ctx, 'reports', ctx.params.id)) - .post('/reports/:id/share', koaBody(), async ctx => sharePdfAsPm(ctx, 'reports', ctx.params.id)) - .get('/votes/:id/pdf', async ctx => sendContentPdf(ctx, 'votes', ctx.params.id)) - .post('/votes/:id/share', koaBody(), async ctx => sharePdfAsPm(ctx, 'votes', ctx.params.id)) - .get('/events/:id/pdf', async ctx => sendContentPdf(ctx, 'events', ctx.params.id)) - .post('/events/:id/share', koaBody(), async ctx => sharePdfAsPm(ctx, 'events', ctx.params.id)) - .get('/tasks/:id/pdf', async ctx => sendContentPdf(ctx, 'tasks', ctx.params.id)) - .post('/tasks/:id/share', koaBody(), async ctx => sharePdfAsPm(ctx, 'tasks', ctx.params.id)) - .get('/calendars/:id/pdf', async ctx => sendContentPdf(ctx, 'calendars', ctx.params.id)) - .post('/calendars/:id/share', koaBody(), async ctx => sharePdfAsPm(ctx, 'calendars', ctx.params.id)) - .get('/cv/pdf', async ctx => sendContentPdf(ctx, 'cv', null)) - .get('/cv/pdf/:id', async ctx => { - const targetId = ctx.params.id; - const viewerId = getViewerId(); - const cv = await cvModel.getCVByUserId(targetId).catch(() => null); - const hidden = cv && String(cv.visibility || '').toUpperCase() === 'HIDDEN'; - if (!cv || (hidden && String(targetId) !== String(viewerId))) { ctx.redirect('/inhabitants?filter=CVs'); return; } - const pdf = buildContentPdf('cv', cv, {}, String(targetId) === String(viewerId) ? viewerId : null); - ctx.set('Content-Type', 'application/pdf'); - ctx.set('Content-Disposition', `attachment; filename="${pdfFilename('cv', cv)}"`); - ctx.body = pdf; - }) - .post('/cv/share', koaBody(), async ctx => sharePdfAsPm(ctx, 'cv', null)) - .post('/cv/share/:id', koaBody(), async ctx => { - const targetId = ctx.params.id; - const viewerId = getViewerId(); - const cv = await cvModel.getCVByUserId(targetId).catch(() => null); - const hidden = cv && String(cv.visibility || '').toUpperCase() === 'HIDDEN'; - if (!cv || (hidden && String(targetId) !== String(viewerId))) { ctx.redirect('/inhabitants?filter=CVs'); return; } - const pdf = buildContentPdf('cv', cv, {}, null); - await sharePdfBuffer(ctx, pdf, pdfFilename('cv', cv), cv.name || targetId); - }) .get('/transfers/contract/:transferId', async ctx => { if (!checkMod(ctx, 'transfersMod')) { ctx.redirect('/modules'); return; } const transfer = await transfersModel.getTransferById(ctx.params.transferId, getViewerId()); @@ -5964,10 +4997,7 @@ router } catch (_) { block = null; } } await enrichItemLifetime(transfer, { author: transfer.from }); - const comments = await getVoteComments(transfer.id); - const fav = await contentFavorites.getFavoriteSet('transfers'); - await warmAuthorNames([transfer], comments); - ctx.body = await singleTransferView({ ...transfer, isFavorite: fav.has(String(transfer.id)) }, filter, { q: ctx.query.q || '', minAmount: ctx.query.minAmount ?? '', maxAmount: ctx.query.maxAmount ?? '', sort: ctx.query.sort || 'recent', returnTo: safeReturnTo(ctx, `/transfers?filter=${encodeURIComponent(filter)}`, ['/transfers']), block, comments, spreads: await spreads.forMessage(transfer.id).catch(() => null) }); + ctx.body = await singleTransferView(transfer, filter, { q: ctx.query.q || '', minAmount: ctx.query.minAmount ?? '', maxAmount: ctx.query.maxAmount ?? '', sort: ctx.query.sort || 'recent', returnTo: safeReturnTo(ctx, `/transfers?filter=${encodeURIComponent(filter)}`, ['/transfers']), block, spreads: await spreads.forMessage(transfer.id).catch(() => null) }); }) .post('/ai', koaBody(), async (ctx) => { const { input } = ctx.request.body; @@ -6126,6 +5156,7 @@ router if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } const raw = String(ctx.query?.q || ctx.query?.prompt || '').trim(); if (!raw) { ctx.redirect('/'); return; } + if (!checkMod(ctx, 'aiNavMod')) { ctx.redirect('/search?query=' + encodeURIComponent(raw)); return; } const routesIndex = require('../AI/routes_index'); const { aiNavResultsView } = require('../views/main_views'); const isModuleEnabled = (modName) => checkMod(ctx, modName); @@ -6155,7 +5186,7 @@ router const raw = String(ctx.request.body?.q || ctx.request.body?.prompt || '').trim(); if (!raw) { ctx.redirect('/'); return; } const ssbRefLib = require('../server/node_modules/ssb-ref'); - const hashtagMatch = raw.match(/^#([0-9A-Za-z_À-￿-]+)/); // sin \p{}: nodejs-mobile no lleva ICU completo + const hashtagMatch = raw.match(/^#([0-9A-Za-z_À-￿-]+)/); if (hashtagMatch) { ctx.redirect('/search?query=' + encodeURIComponent('#' + hashtagMatch[1])); return; @@ -6174,17 +5205,18 @@ router try { const embedder = require('../AI/embedder'); const routesIndex = require('../AI/routes_index'); - const isModuleEnabled = (modName) => checkMod(ctx, modName); - if (embedder.isInstalled()) { - const vec = await embedder.embed(raw); - if (vec) { - const best = await routesIndex.resolveBest(vec, { isModuleEnabled, embed: embedder.embed }); - if (best && best.path) { ctx.redirect(best.path); return; } - } + if (!embedder.isInstalled()) { + ctx.redirect('/search?query=' + encodeURIComponent(raw)); + return; } - const { aiNavResultsView } = require('../views/main_views'); - const results = routesIndex.resolveKeywordTopK({ isModuleEnabled }, raw, 8); - if (Array.isArray(results) && results.length) { ctx.body = await aiNavResultsView({ query: raw, results }); return; } + const vec = await embedder.embed(raw); + if (!vec) { + ctx.redirect('/search?query=' + encodeURIComponent(raw)); + return; + } + const isModuleEnabled = (modName) => checkMod(ctx, modName); + const best = await routesIndex.resolveBest(vec, { isModuleEnabled, embed: embedder.embed }); + if (best && best.path) { ctx.redirect(best.path); return; } } catch (_) {} ctx.redirect('/search?query=' + encodeURIComponent(raw)); }) @@ -6208,11 +5240,10 @@ router const viewer = getViewerId(); for (const rid of recipientsArr) { if (rid === viewer) continue; - let rel = null; + let rel; try { rel = await friend.getRelationship(rid); } catch (e) { rel = null; } - if (!pmPolicy.isRecipientAllowed({ pmVisibility: cfgNow.pmVisibility, viewerId: viewer, recipientId: rid, relationship: rel })) { - ctx.throw(403, 'You can only send private messages to habitants with mutual support.'); - } + const mutual = !!(rel && rel.following && rel.followsMe); + if (!mutual) ctx.throw(403, 'You can only send private messages to habitants with mutual support.'); } } const cleanSubject = stripDangerousTags(subject); @@ -6239,15 +5270,7 @@ router ctx.body = await pmView('', '', '', false, key); return; } - try { - await pmModel.sendMessage(recipientsArr, cleanSubject, cleanText); - } catch (e) { - if (isSsbTooLargeError(e)) { - sendErrorPage(ctx, require('../views/main_views').i18n.publishTooLong || 'Your message is too long. Please shorten it.', { status: 400 }); - return; - } - throw e; - } + await pmModel.sendMessage(recipientsArr, cleanSubject, cleanText); await refreshInboxCount(); ctx.redirect('/inbox?filter=sent'); }) @@ -6270,135 +5293,6 @@ router } ctx.body = await pmView(recipients, subject, text, true); }) - .post('/pm/file/preview', koaBodyFileshare, async ctx => { - const b = ctx.request.body || {}; - const file = ctx.request.files && (ctx.request.files.file || ctx.request.files.blob); - const cleanup = () => { try { if (file && file.filepath) fs.unlinkSync(file.filepath); } catch (_) {} }; - const recipient = String(b.recipient || '').trim(); - if (!ssbRef.isFeedId(recipient)) { cleanup(); ctx.redirect('/pm?fileerror=recipient#fileshare'); return; } - const cfgNow = getConfig(); - if (cfgNow.pmVisibility === 'mutuals' && recipient !== getViewerId()) { - let rel = null; - try { rel = await friend.getRelationship(recipient); } catch (_) { rel = null; } - if (!pmPolicy.isRecipientAllowed({ pmVisibility: cfgNow.pmVisibility, viewerId: getViewerId(), recipientId: recipient, relationship: rel })) { - cleanup(); ctx.redirect('/pm?fileerror=mutual#fileshare'); return; - } - } - if (!file || !file.filepath || !file.size) { cleanup(); ctx.redirect('/pm?fileerror=nofile#fileshare'); return; } - if (file.size > FILESHARE_MAX_SIZE) { cleanup(); ctx.redirect('/pm?fileerror=size#fileshare'); return; } - let pointer; - try { - pointer = await fileshareModel.createShareFromFile({ - filepath: file.filepath, - filename: file.originalFilename || file.name || 'file', - mime: file.mimetype || null - }); - } catch (_) { cleanup(); ctx.redirect('/pm?fileerror=failed#fileshare'); return; } - cleanup(); - const useCrypter = !!b.crypter; - const sharedKey = useCrypter ? cipherModel.generateKey() : ''; - ctx.body = await pmView(recipient, stripDangerousTags(b.subject || ''), '', false, '', false, null, false, '', { - recipient, subject: stripDangerousTags(b.subject || ''), - manifestBlobId: pointer.manifestBlobId, keyHex: pointer.key, - filename: pointer.filename, mime: pointer.mime, size: pointer.size, - sizeLabel: formatFileSize(pointer.size), crypter: useCrypter, sharedKey - }); - }) - .post('/pm/file', koaBodyFileshare, async ctx => { - const b = ctx.request.body || {}; - const file = ctx.request.files && (ctx.request.files.file || ctx.request.files.blob); - const cleanup = () => { try { if (file && file.filepath) fs.unlinkSync(file.filepath); } catch (_) {} }; - const recipient = String(b.recipient || '').trim(); - if (!ssbRef.isFeedId(recipient)) { cleanup(); ctx.redirect('/pm?fileerror=recipient#fileshare'); return; } - const cfgNow = getConfig(); - if (cfgNow.pmVisibility === 'mutuals' && recipient !== getViewerId()) { - let rel = null; - try { rel = await friend.getRelationship(recipient); } catch (_) { rel = null; } - if (!pmPolicy.isRecipientAllowed({ pmVisibility: cfgNow.pmVisibility, viewerId: getViewerId(), recipientId: recipient, relationship: rel })) { - cleanup(); ctx.redirect('/pm?fileerror=mutual#fileshare'); return; - } - } - if (!file && b.manifestBlobId) { - let pointer = { - type: 'fileShare', v: 1, key: String(b.keyHex || ''), - manifestBlobId: String(b.manifestBlobId), filename: String(b.filename || 'file'), - mime: String(b.mime || 'application/octet-stream'), size: Number(b.size) || 0 - }; - const useCrypter = !!b.crypter; - if (useCrypter) { - const sk = String(b.sharedKey || ''); - if (sk.length >= 32) { const { encryptedText } = cipherModel.encryptData(pointer.key, sk); pointer = { ...pointer, key: encryptedText, crypter: true }; } - } - try { await pmModel.sendFileShare([recipient], stripDangerousTags(b.subject || ''), pointer, useCrypter); } - catch (_) { ctx.redirect('/pm?fileerror=send#fileshare'); return; } - await refreshInboxCount(); - ctx.redirect('/inbox?filter=sent'); return; - } - if (!file || !file.filepath || !file.size) { cleanup(); ctx.redirect('/pm?fileerror=nofile#fileshare'); return; } - if (file.size > FILESHARE_MAX_SIZE) { cleanup(); ctx.redirect('/pm?fileerror=size#fileshare'); return; } - let pointer; - try { - pointer = await fileshareModel.createShareFromFile({ - filepath: file.filepath, - filename: file.originalFilename || file.name || 'file', - mime: file.mimetype || null - }); - } catch (_) { cleanup(); ctx.redirect('/pm?fileerror=failed#fileshare'); return; } - cleanup(); - const useCrypter = !!b.crypter; - let sharedKey = ''; - if (useCrypter) { - sharedKey = (typeof b.crypterKey === 'string' && b.crypterKey.length >= 32) ? b.crypterKey : cipherModel.generateKey(); - const { encryptedText } = cipherModel.encryptData(pointer.key, sharedKey); - pointer = { ...pointer, key: encryptedText, crypter: true }; - } - try { - await pmModel.sendFileShare([recipient], stripDangerousTags(b.subject || ''), pointer, useCrypter); - } catch (_) { ctx.redirect('/pm?fileerror=send#fileshare'); return; } - await refreshInboxCount(); - if (useCrypter) { ctx.body = await pmView('', '', '', false, sharedKey); return; } - ctx.redirect('/inbox?filter=sent'); - }) - .get('/inbox/file/:id', async ctx => { - if (!checkMod(ctx, 'inboxMod')) { ctx.redirect('/modules'); return; } - const messages = await buildInboxMessages(); - const msg = messages.find(m => m && m.key === ctx.params.id); - const fileShare = msg && msg.value && msg.value.content && msg.value.content.fileShare; - if (!fileShare || fileShare.crypter) { ctx.redirect('/inbox'); return; } - if (!(await fileshareModel.ensureAvailable(fileShare))) { ctx.redirect('/inbox?filestatus=unavailable'); return; } - const safeName = String(fileShare.filename || 'file').replace(/["\r\n\\]/g, ''); - ctx.type = fileShare.mime || 'application/octet-stream'; - ctx.set('Content-Disposition', `attachment; filename="${safeName}"`); - const stream = fileshareModel.readShareStream(fileShare); - if (msg.value.author && msg.value.author !== getViewerId()) { - stream.on('end', () => { fileshareModel.removeLocalBlobs(fileShare).catch(() => {}); }); - } - ctx.body = stream; - }) - .post('/inbox/file/:id', koaBody(), async ctx => { - if (!checkMod(ctx, 'inboxMod')) { ctx.redirect('/modules'); return; } - const { key } = ctx.request.body || {}; - const messages = await buildInboxMessages(); - const msg = messages.find(m => m && m.key === ctx.params.id); - const fileShare = msg && msg.value && msg.value.content && msg.value.content.fileShare; - if (!fileShare) { ctx.redirect('/inbox'); return; } - let pointer = fileShare; - if (fileShare.crypter) { - if (typeof key !== 'string' || !key) { ctx.redirect('/inbox'); return; } - try { - pointer = { ...fileShare, key: cipherModel.decryptData(fileShare.key, key) }; - } catch (_) { ctx.redirect('/inbox?filekey=bad'); return; } - } - if (!(await fileshareModel.ensureAvailable(pointer))) { ctx.redirect('/inbox?filestatus=unavailable'); return; } - const safeName = String(fileShare.filename || 'file').replace(/["\r\n\\]/g, ''); - ctx.type = fileShare.mime || 'application/octet-stream'; - ctx.set('Content-Disposition', `attachment; filename="${safeName}"`); - const stream = fileshareModel.readShareStream(pointer); - if (msg.value.author && msg.value.author !== getViewerId()) { - stream.on('end', () => { fileshareModel.removeLocalBlobs(pointer).catch(() => {}); }); - } - ctx.body = stream; - }) .post('/inbox/delete/:id', koaBody(), async ctx => { await pmModel.deleteMessageById(ctx.params.id); await refreshInboxCount(); @@ -6416,7 +5310,6 @@ router const applySearchPrivacy = (msgs) => msgs.filter(msg => { const c = msg.value?.content; if (!c) return true; - if (c.type === 'pub') return false; if (Array.isArray(c.recps)) return false; if (c.type === 'post' && (c.private === true || c.recps)) return false; if (c.tribeId && anonTribeIds.has(c.tribeId)) return false; @@ -6504,6 +5397,29 @@ router }); ctx.redirect(`/thread/${encodeURIComponent(parent.key)}`); }) + .post("/publish/preview", koaBody({multipart: true, formidable: { multiples: false, maxFileSize: maxSize }, urlencoded: true }), async (ctx) => { + const cw = stripDangerousTags(ctx.request.body.contentWarning?.toString().trim() || ""); + ctx.body = await previewView({ previewData: await preparePreview(ctx), contentWarning: cw.length > 0 ? cw : undefined }); + }) + .post("/publish", koaBody({ multipart: true, urlencoded: true, formidable: { multiples: false, maxFileSize: maxSize } }), async (ctx) => { + const b = ctx.request.body, text = stripDangerousTags(b.text?.toString().trim() || ""), cw = stripDangerousTags(b.contentWarning?.toString().trim() || ""); + let mentions = []; + try { mentions = JSON.parse(b.mentions || "[]"); } catch { mentions = await extractMentions(text); } + await post.root({ text, mentions, contentWarning: cw.length > 0 ? cw : undefined }); + try { activityModel.invalidateCache(); } catch (_) {} + ctx.redirect("/public/latest"); + }) + .post("/publish/custom", koaBody(), async (ctx) => { + const text = String(ctx.request.body.text); + const obj = JSON.parse(text); + const ALLOWED_TYPES = ['post','about','contact','vote','pub','channel']; + if (!obj.type || !ALLOWED_TYPES.includes(obj.type)) { ctx.throw(400, 'Invalid message type'); return; } + const sanitizeObj = (o) => { for (const k of Object.keys(o)) { if (typeof o[k] === 'string') o[k] = stripDangerousTags(o[k]); else if (o[k] && typeof o[k] === 'object') sanitizeObj(o[k]); } }; + sanitizeObj(obj); + ctx.body = await post.publishCustom(obj); + try { activityModel.invalidateCache(); } catch (_) {} + ctx.redirect(`/public/latest`); + }) .post("/follow/:feed", koaBody(), async (ctx) => { ctx.body = await friend.follow(ctx.params.feed); safeRefererRedirect(ctx, '/inhabitants'); @@ -6523,7 +5439,7 @@ router .post("/spread/:message", koaBody(), async (ctx) => { const { message } = ctx.params; const ref = ctx.request.header.referer; - let target = '/blogs'; + let target = '/public/latest'; try { if (ref) { const u = new URL(ref); @@ -6608,7 +5524,7 @@ router .post("/like/:message", koaBody(), async (ctx) => { const { message } = ctx.params, voteValue = Number(ctx.request.body.voteValue); const ref = ctx.request.header.referer; - let target = '/blogs'; + let target = '/public/latest'; try { if (ref) { const u = new URL(ref); @@ -6681,20 +5597,16 @@ router const pw = ctx.request.body.password; if (!pw || pw.length < 32) return ctx.redirect('/legacy'); try { - const { filename, data } = await legacyModel.exportData({ password: pw }); - ctx.set('Content-Type', 'application/octet-stream'); - ctx.set('Content-Disposition', `attachment; filename="${filename}"`); - ctx.set('Content-Length', String(data.length)); - ctx.body = data; - try { onboardingModel.markStep('backup'); } catch (_) {} - } catch (error) { sendErrorPage(ctx, error.message); } + ctx.body = { message: 'Data exported successfully!', file: await legacyModel.exportData({ password: pw }) }; + ctx.redirect('/legacy'); + } catch (error) { ctx.status = 500; ctx.body = { error: `Error: ${error.message}` }; ctx.redirect('/legacy'); } }) - .post('/legacy/import', koaBody({ - multipart: true, - formidable: { - keepExtensions: true, - uploadDir: os.tmpdir(), - } + .post('/legacy/import', koaBody({ + multipart: true, + formidable: { + keepExtensions: true, + uploadDir: '/tmp', + } }), async (ctx) => { const uploadedFile = ctx.request.files?.uploadedFile, pw = ctx.request.body.importPassword; if (!uploadedFile) { ctx.body = { error: 'No file uploaded' }; return ctx.redirect('/legacy'); } @@ -6757,13 +5669,13 @@ router .post("/bookmarks/create", koaBody(), async (ctx) => { if (!checkMod(ctx, 'bookmarksMod')) { ctx.redirect('/modules'); return; } const b = ctx.request.body; - await bookmarksModel.createBookmark(stripDangerousTags(b.url), b.tags, stripDangerousTags(b.description), b.lastVisit); + await bookmarksModel.createBookmark(stripDangerousTags(b.url), b.tags, stripDangerousTags(b.description), b.category, b.lastVisit); ctx.redirect(safeReturnTo(ctx, '/bookmarks?filter=all', ['/bookmarks'])); }) .post("/bookmarks/update/:id", koaBody(), async (ctx) => { if (!checkMod(ctx, 'bookmarksMod')) { ctx.redirect('/modules'); return; } const b = ctx.request.body; - await bookmarksModel.updateBookmarkById(ctx.params.id, { url: stripDangerousTags(b.url), tags: b.tags, description: stripDangerousTags(b.description), lastVisit: b.lastVisit }); + await bookmarksModel.updateBookmarkById(ctx.params.id, { url: stripDangerousTags(b.url), tags: b.tags, description: stripDangerousTags(b.description), category: b.category, lastVisit: b.lastVisit }); ctx.redirect(safeReturnTo(ctx, '/bookmarks?filter=mine', ['/bookmarks'])); }) .post("/bookmarks/delete/:id", koaBody(), async ctx => deleteAction(ctx, 'bookmarks')) @@ -6774,13 +5686,13 @@ router .post("/images/create", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; } const blob = await handleBlobUpload(ctx, 'image'), b = ctx.request.body; - await imagesModel.createImage(blob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description), stripDangerousTags(b.mapUrl || "")); + await imagesModel.createImage(blob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description), parseBool01(b.meme), stripDangerousTags(b.mapUrl || "")); ctx.redirect(safeReturnTo(ctx, '/images?filter=all', ['/images'])); }) .post("/images/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; } const b = ctx.request.body, blob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null; - await imagesModel.updateImageById(ctx.params.id, blob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description), stripDangerousTags(b.mapUrl || "")); + await imagesModel.updateImageById(ctx.params.id, blob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description), parseBool01(b.meme), stripDangerousTags(b.mapUrl || "")); ctx.redirect(safeReturnTo(ctx, '/images?filter=mine', ['/images'])); }) .post("/images/delete/:id", koaBody(), async ctx => deleteAction(ctx, 'images')) @@ -6828,7 +5740,7 @@ router if (!checkMod(ctx, 'mapsMod')) { ctx.redirect('/modules'); return; } try { const code = await mapsModel.generateInvite(ctx.params.id); - ctx.body = renderMapInvitePage(code); + ctx.body = `

Map invite code: ${code}

Back

`; } catch (e) { ctx.redirect(`/maps/${encodeURIComponent(ctx.params.id)}`); } @@ -7135,22 +6047,6 @@ router await tribesContentModel.updateStatus(ctx.params.taskId, ctx.request.body.status); ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=tasks`); }) - .post('/tribe/:id/polls/create', koaBody(), async ctx => { - if (!checkMod(ctx, 'tribesMod') || !checkMod(ctx, 'pollsMod')) { ctx.redirect('/modules'); return; } - const tribe = await tribesModel.getTribeById(ctx.params.id); - if (!tribe || !tribe.members.includes(getViewerId())) { sendErrorPage(ctx, 'Forbidden', { status: 403 }); return; } - const b = ctx.request.body || {}; - try { - await pollsModel.createPoll({ - question: stripDangerousTags(b.question), - options: stripDangerousTags(String(b.options || '')).split('\n'), - anonymous: [].concat(b.anonymous).includes('1'), - multiple: [].concat(b.multiple).includes('1'), - tribeId: tribe.id - }); - } catch (err) { sendErrorPage(ctx, err.message || String(err), { status: 400 }); return; } - ctx.redirect(`/tribe/${encodeURIComponent(ctx.params.id)}?section=polls`); - }) .post('/tribe/:id/votations/create', koaBody(), async ctx => { if (!checkMod(ctx, 'tribesMod')) { ctx.redirect('/modules'); return; } const tribe = await tribesModel.getTribeById(ctx.params.id); @@ -7266,22 +6162,18 @@ router }) .post('/tasks/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => { const b = ctx.request.body; - const media = await readGalleryUpload(ctx); - const draftImages = mergeGallery(media.keep, media.uploaded, media.removeIndex); - if (media.isMediaAction) { - ctx.body = await taskView([], 'create', null, b.returnTo, { draft: { ...b, images: draftImages } }); - return; - } - await tasksModel.createTask(stripDangerousTags(b.title), stripDangerousTags(b.description), b.startTime, b.endTime, b.priority, stripDangerousTags(b.location), b.tags, b.isPublic, { images: draftImages, video: media.clip || '' }); + const imageMarkdown = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null; + let desc = stripDangerousTags(b.description); + if (imageMarkdown) desc = (desc ? desc + '\n' : '') + imageMarkdown; + await tasksModel.createTask(stripDangerousTags(b.title), desc, b.startTime, b.endTime, b.priority, stripDangerousTags(b.location), b.tags, b.isPublic); ctx.redirect(safeReturnTo(ctx, '/tasks?filter=mine', ['/tasks'])); }) .post('/tasks/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => { const b = ctx.request.body, tags = Array.isArray(b.tags) ? b.tags.filter(Boolean) : (typeof b.tags === 'string' ? b.tags.split(',').map(t => t.trim()).filter(Boolean) : []); - const media = await readGalleryUpload(ctx); - let current = []; - try { current = (await tasksModel.getTaskById(ctx.params.id)).images || []; } catch (_) { current = []; } - await tasksModel.updateTaskById(ctx.params.id, { title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), startTime: b.startTime, endTime: b.endTime, priority: b.priority, location: stripDangerousTags(b.location), tags, isPublic: b.isPublic, ...galleryPatch(media, current) }); - if (media.isMediaAction) { ctx.redirect(`/tasks/edit/${encodeURIComponent(ctx.params.id)}`); return; } + const imageMarkdown = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null; + let desc = stripDangerousTags(b.description); + if (imageMarkdown) desc = (desc ? desc + '\n' : '') + imageMarkdown; + await tasksModel.updateTaskById(ctx.params.id, { title: stripDangerousTags(b.title), description: desc, startTime: b.startTime, endTime: b.endTime, priority: b.priority, location: stripDangerousTags(b.location), tags, isPublic: b.isPublic }); ctx.redirect(safeReturnTo(ctx, '/tasks?filter=mine', ['/tasks'])); }) .post('/tasks/assign/:id', koaBody(), async ctx => { @@ -7298,37 +6190,26 @@ router }) .post('/tasks/:taskId/comments', koaBodyMiddleware, async ctx => commentAction(ctx, 'tasks', 'taskId')) .post('/reports/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => { - const b = ctx.request.body; - const media = await readGalleryUpload(ctx); - const draftImages = mergeGallery(media.keep, media.uploaded, media.removeIndex); - if (media.isMediaAction) { - ctx.body = await reportView([], 'create', null, b.category || '', { draft: { ...b, images: draftImages } }); - return; - } - await reportsModel.createReport(stripDangerousTags(b.title), stripDangerousTags(b.description), b.category, null, b.tags, b.severity, { + const b = ctx.request.body, image = await handleBlobUpload(ctx, 'image'); + await reportsModel.createReport(stripDangerousTags(b.title), stripDangerousTags(b.description), b.category, image, b.tags, b.severity, { stepsToReproduce: stripDangerousTags(b.stepsToReproduce), expectedBehavior: stripDangerousTags(b.expectedBehavior), actualBehavior: stripDangerousTags(b.actualBehavior), environment: stripDangerousTags(b.environment), reproduceRate: b.reproduceRate, problemStatement: stripDangerousTags(b.problemStatement), userStory: stripDangerousTags(b.userStory), acceptanceCriteria: stripDangerousTags(b.acceptanceCriteria), whatHappened: stripDangerousTags(b.whatHappened), reportedUser: b.reportedUser, evidenceLinks: stripDangerousTags(b.evidenceLinks), - contentLocation: stripDangerousTags(b.contentLocation), whyInappropriate: stripDangerousTags(b.whyInappropriate) - }, { images: draftImages, video: media.clip || '' }); + contentLocation: stripDangerousTags(b.contentLocation), whyInappropriate: stripDangerousTags(b.whyInappropriate), requestedAction: stripDangerousTags(b.requestedAction) + }); ctx.redirect('/reports'); }) .post('/reports/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => { - const b = ctx.request.body; - const media = await readGalleryUpload(ctx); - let current = []; - try { current = (await reportsModel.getReportById(ctx.params.id)).images || []; } catch (_) { current = []; } + const b = ctx.request.body, image = await handleBlobUpload(ctx, 'image'); await reportsModel.updateReportById(ctx.params.id, { - title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), category: b.category, tags: b.tags, severity: b.severity, - ...galleryPatch(media, current), + title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), category: b.category, image, tags: b.tags, severity: b.severity, template: { stepsToReproduce: stripDangerousTags(b.stepsToReproduce), expectedBehavior: stripDangerousTags(b.expectedBehavior), actualBehavior: stripDangerousTags(b.actualBehavior), environment: stripDangerousTags(b.environment), reproduceRate: b.reproduceRate, problemStatement: stripDangerousTags(b.problemStatement), userStory: stripDangerousTags(b.userStory), acceptanceCriteria: stripDangerousTags(b.acceptanceCriteria), whatHappened: stripDangerousTags(b.whatHappened), reportedUser: stripDangerousTags(b.reportedUser), evidenceLinks: stripDangerousTags(b.evidenceLinks), - contentLocation: stripDangerousTags(b.contentLocation), whyInappropriate: stripDangerousTags(b.whyInappropriate) + contentLocation: stripDangerousTags(b.contentLocation), whyInappropriate: stripDangerousTags(b.whyInappropriate), requestedAction: stripDangerousTags(b.requestedAction) } }); - if (media.isMediaAction) { ctx.redirect(`/reports/edit/${encodeURIComponent(ctx.params.id)}`); return; } ctx.redirect('/reports?filter=mine'); }) .post('/reports/delete/:id', async ctx => { @@ -7346,21 +6227,10 @@ router .post('/reports/:reportId/comments', koaBodyMiddleware, async ctx => commentAction(ctx, 'reports', 'reportId')) .post('/events/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { const b = ctx.request.body; - const media = await readGalleryUpload(ctx); - const draftImages = mergeGallery(media.keep, media.uploaded, media.removeIndex); - if (media.isMediaAction) { - const viewerPrefsDraft = await about.visibilityPrefs(getViewerId()).catch(() => null); - ctx.body = await eventView([], 'create', null, b.returnTo, { viewerPrefs: viewerPrefsDraft, draft: { ...b, images: draftImages } }); - return; - } - const { intervalWeekly, intervalMonthly, intervalYearly } = readInterval(b); - const recurrence = { - weekly: intervalWeekly, - monthly: intervalMonthly, - yearly: intervalYearly, - until: b.intervalDeadline || b.recurrenceUntil || '' - }; - const evResult = await eventsModel.createEvent(stripDangerousTags(b.title), stripDangerousTags(b.description), b.date, stripDangerousTags(b.location), b.price, b.url, b.attendees || [], b.tags, b.isPublic, stripDangerousTags(b.mapUrl), b.clearnetPublic, { images: draftImages, video: media.clip || '' }, recurrence); + const imageMarkdown = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null; + let desc = stripDangerousTags(b.description); + if (imageMarkdown) desc = (desc ? desc + '\n' : '') + imageMarkdown; + const evResult = await eventsModel.createEvent(stripDangerousTags(b.title), desc, b.date, stripDangerousTags(b.location), b.price, b.url, b.attendees || [], b.tags, b.isPublic, stripDangerousTags(b.mapUrl), b.clearnetPublic); if ([].concat(b.addToCalendar).includes("1") && evResult && evResult.key) { try { await calendarsModel.createCalendar({ @@ -7381,9 +6251,10 @@ router }) .post('/events/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { const b = ctx.request.body, existing = await eventsModel.getEventById(ctx.params.id); - const media = await readGalleryUpload(ctx); - await eventsModel.updateEventById(ctx.params.id, { title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), date: b.date, location: stripDangerousTags(b.location), price: b.price, url: b.url, attendees: b.attendees, tags: b.tags, isPublic: b.isPublic, createdAt: existing.createdAt, organizer: existing.organizer, mapUrl: stripDangerousTags(b.mapUrl), clearnetPublic: b.clearnetPublic, ...readInterval(b), recurrenceUntil: b.intervalDeadline || b.recurrenceUntil || '', ...galleryPatch(media, existing.images || []) }); - if (media.isMediaAction) { ctx.redirect(`/events/edit/${encodeURIComponent(ctx.params.id)}`); return; } + const imageMarkdown = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null; + let desc = stripDangerousTags(b.description); + if (imageMarkdown) desc = (desc ? desc + '\n' : '') + imageMarkdown; + await eventsModel.updateEventById(ctx.params.id, { title: stripDangerousTags(b.title), description: desc, date: b.date, location: stripDangerousTags(b.location), price: b.price, url: b.url, attendees: b.attendees, tags: b.tags, isPublic: b.isPublic, createdAt: existing.createdAt, organizer: existing.organizer, mapUrl: stripDangerousTags(b.mapUrl), clearnetPublic: b.clearnetPublic }); ctx.redirect(safeReturnTo(ctx, '/events?filter=mine', ['/events'])); }) .post('/events/attend/:id', koaBody(), async ctx => { @@ -7427,22 +6298,12 @@ router .post('/votes/create', koaBody(), async ctx => { const b = ctx.request.body, defaultOptions = ['YES', 'NO', 'ABSTENTION', 'CONFUSED', 'FOLLOW_MAJORITY', 'NOT_INTERESTED']; const parsedOptions = b.options ? b.options.split(',').map(o => o.trim()).filter(Boolean) : defaultOptions; - try { - await votesModel.createVote(stripDangerousTags(b.question), b.deadline, parsedOptions, String(b.tags || '').split(',').map(t => t.trim()).filter(Boolean)); - } catch (err) { - ctx.redirect(voteFormRedirect('create', null, err, b)); - return; - } + await votesModel.createVote(stripDangerousTags(b.question), b.deadline, parsedOptions, String(b.tags || '').split(',').map(t => t.trim()).filter(Boolean)); ctx.redirect(safeReturnTo(ctx, '/votes?filter=mine', ['/votes'])); }) .post('/votes/update/:id', koaBody(), async ctx => { const b = ctx.request.body, parsedOptions = b.options ? b.options.split(',').map(o => o.trim()).filter(Boolean) : undefined; - try { - await votesModel.updateVoteById(ctx.params.id, { question: stripDangerousTags(b.question), deadline: b.deadline, options: parsedOptions, tags: b.tags ? b.tags.split(',').map(t => t.trim()).filter(Boolean) : [] }); - } catch (err) { - ctx.redirect(voteFormRedirect('edit', ctx.params.id, err, b)); - return; - } + await votesModel.updateVoteById(ctx.params.id, { question: stripDangerousTags(b.question), deadline: b.deadline, options: parsedOptions, tags: b.tags ? b.tags.split(',').map(t => t.trim()).filter(Boolean) : [] }); ctx.redirect(safeReturnTo(ctx, '/votes?filter=mine', ['/votes'])); }) .post('/votes/delete/:id', koaBody(), async ctx => { @@ -7697,7 +6558,7 @@ router const b = ctx.request.body, image = await handleBlobUpload(ctx, "image"), parsedStock = parseInt(String(b.stock || "0"), 10); if (!parsedStock || parsedStock <= 0) ctx.throw(400, "Stock must be a positive number."); const pickLast = v => Array.isArray(v) ? v[v.length - 1] : v, shpVal = pickLast(b.includesShipping); - await marketModel.createItem(b.item_type, stripDangerousTags(b.title), stripDangerousTags(b.description), image, b.price, b.tags, b.item_status, b.deadline, shpVal === "1" || shpVal === "on" || shpVal === true || shpVal === "true", parsedStock, stripDangerousTags(b.mapUrl), { industry: stripDangerousTags(b.industry || "") }, b.visibility); + await marketModel.createItem(b.item_type, stripDangerousTags(b.title), stripDangerousTags(b.description), image, b.price, b.tags, b.item_status, b.deadline, shpVal === "1" || shpVal === "on" || shpVal === true || shpVal === "true", parsedStock, stripDangerousTags(b.mapUrl), {}, b.visibility); ctx.redirect(safeReturnTo(ctx, "/market", ["/market"])); }) .post("/market/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { @@ -7743,7 +6604,7 @@ router } catch (_) {} if (item.item_type === "exchange") await marketModel.setItemAsSold(ctx.params.id); else await marketModel.decrementStock(ctx.params.id); - try { await contentFavorites.addFavorite('market', item.id || ctx.params.id); } catch (_) {} + try { await mediaFavorites.addFavorite('market', item.id || ctx.params.id); } catch (_) {} if (item.shopProductId && checkMod(ctx, 'shopsMod')) { try { await shopsModel.buyProduct(item.shopProductId); } catch (_) {} } @@ -7769,141 +6630,11 @@ router await marketModel.addBidToAuction(ctx.params.id, getViewerId(), ctx.request.body.bidAmount) ctx.redirect(safeReturnTo(ctx, "/market?filter=auctions", ["/market"])) }) - .post("/market/opinions/:itemId/:category", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; } - try { await marketModel.createOpinion(ctx.params.itemId, ctx.params.category) } catch (_) {} - ctx.redirect(safeReturnTo(ctx, `/market/${encodeURIComponent(ctx.params.itemId)}`, ['/market'])) - }) .post("/market/:itemId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'market', 'itemId')) - .post('/housing/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - const b = ctx.request.body - const media = await readGalleryUpload(ctx) - const images = mergeGallery(media.keep, media.uploaded, media.removeIndex) - const clip = media.clip - if (media.isMediaAction) { - ctx.body = await housingView([], 'CREATE', { maxImages: housingModel.MAX_IMAGES, draft: { ...b, images } }) - return - } - let created = null - try { - created = await housingModel.createHousing({ - housing_type: stripDangerousTags(b.housing_type), - property_type: stripDangerousTags(b.property_type), - title: stripDangerousTags(b.title), - description: stripDangerousTags(b.description), - rules: stripDangerousTags(b.rules), - place: stripDangerousTags(b.place), - mapUrl: stripDangerousTags(b.mapUrl), - price: b.price, - rooms: b.rooms, - size: b.size, - capacity: b.capacity, - availableFrom: b.availableFrom, - availableTo: b.availableTo, - images, - video: clip || '', - tags: b.tags, - visibility: b.visibility - }) - } catch (err) { sendErrorPage(ctx, err.message || String(err), { status: 400 }); return } - ctx.redirect(safeReturnTo(ctx, '/housing?filter=MINE', ['/housing'])) - }) - .post('/housing/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - const b = ctx.request.body - const uploaded = await handleBlobUploads(ctx, 'images') - const newClip = ctx.request.files?.video ? await handleBlobUpload(ctx, 'video') : null - const patch = { - housing_type: stripDangerousTags(b.housing_type), - property_type: stripDangerousTags(b.property_type), - title: stripDangerousTags(b.title), - description: stripDangerousTags(b.description), - rules: stripDangerousTags(b.rules), - place: stripDangerousTags(b.place), - mapUrl: stripDangerousTags(b.mapUrl), - price: b.price, - rooms: b.rooms, - size: b.size, - capacity: b.capacity, - availableFrom: b.availableFrom, - availableTo: b.availableTo, - tags: b.tags, - visibility: b.visibility - } - const removeIndex = b.removePhoto !== undefined && b.removePhoto !== '' ? parseInt(b.removePhoto, 10) : -1 - if (uploaded.length || removeIndex >= 0) { - let current = [] - try { current = (await housingModel.getHousingById(ctx.params.id, getViewerId())).images || [] } catch (_) { current = [] } - if (removeIndex >= 0) current = current.filter((_, i) => i !== removeIndex) - patch.images = [...current, ...uploaded] - } - if (newClip) patch.video = newClip - else if (b.action === 'removeVideo') patch.video = '' - try { await housingModel.updateHousing(ctx.params.id, patch) } - catch (err) { sendErrorPage(ctx, err.message || String(err), { status: 400 }); return } - if (b.action === 'addPhoto' || b.action === 'addVideo' || b.action === 'removeVideo' || removeIndex >= 0) { ctx.redirect(`/housing/edit/${encodeURIComponent(ctx.params.id)}`); return } - ctx.redirect(safeReturnTo(ctx, '/housing?filter=MINE', ['/housing'])) - }) - .post('/housing/delete/:id', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - let item = null - try { item = await housingModel.getHousingById(ctx.params.id, getViewerId()) } catch (_) {} - try { await housingModel.deleteHousing(ctx.params.id) } catch (_) {} - await notifyHousingRequesters(item, 'deleted') - ctx.redirect(safeReturnTo(ctx, '/housing?filter=MINE', ['/housing'])) - }) - .post('/housing/status/:id', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - const nextStatus = String(ctx.request.body.status || '').toUpperCase() - let item = null - try { item = await housingModel.getHousingById(ctx.params.id, getViewerId()) } catch (_) {} - try { await housingModel.updateHousingStatus(ctx.params.id, nextStatus) } catch (_) {} - if (nextStatus === 'CLOSED') await notifyHousingRequesters(item, 'closed') - ctx.redirect(safeReturnTo(ctx, '/housing?filter=MINE', ['/housing'])) - }) - .post('/housing/visibility/:id', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - const next = String(ctx.request.body?.visibility || '').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC' - try { await housingModel.updateHousing(ctx.params.id, { visibility: next }) } catch (_) {} - ctx.redirect(safeReturnTo(ctx, `/housing/${encodeURIComponent(ctx.params.id)}`, ['/housing'])) - }) - .post('/housing/request/:id', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - let item = null - try { item = await housingModel.getHousingById(ctx.params.id, getViewerId()) } catch (_) {} - if (!item) { ctx.redirect(safeReturnTo(ctx, '/housing', ['/housing'])); return } - try { - const res = await housingModel.requestHousing(ctx.params.id) - if (!res || !res.alreadyRequested) { - try { await pmModel.sendMessage([item.author], 'HOUSING_REQUESTED', `A new habitant has requested your place [${item.title || 'a place'}](/housing/${encodeURIComponent(item.id)})`) } catch (_) {} - } - } catch (_) {} - ctx.redirect(safeReturnTo(ctx, '/housing', ['/housing'])) - }) - .post('/housing/cancel/:id', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - let item = null - try { item = await housingModel.getHousingById(ctx.params.id, getViewerId()) } catch (_) {} - if (!item) { ctx.redirect(safeReturnTo(ctx, '/housing', ['/housing'])); return } - try { - const res = await housingModel.cancelRequest(ctx.params.id) - if (!res || !res.notRequested) { - try { await pmModel.sendMessage([item.author], 'HOUSING_CANCELLED', `A habitant has cancelled the request on your place [${item.title || 'a place'}](/housing/${encodeURIComponent(item.id)})`) } catch (_) {} - } - } catch (_) {} - ctx.redirect(safeReturnTo(ctx, '/housing', ['/housing'])) - }) - .post('/housing/opinions/:housingId/:category', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } - try { await housingModel.createOpinion(ctx.params.housingId, ctx.params.category) } catch (_) {} - ctx.redirect(safeReturnTo(ctx, `/housing/${encodeURIComponent(ctx.params.housingId)}`, ['/housing'])) - }) - .post('/housing/:housingId/comments', koaBodyMiddleware, async ctx => commentAction(ctx, 'housing', 'housingId')) .post('/jobs/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; } const b = ctx.request.body, imageBlob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null; - await jobsModel.createJob({ job_type: stripDangerousTags(b.job_type), title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), requirements: stripDangerousTags(b.requirements), languages: stripDangerousTags(b.languages), job_time: b.job_time, tasks: stripDangerousTags(b.tasks), location: stripDangerousTags(b.location), vacants: b.vacants ? parseInt(b.vacants, 10) : 1, salary: b.salary != null && b.salary !== '' ? parseFloat(String(b.salary).replace(',', '.')) : 0, hoursOffered: b.hoursOffered != null && b.hoursOffered !== '' ? parseFloat(String(b.hoursOffered).replace(',', '.')) : 0, hoursRequested: b.hoursRequested != null && b.hoursRequested !== '' ? parseFloat(String(b.hoursRequested).replace(',', '.')) : 0, exchangeSkill: stripDangerousTags(b.exchangeSkill || ''), tags: b.tags, image: imageBlob, mapUrl: stripDangerousTags(b.mapUrl), industry: stripDangerousTags(b.industry || ''), visibility: b.visibility, clearnetPublic: b.clearnetPublic }); + await jobsModel.createJob({ job_type: stripDangerousTags(b.job_type), title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), requirements: stripDangerousTags(b.requirements), languages: stripDangerousTags(b.languages), job_time: b.job_time, tasks: stripDangerousTags(b.tasks), location: stripDangerousTags(b.location), vacants: b.vacants ? parseInt(b.vacants, 10) : 1, salary: b.salary != null && b.salary !== '' ? parseFloat(String(b.salary).replace(',', '.')) : 0, hoursOffered: b.hoursOffered != null && b.hoursOffered !== '' ? parseFloat(String(b.hoursOffered).replace(',', '.')) : 0, hoursRequested: b.hoursRequested != null && b.hoursRequested !== '' ? parseFloat(String(b.hoursRequested).replace(',', '.')) : 0, exchangeSkill: stripDangerousTags(b.exchangeSkill || ''), tags: b.tags, image: imageBlob, mapUrl: stripDangerousTags(b.mapUrl), visibility: b.visibility, clearnetPublic: b.clearnetPublic }); ctx.redirect(safeReturnTo(ctx, '/jobs?filter=MINE', ['/jobs'])); }) .post('/jobs/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { @@ -8028,8 +6759,6 @@ router } }) .post("/shops/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'shops', 'add')) - .post("/shopProducts/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'shopProducts', 'add')) - .post("/shopProducts/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'shopProducts', 'remove')) .post("/shops/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'shops', 'remove')) .post("/shops/opinions/:shopId/:category", koaBody(), async (ctx) => { if (!checkMod(ctx, 'shopsMod')) { ctx.redirect('/modules'); return; } @@ -8081,12 +6810,7 @@ router await shopsModel.buyProduct(ctx.params.id); try { const pr = await shopsModel.getProductById(ctx.params.id).catch(() => null); - if (pr && pr.author && String(pr.author) !== String(getViewerId())) { - try { - await pmModel.sendMessage([pr.author], "SHOP_SOLD", `product "${pr.title}" has been sold -> /shops/product/${ctx.params.id} OASIS ID: ${getViewerId()} for: ${pr.price} ECO`); - } catch (_) {} - } - await contentFavorites.addFavorite('shopProducts', (pr && pr.rootId) || ctx.params.id); + await mediaFavorites.addFavorite('shopProducts', (pr && pr.rootId) || ctx.params.id); } catch (_) {} if (checkMod(ctx, 'marketMod')) { try { const mi = await marketModel.getItemByShopProductId(ctx.params.id); if (mi) await marketModel.decrementStock(mi.id); } catch (_) {} @@ -8188,36 +6912,6 @@ router }) .post("/chats/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'chats', 'add')) .post("/chats/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'chats', 'remove')) - .post("/chats/:chatId/polls/create", koaBody(), async (ctx) => { - if (!checkMod(ctx, 'chatsMod') || !checkMod(ctx, 'pollsMod')) { ctx.redirect('/modules'); return; } - const uid = getViewerId(); - const chat = await chatsModel.getChatById(ctx.params.chatId).catch(() => null); - if (!chat) { ctx.redirect('/chats'); return; } - const members = Array.isArray(chat.members) ? chat.members : []; - const isOpen = String(chat.status || '').toUpperCase() === 'OPEN'; - if (String(chat.status || '').toUpperCase() === 'CLOSED') { ctx.redirect(`/chats/${encodeURIComponent(ctx.params.chatId)}`); return; } - if (chat.tribeId) { - try { - const t = await tribesModel.getTribeById(chat.tribeId); - if (!t.members.includes(uid)) { sendErrorPage(ctx, "Forbidden", { status: 403 }); return; } - } catch { sendErrorPage(ctx, "Forbidden", { status: 403 }); return; } - } else if (!isOpen && chat.author !== uid && !members.includes(uid)) { - sendErrorPage(ctx, "Forbidden", { status: 403 }); return; - } - const b = ctx.request.body || {}; - try { - await pollsModel.createPoll({ - question: stripDangerousTags(b.question), - options: stripDangerousTags(String(b.options || '')).split('\n'), - anonymous: [].concat(b.anonymous).includes('1'), - multiple: [].concat(b.multiple).includes('1'), - chatId: chat.rootId || chat.key, - tribeId: chat.tribeId || null - }); - } catch (err) { sendErrorPage(ctx, err.message || String(err), { status: 400 }); return; } - try { activityModel.invalidateCache(); } catch (_) {} - ctx.redirect(`/chats/${encodeURIComponent(ctx.params.chatId)}`); - }) .post("/chats/:chatId/message", koaBody({ multipart: true }), async (ctx) => { if (!checkMod(ctx, 'chatsMod')) { ctx.redirect('/modules'); return; } const uid = getViewerId(); @@ -8231,17 +6925,7 @@ router const text = stripDangerousTags(String(ctx.request.body.text || '').trim()); const imageBlob = ctx.request.files?.image ? extractBlobId(await handleBlobUpload(ctx, 'image')) : null; if (!text && !imageBlob) { ctx.redirect(`/chats/${encodeURIComponent(ctx.params.chatId)}`); return; } - try { - const replyTo = String(ctx.request.body.replyTo || '').trim() || null; - await chatsModel.sendMessage(ctx.params.chatId, text, imageBlob, replyTo); - } catch (err) { - if (err && err.code === 'CHAT_RATE_LIMIT') { - const { i18n } = require('../views/main_views'); - sendErrorPage(ctx, i18n.chatRateLimitMessage, { title: i18n.chatRateLimitTitle, status: 429 }); - return; - } - throw err; - } + await chatsModel.sendMessage(ctx.params.chatId, text, imageBlob); ctx.redirect(safeReturnTo(ctx, `/chats/${encodeURIComponent(ctx.params.chatId)}`, ['/chats'])); }) .post("/pads/create", koaBody(), async (ctx) => { @@ -8343,27 +7027,6 @@ router }) .post("/pads/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'pads', 'add')) .post("/pads/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'pads', 'remove')) - .post("/forum/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'forum', 'add')) - .post("/forum/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'forum', 'remove')) - .post("/events/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'events', 'add')) - .post("/events/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'events', 'remove')) - .post("/tasks/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'tasks', 'add')) - .post("/tasks/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'tasks', 'remove')) - .post("/reports/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'reports', 'add')) - .post("/reports/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'reports', 'remove')) - .post("/votes/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'votes', 'add')) - .post("/votes/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'votes', 'remove')) - .post("/jobs/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'jobs', 'add')) - .post("/jobs/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'jobs', 'remove')) - .post("/housing/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'housing', 'add')) - .post("/housing/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'housing', 'remove')) - .post("/projects/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'projects', 'add')) - .post("/projects/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'projects', 'remove')) - .post("/transfers/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'transfers', 'add')) - .post("/transfers/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'transfers', 'remove')) - .post("/transfers/:transferId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'transfers', 'transferId')) - .post("/market/favorites/add/:id", koaBody(), async ctx => favAction(ctx, 'market', 'add')) - .post("/market/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'market', 'remove')) .post("/calendars/create", koaBody(), async (ctx) => { if (!checkMod(ctx, 'calendarsMod')) { ctx.redirect('/modules'); return; } const b = ctx.request.body || {}; @@ -8372,7 +7035,9 @@ router const t = await tribesModel.getTribeById(tribeId).catch(() => null); if (!t || !t.members.includes(getViewerId())) { ctx.status = 403; ctx.redirect('/tribes'); return; } } - const { intervalWeekly, intervalMonthly, intervalYearly } = readInterval(b); + const intervalWeekly = [].concat(b.intervalWeekly).includes("1"); + const intervalMonthly = [].concat(b.intervalMonthly).includes("1"); + const intervalYearly = [].concat(b.intervalYearly).includes("1"); try { const msg = await calendarsModel.createCalendar({ title: stripDangerousTags(b.title || ""), @@ -8383,8 +7048,6 @@ router firstDateLabel: stripDangerousTags(b.firstDateLabel || ""), firstNote: stripDangerousTags(b.firstNote || ""), intervalWeekly, intervalMonthly, intervalYearly, - intervalDeadline: b.intervalDeadline || "", - mapUrl: stripDangerousTags(b.mapUrl || ""), tribeId }); ctx.redirect(tribeId ? `/tribe/${encodeURIComponent(tribeId)}?section=calendars` : `/calendars/${encodeURIComponent(msg.key)}`); @@ -8406,8 +7069,7 @@ router title: stripDangerousTags(b.title || ""), status: b.status || "OPEN", deadline: b.deadline || "", - tags: b.tags || "", - mapUrl: stripDangerousTags(b.mapUrl || "") + tags: b.tags || "" }); } catch (_) {} ctx.redirect(`/calendars/${encodeURIComponent(ctx.params.id)}`); @@ -8483,7 +7145,9 @@ router } catch { sendErrorPage(ctx, "Forbidden", { status: 403 }); return; } } const b = ctx.request.body || {}; - const { intervalWeekly, intervalMonthly, intervalYearly } = readInterval(b); + const intervalWeekly = [].concat(b.intervalWeekly).includes("1"); + const intervalMonthly = [].concat(b.intervalMonthly).includes("1"); + const intervalYearly = [].concat(b.intervalYearly).includes("1"); try { const dateMsgs = await calendarsModel.addDate(ctx.params.id, b.date || "", stripDangerousTags(b.label || ""), intervalWeekly, intervalMonthly, intervalYearly, b.intervalDeadline || ""); const noteText = stripDangerousTags(String(b.text || "").trim()); @@ -8730,20 +7394,6 @@ router console.log("oasis@version: running install.sh...", shOut, shErr); safeRefererRedirect(ctx, '/settings'); }) - .post("/settings/workflow", koaBody(), async (ctx) => { - if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } - const workflow = workflowsModel.getWorkflow(String(ctx.request.body.workflow || '').trim()); - if (!workflow) { ctx.redirect('/settings'); return; } - const cfg = getConfig(); - const enabled = new Set(workflowsModel.modulesOf(workflow)); - workflowsModel.ALL_MODULES.forEach(mod => { cfg.modules[`${mod}Mod`] = enabled.has(mod) ? 'on' : 'off'; }); - cfg.themes.current = workflow.theme; - if (workflow.homePage) cfg.homePage = workflow.homePage; - if (!enabled.has('aiNav') && cfg.ux) cfg.ux.current = 'blocks'; - saveConfig(cfg); - ctx.cookies.set("theme", cfg.themes.current, { httpOnly: true, sameSite: 'strict', secure: ctx.secure }); - ctx.redirect("/settings"); - }) .post("/settings/theme", koaBody(), async (ctx) => { const theme = String(ctx.request.body.theme || "").trim(), cfg = getConfig(); cfg.themes.current = theme || "Dark-SNH"; @@ -8757,7 +7407,6 @@ router cfg.language = lang; fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2)); ctx.cookies.set("language", lang, { maxAge: 365 * 24 * 60 * 60 * 1000, httpOnly: true, sameSite: 'strict', secure: ctx.secure }); - try { onboardingModel.markStep('language'); } catch (_) {} safeRefererRedirect(ctx, '/settings'); }) .post("/settings/conn/start", koaBody(), async ctx => { await meta.connStart(); ctx.redirect("/peers"); }) @@ -8770,15 +7419,15 @@ router if (pubKey) { try { const os = require('os'), fsx = require('fs'), px = require('path'); - const gossip = JSON.parse(fsx.readFileSync(px.join(require('../server/ssb_config').path, 'gossip.json'), 'utf8') || '[]'); + const gossip = JSON.parse(fsx.readFileSync(px.join(os.homedir(), '.ssb', 'gossip.json'), 'utf8') || '[]'); let unfollowed = []; - try { unfollowed = JSON.parse(fsx.readFileSync(px.join(require('../server/ssb_config').path, 'gossip_unfollowed.json'), 'utf8') || '[]'); } catch (_) {} + try { unfollowed = JSON.parse(fsx.readFileSync(px.join(os.homedir(), '.ssb', 'gossip_unfollowed.json'), 'utf8') || '[]'); } catch (_) {} const activePub = Array.isArray(gossip) && gossip.some(p => p && p.key === pubKey) && !unfollowed.some(u => u && u.key === pubKey); if (activePub) { ctx.redirect('/invites?flash=alreadyFederated'); return; } } catch (_) {} } try { await meta.acceptInvite(invite); } catch (_) {} - safeRefererRedirect(ctx, "/invites"); + ctx.redirect("/invites"); }) .post("/invites/inhabitant/follow", koaBody(), async (ctx) => { const feedId = String(ctx.request.body?.feedId || '').trim(); @@ -9161,11 +7810,10 @@ router const cfg = getConfig(); const v = String(ctx.request.body.ux || "").trim().toLowerCase(); const aiNavEnabled = cfg.modules && cfg.modules.aiNavMod === 'on'; - const chatsEnabled = cfg.modules && cfg.modules.chatsMod === 'on'; - const next = (v === "ainav" && aiNavEnabled) ? "ainav" : (v === "chats" && chatsEnabled) ? "chats" : "blocks"; + const next = (v === "ainav" && aiNavEnabled) ? "ainav" : "blocks"; cfg.ux = { ...(cfg.ux && typeof cfg.ux === 'object' ? cfg.ux : {}), current: next }; saveConfig(cfg); - ctx.redirect(next === "ainav" ? "/" : next === "chats" ? "/chats" : "/settings"); + ctx.redirect(next === "ainav" ? "/" : "/settings"); }) .post("/settings/lan-broadcasting", koaBody(), async (ctx) => { const enabled = !!(ctx.request.body && (ctx.request.body.lanBroadcasting === 'on' || ctx.request.body.lanBroadcasting === '1' || ctx.request.body.lanBroadcasting === 'true')); @@ -9216,12 +7864,11 @@ router .post("/settings/rebuild", async ctx => { meta.rebuild(); ctx.redirect("/settings"); }) .post("/modules/preset", koaBody(), async (ctx) => { if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } - const ALL_MODULES = workflowsModel.ALL_MODULES; + const ALL_MODULES = ['popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'fediverse', 'invites', 'wallet', 'legacy', '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', 'jobs', 'projects', 'shops', 'banking', 'parliament', 'courts', 'logs', 'torrents']; const PRESETS = { - minimal: ['feed', 'forum', 'games', 'images', 'videos', 'audios', 'bookmarks', 'tags', 'trending', 'blogs', 'polls', 'opinions', 'cipher', 'legacy'], - social: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'blogs', 'polls', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'trending', 'tribes', 'videos', 'votes'], - economy: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'blogs', 'polls', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'trending', 'tribes', 'videos', 'votes', 'banking', 'wallet', 'transfers', 'market', 'housing', 'jobs', 'shops', 'industry'], - mobile: workflowsModel.MOBILE_MODULES, + minimal: ['feed', 'forum', 'games', 'images', 'videos', 'audios', 'bookmarks', 'tags', 'trending', 'popular', 'latest', 'threads', 'opinions', 'cipher', 'legacy'], + social: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'multiverse', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'threads', 'trending', 'tribes', 'videos', 'votes'], + economy: ['agenda', 'audios', 'bookmarks', 'calendars', 'chats', 'cipher', 'courts', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'images', 'invites', 'larp', 'legacy', 'logs', 'maps', 'multiverse', 'opinions', 'pads', 'parliament', 'pixelia', 'melody', 'projects', 'reports', 'tags', 'tasks', 'threads', 'trending', 'tribes', 'videos', 'votes', 'banking', 'wallet', 'transfers', 'market', 'jobs', 'shops'], full: ALL_MODULES }; const preset = String(ctx.request.body.preset || ''); @@ -9234,17 +7881,46 @@ router }) .post("/save-modules", koaBody(), async (ctx) => { if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } - const modules = workflowsModel.ALL_MODULES; + const modules = ['popular', 'topics', 'summaries', 'latest', 'threads', 'multiverse', 'fediverse', 'invites', 'wallet', 'legacy', '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', 'graphos', 'jobs', 'projects', 'shops', 'banking', 'parliament', 'courts', 'logs', 'torrents']; const cfg = getConfig(); modules.forEach(mod => cfg.modules[`${mod}Mod`] = ctx.request.body[`${mod}Form`] === 'on' ? 'on' : 'off'); saveConfig(cfg); ctx.redirect(`/modules`); }) + .get("/settings/bottombar", async (ctx) => { + ctx.body = require("../views/main_views").bottomBarPickerView(); + }) + .post("/settings/bottombar", koaBody(), async (ctx) => { + const cfg = getConfig(); + const MAX = 4; + const body = ctx.request.body || {}; + const action = String(body.action || '').trim(); + const key = String(body.key || '').trim(); + const catalog = require("../views/main_views").PINNABLE_MODULES || []; + let pins = Array.isArray(cfg.bottomBarPins) ? cfg.bottomBarPins.slice() : []; + if (action === 'add') { + if (key && catalog.some(m => m.key === key) && !pins.includes(key) && pins.length < MAX) pins.push(key); + } else if (action === 'remove') { + pins = pins.filter(k => k !== key); + } else if (action === 'clear') { + pins = []; + } + cfg.bottomBarPins = pins.slice(0, MAX); + saveConfig(cfg); + ctx.redirect('/settings/bottombar'); + }) .post("/settings/ai", koaBody(), async (ctx) => { const aiPrompt = String(ctx.request.body.ai_prompt || "").trim(); if (aiPrompt.length > 128) { sendErrorPage(ctx, "Prompt too long. Must be 128 characters or fewer.", { status: 400 }); return; } const cfg = getConfig(); - cfg.ai = { ...(cfg.ai || {}), prompt: aiPrompt, suggestions: ctx.request.body.ai_suggestions === 'on' }; + cfg.ai = { ...(cfg.ai || {}), prompt: aiPrompt }; + saveConfig(cfg); + ctx.redirect("/settings"); + }) + .post("/settings/pub-id", koaBody(), async (ctx) => { + if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } + const b = ctx.request.body, cfg = getConfig(); + cfg.walletPub = { pubId: String(b.pub_id || "").trim() }; saveConfig(cfg); ctx.redirect("/settings"); }) @@ -9338,35 +8014,16 @@ const middleware = [ const { i18n } = require('../views/main_views'); if (err.name === 'FileTooLargeError' || (err.message && err.message.includes('maxFileSize'))) { sendErrorPage(ctx, i18n.fileTooLargeMessage, { title: i18n.fileTooLargeTitle, status: 413 }); - return; + } else { + sendErrorPage(ctx, err.message || 'Internal Server Error', { status: err.status || 500 }); } - const moduleHome = moduleHomeFor(ctx); - if (moduleHome && isMissingContentError(err)) { ctx.redirect(moduleHome); return; } - sendErrorPage(ctx, err.message || 'Internal Server Error', { status: err.status || 500 }); } } }, async (ctx, next) => { if (!ctx.path.startsWith('/assets/') && !ctx.path.startsWith('/image/') && !ctx.path.startsWith('/blob/') && !ctx.path.startsWith('/qr') && !ctx.path.startsWith('/c/')) { - // filtro del hive (Personal/Community); lo leen la topbar y renderHiveNav - global.__OASIS_HIVE_FILTER__ = (ctx.path === '/activity' && (ctx.query.hive === 'personal' || ctx.query.hive === 'community')) ? ctx.query.hive : ''; - // los hexagonos SOLO en la home; en el resto de paginas el top queda limpio - global.__OASIS_SHOW_HIVE__ = (ctx.path === '/activity' || ctx.path === '/'); const now = Date.now(); if (now - sharedState.getLastRefresh() > 60000) { sharedState.setLastRefresh(now); - try { - // avatar propio para el circulo de la topbar movil (lo lee renderMobileTopbar) - const meId = getViewerId(); - const selfImg = meId ? await about.image(meId) : null; - const NULL_IMG = '&' + '0'.repeat(43) + '=.sha256'; - if (typeof selfImg === 'string' && selfImg[0] === '&' && selfImg !== NULL_IMG) { - global.__OASIS_SELF_AVATAR__ = `/image/256/${encodeURIComponent(selfImg)}`; - } else if (typeof selfImg === 'string' && selfImg[0] === '/') { - global.__OASIS_SELF_AVATAR__ = selfImg; - } else { - global.__OASIS_SELF_AVATAR__ = null; - } - } catch (_) { } try { const stats = await statsModel.getStats('ALL'); const totalMB = parseSizeMB(stats.statsBlobsSize) + parseSizeMB(stats.statsBlockchainSize); @@ -9376,17 +8033,9 @@ const middleware = [ sharedState.setCarbonHcT(hcT); sharedState.setCarbonHcH(hcH); } catch (_) {} - try { - const dataRes = await dataModel.listMatches('ALL'); - const top = (dataRes.matches || [])[0] || null; - sharedState.setBestMatch(top ? { href: top.href, title: top.title || top.id, kind: top.kind, score: top.score } : null); - } catch (_) {} try { await refreshInboxCount(); } catch (_) {} - try { await refreshMentionsCount(); } catch (_) {} try { await calendarsModel.checkDueReminders(); } catch (_) {} try { await tasksModel.checkDueReminders(); } catch (_) {} - try { await checkPoliticalChanges(); } catch (_) {} - try { await checkJobMatches(); } catch (_) {} try { const peers = await meta.connectedPeers(); sharedState.setOnlinePeerCount(Array.isArray(peers) ? peers.length : 0); @@ -9441,10 +8090,6 @@ const middleware = [ } await next(); }, - // rutas propias de la rama (Karvan, barra inferior, exportacion): fuera de la cadena de upstream - require('./karvan_routes')({ cooler, pull, pmModel, checkMod, getViewerId, - getConfig, saveConfig, - isLoopbackRequest, sendErrorPage }), routes, ]; const app = http({ host, port, middleware, allowHost: config.allowHost }); @@ -9492,9 +8137,7 @@ async function sendWelcomePmIfFirstLaunch() { welcomePmAttempted = false; return; } - try { - if (!onboardingModel.begin(ownId)) console.error('[welcome-pm] flag write failed'); - } catch (e) { + try { fs.writeFileSync(flagPath, ownId + '\n' + new Date().toISOString() + '\n'); } catch (e) { console.error('[welcome-pm] flag write failed:', e && e.message ? e.message : e); } } catch (err) { @@ -9502,207 +8145,8 @@ async function sendWelcomePmIfFirstLaunch() { welcomePmAttempted = false; } } -let welcomePmRetries = 0; -const welcomePmTick = async () => { - if (onboardingModel.firstContactSeen()) return; - await sendWelcomePmIfFirstLaunch(); - if (onboardingModel.firstContactSeen()) return; - if (welcomePmRetries++ >= 30) return; - setTimeout(welcomePmTick, 4000); -}; -setTimeout(welcomePmTick, 3000); +setTimeout(() => { sendWelcomePmIfFirstLaunch(); }, 20000); -const pbFmtDate = (iso) => iso ? moment(iso).format('YYYY-MM-DD HH:mm:ss') : ''; -const pbFmtRemaining = (endIso) => { - if (!endIso) return ''; - const ms = moment(endIso).diff(moment()); - if (ms <= 0) return '0d 00:00:00'; - const d = Math.floor(ms / 86400000); - const h = Math.floor((ms % 86400000) / 3600000); - const mi = Math.floor((ms % 3600000) / 60000); - const se = Math.floor((ms % 60000) / 1000); - const pad = n => String(n).padStart(2, '0'); - return `${d}d ${pad(h)}:${pad(mi)}:${pad(se)}`; -}; -const pbFill = (tpl, vars) => String(tpl || '').replace(/\{(\w+)\}/g, (m, k) => (k in vars ? vars[k] : m)); -const pbTitleCase = (s) => { const t = String(s || ''); return t ? t.charAt(0).toUpperCase() + t.slice(1).toLowerCase() : ''; }; -const pbAtId = (id) => id ? '@' + String(id).replace(/^@/, '') : ''; -function buildLarpRulingMsg(data = {}, t = {}) { - const house = data.house || {}; - const name = house.name || data.houseKey || ''; - const intro = pbFill(t.politicalBotLarpIntro || 'Now {house} rules during this cycle: {cycle}', { - house: `[${name}](/larp/${encodeURIComponent(data.houseKey || '')})`, - cycle: `[${data.cycleFormatted || ''}](/larp)` - }); - const blocks = [ - [intro], - [name, house.description || ''], - [ - `${t.larpRolesLabel || 'Roles'}: ${house.roles || ''}`, - `${t.larpFunctionLabel || 'Function'}: ${house.function || ''}`, - `${t.larpMonthLabel || 'Governance cycle'}: ${house.month != null ? house.month : ''}`, - `${t.larpMembersCount || 'Members'}: ${data.memberCount || 0}` - ], - [house.motto ? `“${house.motto}”` : ''] - ]; - const body = blocks - .map(block => block.filter(l => l != null && String(l).trim() !== '').join('\n')) - .filter(Boolean) - .join('\n\n'); - return { subject: 'LARP_RULING', body }; -} -function pbGovBlock(gov = {}, t = {}) { - const lines = [`${t.parliamentGovernmentCard || 'Current Government'}: ${String(gov.method || '').toUpperCase()}`]; - if (gov.leaderId) lines.push(`${t.politicalBotLeaderLabel || 'Leader'}: ${pbAtId(gov.leaderId)}`); - lines.push(`${t.parliamentLegSince || 'CYCLE SINCE'}: ${pbFmtDate(gov.since)}`); - lines.push(`${t.parliamentLegEnd || 'CYCLE END'}: ${pbFmtDate(gov.end)}`); - lines.push(`${t.parliamentTimeRemaining || 'Time remaining'}: ${pbFmtRemaining(gov.end)}`); - lines.push(`${t.parliamentPopulation || 'Population'}: ${gov.population || 0}`); - return lines; -} -function buildParliamentGovMsg(gov = {}, t = {}) { - const govLink = `[${pbTitleCase(gov.method)}](/parliament)`; - const intro = gov.leaderId - ? pbFill(t.politicalBotParliamentIntroLeader || 'The current government is {gov}, led by {leader}', { gov: govLink, leader: pbAtId(gov.leaderId) }) - : pbFill(t.politicalBotParliamentIntro || 'The current government is {gov}', { gov: govLink }); - return { subject: 'PARLIAMENT_GOV', body: [intro, ''].concat(pbGovBlock(gov, t)).join('\n') }; -} -function buildTribeGovMsg(gov = {}, t = {}) { - const govLink = `[${pbTitleCase(gov.method)}](/tribe/${encodeURIComponent(gov.tribeId || '')}?section=governance)`; - const vars = { tribe: gov.tribeName || '', gov: govLink, leader: pbAtId(gov.leaderId) }; - const intro = gov.leaderId - ? pbFill(t.politicalBotTribeIntroLeader || 'The current government in the Tribe {tribe} is {gov}, led by {leader}', vars) - : pbFill(t.politicalBotTribeIntro || 'The current government in the Tribe {tribe} is {gov}', vars); - return { subject: 'TRIBE_GOV', body: [intro, ''].concat(pbGovBlock(gov, t)).join('\n') }; -} -let jobsBotRunning = false; -async function checkJobMatches() { - if (jobsBotRunning) return; - jobsBotRunning = true; - try { - const viewerId = getViewerId(); - if (!viewerId) return; - const cv = await cvModel.getCVByUserId().catch(() => null); - if (!cv || cv.aiManaged === false) return; - const threshold = Math.min(100, Math.max(0, Number(cv.matchThreshold) || 80)) / 100; - const matches = await dataModel.jobMatchesFor(viewerId, { minScore: threshold }).catch(() => []); - if (!matches.length) return; - const announced = await pmModel.sentRefs().catch(() => new Set()); - const i18nAll = require('../client/assets/translations/i18n'); - const lang = (getConfig() && getConfig().language) || 'en'; - const t = i18nAll[lang] || i18nAll.en; - for (const m of matches.slice(0, 5)) { - const ref = `${m.id}|${Math.round(m.score * 100)}`; - if (announced.has(`JOB_MATCH|${ref}`)) continue; - const pctText = `${Math.round(m.score * 100)}%`; - const body = [ - (t.jobsBotMatchIntro || 'A job matches your curriculum.'), - '', - `${t.jobsBotMatchJob || 'Job'}: [${m.title || m.id}](${m.href})`, - `${t.matchScore || 'Match'}: ${pctText}`, - m.common.length ? `${t.commonSkills || 'Common skills'}: ${m.common.slice(0, 10).join(', ')}` : '', - '', - (t.jobsBotMatchHint || 'You receive this because your curriculum is AI managed. You can turn it off in your CV.') - ].filter(Boolean).join('\n'); - try { - await pmModel.sendMessage([], 'JOB_MATCH', body, false, ref); - announced.add(`JOB_MATCH|${ref}`); - } catch (e) { - if (config.debug) console.error('[jobs-bot] send failed:', e && e.message); - } - } - } catch (e) { - if (config.debug) console.error('[jobs-bot] failed:', e && e.message); - } finally { - jobsBotRunning = false; - } -} - -let politicalCheckRunning = false; -async function checkPoliticalChanges() { - if (politicalCheckRunning) return; - politicalCheckRunning = true; - try { - const ssbClient = await cooler.open().catch(() => null); - if (!ssbClient || !ssbClient.id) return; - const ownId = ssbClient.id; - const i18nAll = require('../client/assets/translations/i18n'); - const lang = (getConfig() && getConfig().language) || 'en'; - const t = i18nAll[lang] || i18nAll.en; - const announced = await pmModel.sentRefs().catch(() => new Set()); - const alreadyAnnounced = (subject, ref) => announced.has(`${subject}|${ref}`); - const seen = pmPolicy.readAnnounceSeen(); - const send = async (subject, body, ref) => { - const { send: shouldSend, remember } = pmPolicy.decideAnnouncement({ subject, ref, announced, seen }); - if (!shouldSend) { - if (remember) { seen[subject] = String(ref); pmPolicy.writeAnnounceSeen(seen); } - return; - } - try { - await pmModel.sendMessage([], subject, body, false, ref); - announced.add(`${subject}|${ref}`); - seen[subject] = String(ref); - pmPolicy.writeAnnounceSeen(seen); - } catch (e) { console.error('[political-bot] send failed:', e && e.message); } - }; - - try { - const houseKey = larpModel.getGoverningHouseKey(); - if (houseKey) { - let cycleFormatted = ''; - try { cycleFormatted = larpModel.computeCycle().formatted; } catch (_) {} - const ref = larpModel.getGoverningPeriodId(); - if (!alreadyAnnounced('LARP_RULING', ref)) { - const house = (larpModel.getHouse && larpModel.getHouse(houseKey)) || {}; - let memberCount = 0; - try { const hc = await larpModel.listHousesWithCounts(); const h = (hc || []).find(x => x.key === houseKey); memberCount = h ? (h.memberCount || 0) : 0; } catch (_) {} - const { subject, body } = buildLarpRulingMsg({ houseKey, house, memberCount, cycleFormatted }, t); - await send(subject, body, ref); - } - } - } catch (e) { console.error('[political-bot] larp:', e && e.message); } - - try { - const card = await parliamentModel.getLatestGovernmentCard().catch(() => null); - if (card) { - const ref = [String(card.id || card.method || ''), String(card.since || '')].filter(Boolean).join(':'); - if (ref && !alreadyAnnounced('PARLIAMENT_GOV', ref)) { - let population = 0; - try { const inh = await inhabitantsModel.listInhabitants({ filter: 'all', includeInactive: true }); population = Array.isArray(inh) ? inh.length : 0; } catch (_) {} - const leaderId = (card.powerType === 'inhabitant' && card.powerId) ? card.powerId : null; - const { subject, body } = buildParliamentGovMsg({ method: card.method, since: card.since, end: card.end, population, leaderId }, t); - await send(subject, body, ref); - } - } - } catch (e) { console.error('[political-bot] parliament:', e && e.message); } - - try { - let tribes = []; - try { tribes = await tribesModel.listAll(); } catch (_) { tribes = []; } - for (const tribe of (tribes || [])) { - if (!tribe || !tribe.id) continue; - if (!Array.isArray(tribe.members) || !tribe.members.includes(ownId)) continue; - if (tribe.parentTribeId) continue; - let term = null; - try { term = await parliamentModel.tribe.getCurrentTerm(tribe.id); } catch (_) { term = null; } - if (!term) continue; - const ref = `${tribe.id}:${String(term.id || term.startAt || term.method || '')}`; - if (!alreadyAnnounced('TRIBE_GOV', ref)) { - const lead = Array.isArray(term.leaders) && term.leaders.length ? term.leaders[0] : null; - const leaderId = lead && typeof lead === 'object' ? (lead.id || lead.powerId || null) : lead; - const population = Array.isArray(tribe.members) ? tribe.members.length : 0; - const { subject, body } = buildTribeGovMsg({ method: term.method, since: term.startAt, end: term.endAt, population, leaderId, tribeId: tribe.id, tribeName: tribe.title || tribe.name || tribe.id }, t); - await send(subject, body, ref); - } - } - } catch (e) { console.error('[political-bot] tribe:', e && e.message); } - - } catch (err) { - console.error('[political-bot] unexpected:', err && err.message); - } finally { - politicalCheckRunning = false; - } -} async function logClearnetStatus() { try { const ssbClient = await cooler.open(); diff --git a/src/backend/blobHandler.js b/src/backend/blobHandler.js index dfa6e17..95c00b8 100644 --- a/src/backend/blobHandler.js +++ b/src/backend/blobHandler.js @@ -11,166 +11,15 @@ try { } catch (e) { } -/** - * Quitar metadatos de una imagen SIN sharp. - * - * sharp no se empaqueta en Android (lleva binarios nativos y el bundle del movil - * va sin ninguno), asi que alli esta funcion devolvia la imagen intacta y las - * fotos subian con su EXIF entero, ubicacion GPS incluida. - * - * Esto recorre el contenedor y tira los bloques de metadatos, sin decodificar ni - * recomprimir la imagen: los pixeles se quedan byte a byte como estaban. - * - * De la orientacion se guarda solo eso, la orientacion. Sin sharp no se pueden - * girar los pixeles, y tirar la etiqueta dejaria las fotos verticales tumbadas; - * asi que se reconstruye un bloque EXIF minimo con esa unica etiqueta, que no - * dice nada de quien hizo la foto ni donde. - */ - -// JPEG: cada segmento es FF . Se tiran los que llevan -// metadatos y se dejan JFIF (APP0) y el perfil de color (APP2), que afectan a como -// se ve la imagen. -const JPEG_DROP = new Set([ - 0xE1, // APP1 Exif y XMP - 0xE5, // APP5 - 0xE6, // APP6 - 0xEC, // APP12 Ducky/Picture Info - 0xED, // APP13 IPTC / Photoshop - 0xEE, // APP14 Adobe - 0xFE // COM comentario -]); - -const jpegOrientation = (seg) => { - // seg empieza en "Exif\0\0"; detras va una cabecera TIFF con la IFD0 - if (seg.length < 14 || seg.toString("latin1", 0, 6) !== "Exif\0\0") return 0; - const tiff = seg.subarray(6); - const le = tiff.toString("latin1", 0, 2) === "II"; - const u16 = (o) => (le ? tiff.readUInt16LE(o) : tiff.readUInt16BE(o)); - const u32 = (o) => (le ? tiff.readUInt32LE(o) : tiff.readUInt32BE(o)); - if (u16(2) !== 0x002A) return 0; - const ifd0 = u32(4); - if (ifd0 + 2 > tiff.length) return 0; - const n = u16(ifd0); - for (let i = 0; i < n; i++) { - const e = ifd0 + 2 + i * 12; - if (e + 12 > tiff.length) break; - if (u16(e) === 0x0112) { // Orientation - const v = u16(e + 8); - return v >= 1 && v <= 8 ? v : 0; - } - } - return 0; -}; - -/** Bloque APP1 de 32 bytes con la orientacion y nada mas. */ -const exifOnlyOrientation = (orientation) => { - const b = Buffer.alloc(32); - b.write("Exif\0\0", 0, "latin1"); - b.write("II", 6, "latin1"); - b.writeUInt16LE(0x002A, 8); - b.writeUInt32LE(8, 10); // la IFD0 empieza justo detras de la cabecera TIFF - b.writeUInt16LE(1, 14); // una sola entrada - b.writeUInt16LE(0x0112, 16); // Orientation - b.writeUInt16LE(3, 18); // SHORT - b.writeUInt32LE(1, 20); // un valor - b.writeUInt16LE(orientation, 24); - b.writeUInt32LE(0, 28); // no hay mas IFDs - return b; -}; - -const stripJpeg = (buf) => { - if (buf.length < 4 || buf[0] !== 0xFF || buf[1] !== 0xD8) return null; - const out = [buf.subarray(0, 2)]; - let orientation = 0; - let i = 2; - while (i + 4 <= buf.length) { - if (buf[i] !== 0xFF) return null; // no es un JPEG sano: no tocarlo - const marker = buf[i + 1]; - if (marker === 0xD8 || (marker >= 0xD0 && marker <= 0xD9)) { i += 2; continue; } - if (marker === 0xDA) { out.push(buf.subarray(i)); break; } // datos comprimidos: hasta el final - const len = buf.readUInt16BE(i + 2); - if (len < 2 || i + 2 + len > buf.length) return null; - const seg = buf.subarray(i + 4, i + 2 + len); - if (marker === 0xE1 && !orientation) orientation = jpegOrientation(seg); - if (!JPEG_DROP.has(marker)) out.push(buf.subarray(i, i + 2 + len)); - i += 2 + len; - } - if (orientation > 1) { - const exif = exifOnlyOrientation(orientation); - const head = Buffer.alloc(4); - head.writeUInt16BE(0xFFE1, 0); - head.writeUInt16BE(exif.length + 2, 2); - out.splice(1, 0, head, exif); - } - return Buffer.concat(out); -}; - -// PNG: cadena de bloques . -const PNG_DROP = new Set(["tEXt", "zTXt", "iTXt", "eXIf", "tIME", "dSIG"]); - -const stripPng = (buf) => { - const SIG = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); - if (buf.length < 8 || !buf.subarray(0, 8).equals(SIG)) return null; - const out = [buf.subarray(0, 8)]; - let i = 8; - while (i + 8 <= buf.length) { - const len = buf.readUInt32BE(i); - const type = buf.toString("latin1", i + 4, i + 8); - const end = i + 12 + len; - if (end > buf.length) return null; - if (!PNG_DROP.has(type)) out.push(buf.subarray(i, end)); - i = end; - if (type === "IEND") break; - } - return Buffer.concat(out); -}; - -// WebP: contenedor RIFF con bloques (+1 de relleno -// si la longitud es impar). -const stripWebp = (buf) => { - if (buf.length < 12) return null; - if (buf.toString("latin1", 0, 4) !== "RIFF" || buf.toString("latin1", 8, 12) !== "WEBP") return null; - const out = []; - let i = 12; - while (i + 8 <= buf.length) { - const type = buf.toString("latin1", i, i + 4); - const len = buf.readUInt32LE(i + 4); - const end = i + 8 + len + (len % 2); - if (end > buf.length) return null; - if (type !== "EXIF" && type !== "XMP ") out.push(buf.subarray(i, end)); - i = end; - } - const body = Buffer.concat(out); - const head = Buffer.alloc(12); - head.write("RIFF", 0, "latin1"); - head.writeUInt32LE(body.length + 4, 4); - head.write("WEBP", 8, "latin1"); - return Buffer.concat([head, body]); -}; - -/** Limpia segun el formato; si algo no cuadra, devuelve la imagen tal cual. */ -const stripMetadataPure = (buffer) => { +const stripImageMetadata = async (buffer) => { + if (typeof sharp !== "function") return buffer; try { - const out = stripJpeg(buffer) || stripPng(buffer) || stripWebp(buffer); - return out && out.length ? out : buffer; + return await sharp(buffer).rotate().toBuffer(); } catch { return buffer; } }; -const stripImageMetadata = async (buffer) => { - // Con sharp se prefiere su camino: ademas de quitar metadatos, aplica la - // orientacion girando los pixeles de verdad. - if (typeof sharp === "function") { - try { - return await sharp(buffer).rotate().toBuffer(); - } catch { - return stripMetadataPure(buffer); - } - } - return stripMetadataPure(buffer); -}; - const PDF_METADATA_KEYS = [ '/Title', '/Author', '/Subject', '/Keywords', '/Creator', '/Producer', '/CreationDate', '/ModDate' @@ -209,30 +58,25 @@ class FileTooLargeError extends Error { } } -const OGG_VIDEO_MARKERS = ['theora', 'VP80', 'OggDS', 'Dirac']; -const OGG_AUDIO_MARKERS = ['vorbis', 'OpusHead', 'FLAC', 'Speex']; +const handleBlobUpload = async function (ctx, fileFieldName) { + if (!ctx.request.files || !ctx.request.files[fileFieldName]) { + return null; + } -const oggMime = (buffer) => { - const head = buffer.slice(0, 65536); - for (const marker of OGG_VIDEO_MARKERS) if (head.includes(Buffer.from(marker))) return 'video/ogg'; - for (const marker of OGG_AUDIO_MARKERS) if (head.includes(Buffer.from(marker))) return 'audio/ogg'; - return 'video/ogg'; -}; - -const storeUploadedFile = async function (blobUpload) { - if (!blobUpload || !blobUpload.filepath) return null; + const blobUpload = ctx.request.files[fileFieldName]; + if (!blobUpload) return null; let data = await promisesFs.readFile(blobUpload.filepath); if (data.length === 0) return null; if (data.length > MAX_BLOB_SIZE) { - throw new FileTooLargeError(blobUpload.originalFilename || blobUpload.name || 'file', data.length); + throw new FileTooLargeError(blobUpload.originalFilename || blobUpload.name || fileFieldName, data.length); } const EXTENSION_MIME_MAP = { '.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg', - '.ogv': 'video/ogg', '.oga': 'audio/ogg', '.mkv': 'video/x-matroska', '.avi': 'video/x-msvideo', '.mov': 'video/quicktime', - '.mp3': 'audio/mpeg', '.wav': 'audio/wav', + '.ogv': 'video/ogg', '.avi': 'video/x-msvideo', '.mov': 'video/quicktime', + '.mkv': 'video/x-matroska', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.flac': 'audio/flac', '.aac': 'audio/aac', '.opus': 'audio/opus', '.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', @@ -249,12 +93,9 @@ const storeUploadedFile = async function (blobUpload) { blob.mime = null; } - if (blob.mime === 'application/ogg') blob.mime = oggMime(data); - - const GENERIC_MIMES = new Set(['application/octet-stream', 'video/x-matroska']); - if ((!blob.mime || GENERIC_MIMES.has(blob.mime)) && blob.name) { + if (!blob.mime && blob.name) { const ext = (blob.name.match(/\.[^.]+$/) || [''])[0].toLowerCase(); - blob.mime = EXTENSION_MIME_MAP[ext] || blob.mime || 'application/octet-stream'; + blob.mime = EXTENSION_MIME_MAP[ext] || 'application/octet-stream'; } if (!blob.mime) { @@ -285,25 +126,6 @@ const storeUploadedFile = async function (blobUpload) { return `\n[${blob.name}](${blob.id})`; }; -const handleBlobUpload = async function (ctx, fileFieldName) { - if (!ctx.request.files || !ctx.request.files[fileFieldName]) return null; - const entry = ctx.request.files[fileFieldName]; - const first = Array.isArray(entry) ? entry[0] : entry; - return storeUploadedFile(first); -}; - -const handleBlobUploads = async function (ctx, fileFieldName, max = 8) { - if (!ctx.request.files || !ctx.request.files[fileFieldName]) return []; - const entry = ctx.request.files[fileFieldName]; - const files = (Array.isArray(entry) ? entry : [entry]).slice(0, Math.max(0, max)); - const out = []; - for (const file of files) { - const stored = await storeUploadedFile(file); - if (stored) out.push(stored); - } - return out; -}; - function waitForBlob(ssbClient, blobId, timeoutMs = 8000) { return new Promise((resolve, reject) => { let done = false; @@ -407,8 +229,6 @@ const serveBlob = async function (ctx) { } } - if (mime === 'application/ogg') mime = oggMime(buffer); - const isSvg = mime === 'image/svg+xml'; const qName = ctx.query.name ? String(ctx.query.name).replace(/["\r\n\\]/g, '').trim() : ''; const safeRaw = String(raw).replace(/["\r\n\\]/g, ''); @@ -455,5 +275,5 @@ const serveBlob = async function (ctx) { } }; -module.exports = { handleBlobUpload, handleBlobUploads, serveBlob, oggMime, FileTooLargeError }; +module.exports = { handleBlobUpload, serveBlob, FileTooLargeError }; diff --git a/src/backend/contentPdf.js b/src/backend/contentPdf.js deleted file mode 100644 index c84e05b..0000000 --- a/src/backend/contentPdf.js +++ /dev/null @@ -1,280 +0,0 @@ -const { buildDocumentPdf } = require('./pdfDocument'); - -const fmtDate = v => { - if (!v) return ''; - const d = new Date(v); - return isNaN(d.getTime()) ? String(v) : d.toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; -}; - -const fmtDay = v => { - if (!v) return ''; - const d = new Date(v); - return isNaN(d.getTime()) ? String(v) : d.toISOString().slice(0, 10); -}; - -const asList = v => (Array.isArray(v) ? v : []); -const txt = v => (v == null ? '' : String(v)); - -const humanLabel = (key) => String(key || '') - .replace(/([a-z0-9])([A-Z])/g, '$1 $2') - .replace(/^./, c => c.toUpperCase()); - -const pushMeta = (out, item) => { - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'METADATA' }); - if (item.id) out.push({ kind: 'kv', label: 'Content ID', value: item.id }); - const author = item.author || item.organizer || item.createdBy || item.from || ''; - if (author) out.push({ kind: 'kv', label: 'Author', value: author }); - if (item.createdAt) out.push({ kind: 'kv', label: 'Created At', value: fmtDate(item.createdAt) }); - if (item.updatedAt) out.push({ kind: 'kv', label: 'Updated At', value: fmtDate(item.updatedAt) }); -}; - -const pushOpinions = (out, item) => { - const op = item.opinions && typeof item.opinions === 'object' ? item.opinions : {}; - const entries = Object.entries(op).filter(([, n]) => Number(n) > 0); - if (!entries.length) return; - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'OPINIONS' }); - for (const [cat, n] of entries) out.push({ kind: 'kv', label: humanLabel(cat), value: String(n) }); -}; - -const pushTags = (out, item) => { - const tags = asList(item.tags).filter(Boolean); - if (tags.length) out.push({ kind: 'kv', label: 'Tags', value: tags.join(', ') }); -}; - -const reportSections = (report) => { - const out = []; - out.push({ kind: 'title', text: `Report: ${txt(report.title) || '-'}` }); - out.push({ kind: 'blank' }); - - out.push({ kind: 'section', text: 'CLASSIFICATION' }); - out.push({ kind: 'kv', label: 'Category', value: txt(report.category).toUpperCase() }); - out.push({ kind: 'kv', label: 'Severity', value: txt(report.severity).toUpperCase() }); - out.push({ kind: 'kv', label: 'Status', value: txt(report.status).toUpperCase() }); - pushTags(out, report); - - if (txt(report.description).trim()) { - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'DESCRIPTION' }); - out.push({ kind: 'text', text: report.description }); - } - - const tpl = report.template && typeof report.template === 'object' ? report.template : {}; - const tplEntries = Object.entries(tpl).filter(([, v]) => txt(v).trim()); - if (tplEntries.length) { - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'DETAILS' }); - for (const [k, v] of tplEntries) out.push({ kind: 'kv', label: humanLabel(k), value: v }); - } - - const confirmations = asList(report.confirmations); - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'CONFIRMATIONS' }); - out.push({ kind: 'kv', label: 'Confirmed', value: String(confirmations.length) }); - for (const c of confirmations) out.push({ kind: 'kv', label: 'Confirmed by', value: c }); - - pushOpinions(out, report); - pushMeta(out, report); - return out; -}; - -const voteSections = (vote) => { - const out = []; - out.push({ kind: 'title', text: `Votation: ${txt(vote.question) || '-'}` }); - out.push({ kind: 'blank' }); - - out.push({ kind: 'section', text: 'TERMS' }); - out.push({ kind: 'kv', label: 'Status', value: txt(vote.status).toUpperCase() }); - if (vote.deadline) out.push({ kind: 'kv', label: 'Deadline', value: fmtDate(vote.deadline) }); - pushTags(out, vote); - - const votes = vote.votes && typeof vote.votes === 'object' ? vote.votes : {}; - const total = Number(vote.totalVotes) || asList(vote.voters).length; - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'RESULTS' }); - out.push({ kind: 'kv', label: 'Total votes', value: String(total) }); - for (const [choice, n] of Object.entries(votes)) { - const count = Number(n) || 0; - const pct = total > 0 ? Math.round((count / total) * 100) : 0; - out.push({ kind: 'kv', label: choice, value: `${count} (${pct}%)` }); - } - - pushOpinions(out, vote); - pushMeta(out, vote); - return out; -}; - -const eventSections = (event) => { - const out = []; - out.push({ kind: 'title', text: `Event: ${txt(event.title) || '-'}` }); - out.push({ kind: 'blank' }); - - out.push({ kind: 'section', text: 'SCHEDULE' }); - out.push({ kind: 'kv', label: 'Date', value: fmtDate(event.date) }); - out.push({ kind: 'kv', label: 'Status', value: txt(event.status).toUpperCase() }); - if (event.location) out.push({ kind: 'kv', label: 'Location', value: event.location }); - if (event.mapUrl) out.push({ kind: 'kv', label: 'Map', value: event.mapUrl }); - if (Number(event.price) > 0) out.push({ kind: 'kv', label: 'Price', value: `${Number(event.price)} ECO` }); - out.push({ kind: 'kv', label: 'Privacy', value: txt(event.isPublic).toUpperCase() }); - if (event.url) out.push({ kind: 'kv', label: 'Url', value: event.url }); - pushTags(out, event); - - if (txt(event.description).trim()) { - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'DESCRIPTION' }); - out.push({ kind: 'text', text: event.description }); - } - - const attendees = asList(event.attendees); - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'ATTENDEES' }); - out.push({ kind: 'kv', label: 'Total', value: String(attendees.length) }); - for (const a of attendees) out.push({ kind: 'kv', label: 'Attendee', value: a }); - - pushOpinions(out, event); - pushMeta(out, event); - return out; -}; - -const taskSections = (task) => { - const out = []; - out.push({ kind: 'title', text: `Task: ${txt(task.title) || '-'}` }); - out.push({ kind: 'blank' }); - - out.push({ kind: 'section', text: 'SCHEDULE' }); - out.push({ kind: 'kv', label: 'Status', value: txt(task.status).toUpperCase() }); - out.push({ kind: 'kv', label: 'Priority', value: txt(task.priority).toUpperCase() }); - if (task.startTime) out.push({ kind: 'kv', label: 'Starts', value: fmtDate(task.startTime) }); - if (task.endTime) out.push({ kind: 'kv', label: 'Ends', value: fmtDate(task.endTime) }); - if (task.location) out.push({ kind: 'kv', label: 'Location', value: task.location }); - out.push({ kind: 'kv', label: 'Privacy', value: txt(task.isPublic).toUpperCase() }); - pushTags(out, task); - - if (txt(task.description).trim()) { - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'DESCRIPTION' }); - out.push({ kind: 'text', text: task.description }); - } - - const assignees = asList(task.assignees); - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'ASSIGNEES' }); - out.push({ kind: 'kv', label: 'Total', value: String(assignees.length) }); - for (const a of assignees) out.push({ kind: 'kv', label: 'Assignee', value: a }); - - pushOpinions(out, task); - pushMeta(out, task); - return out; -}; - -const calendarSections = (calendar, extra = {}) => { - const dates = asList(extra.dates); - const notesByDate = extra.notesByDate && typeof extra.notesByDate === 'object' ? extra.notesByDate : {}; - - const out = []; - out.push({ kind: 'title', text: `Calendar: ${txt(calendar.title) || '-'}` }); - out.push({ kind: 'blank' }); - - out.push({ kind: 'section', text: 'SUMMARY' }); - out.push({ kind: 'kv', label: 'Status', value: calendar.isClosed ? 'CLOSED' : txt(calendar.status).toUpperCase() }); - if (calendar.deadline) out.push({ kind: 'kv', label: 'Deadline', value: fmtDate(calendar.deadline) }); - if (calendar.mapUrl) out.push({ kind: 'kv', label: 'Map', value: calendar.mapUrl }); - out.push({ kind: 'kv', label: 'Participants', value: String(asList(calendar.participants).length) }); - out.push({ kind: 'kv', label: 'Dates', value: String(dates.length) }); - const noteTotal = Object.values(notesByDate).reduce((n, arr) => n + asList(arr).length, 0); - out.push({ kind: 'kv', label: 'Notes', value: String(noteTotal) }); - pushTags(out, calendar); - - const participants = asList(calendar.participants); - if (participants.length) { - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'PARTICIPANTS' }); - for (const p of participants) out.push({ kind: 'kv', label: 'Participant', value: p }); - } - - if (dates.length) { - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'DATES' }); - for (const d of dates) { - const day = fmtDay(d && d.date) || txt(d && d.date); - out.push({ kind: 'kv', label: day, value: txt(d && d.label) }); - for (const note of asList(notesByDate[d && d.key])) { - const noteText = txt(note && note.text); - if (noteText) out.push({ kind: 'text', text: ` - ${noteText}` }); - } - } - } - - pushMeta(out, calendar); - return out; -}; - -const cvSections = (cv) => { - const out = []; - out.push({ kind: 'title', text: `Curriculum: ${txt(cv.name) || txt(cv.author) || '-'}` }); - out.push({ kind: 'blank' }); - - out.push({ kind: 'section', text: 'PROFILE' }); - if (cv.location) out.push({ kind: 'kv', label: 'Location', value: cv.location }); - if (cv.status) out.push({ kind: 'kv', label: 'Status', value: txt(cv.status).toUpperCase() }); - if (cv.preferences) out.push({ kind: 'kv', label: 'Preferences', value: txt(cv.preferences).toUpperCase() }); - if (cv.languages) out.push({ kind: 'kv', label: 'Languages', value: cv.languages }); - - if (txt(cv.description).trim()) { - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'DESCRIPTION' }); - out.push({ kind: 'text', text: cv.description }); - } - - const blocks = [ - ['PERSONAL', cv.personalExperiences, cv.personalSkills], - ['EDUCATION', cv.educationExperiences, cv.educationalSkills], - ['PROFESSIONAL', cv.professionalExperiences, cv.professionalSkills], - ['OASIS', cv.oasisExperiences, cv.oasisSkills] - ]; - - for (const [heading, experiences, skills] of blocks) { - const skillList = asList(skills).filter(Boolean); - const body = txt(experiences).trim(); - if (!body && !skillList.length) continue; - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: heading }); - if (body) out.push({ kind: 'text', text: body }); - if (skillList.length) out.push({ kind: 'kv', label: 'Skills', value: skillList.join(', ') }); - } - - pushMeta(out, cv); - return out; -}; - -const BUILDERS = { - reports: { title: 'OASIS - Report', sections: reportSections, name: item => item.title }, - votes: { title: 'OASIS - Votation', sections: voteSections, name: item => item.question }, - events: { title: 'OASIS - Event', sections: eventSections, name: item => item.title }, - tasks: { title: 'OASIS - Task', sections: taskSections, name: item => item.title }, - calendars: { title: 'OASIS - Calendar', sections: calendarSections, name: item => item.title }, - cv: { title: 'OASIS - Curriculum', sections: cvSections, name: item => item.name || item.author } -}; - -const isSupported = kind => Object.prototype.hasOwnProperty.call(BUILDERS, kind); - -const pdfFilename = (kind, item) => { - const b = BUILDERS[kind]; - const raw = b ? txt(b.name(item || {})) : ''; - const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40); - return `oasis-${kind}-${slug || 'document'}.pdf`; -}; - -const buildContentPdf = (kind, item, extra = {}, viewerId = null) => { - const b = BUILDERS[kind]; - if (!b) throw new Error('Unsupported pdf kind'); - return buildDocumentPdf({ - title: b.title, - issuedToLabel: 'Issued to', - issuedTo: viewerId || null, - sections: b.sections(item || {}, extra) - }); -}; - -module.exports = { buildContentPdf, pdfFilename, isSupported }; diff --git a/src/backend/fileshare_crypto.js b/src/backend/fileshare_crypto.js deleted file mode 100644 index d6bef73..0000000 --- a/src/backend/fileshare_crypto.js +++ /dev/null @@ -1,74 +0,0 @@ -const crypto = require('crypto'); - -const ALG = 'aes-256-gcm'; -const IV_LEN = 12; -const TAG_LEN = 16; -const KEY_LEN = 32; -const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024; - -const generateFileKey = () => crypto.randomBytes(KEY_LEN); - -const keyToHex = (key) => Buffer.isBuffer(key) ? key.toString('hex') : String(key); - -const keyFromHex = (hex) => { - const buf = Buffer.from(String(hex), 'hex'); - if (buf.length !== KEY_LEN) throw new Error('Invalid file key'); - return buf; -}; - -const encryptChunk = (plaintext, key) => { - const iv = crypto.randomBytes(IV_LEN); - const cipher = crypto.createCipheriv(ALG, key, iv); - const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]); - const tag = cipher.getAuthTag(); - return Buffer.concat([iv, tag, ct]); -}; - -const decryptChunk = (payload, key) => { - if (!Buffer.isBuffer(payload) || payload.length < IV_LEN + TAG_LEN) throw new Error('Corrupt chunk'); - const iv = payload.subarray(0, IV_LEN); - const tag = payload.subarray(IV_LEN, IV_LEN + TAG_LEN); - const ct = payload.subarray(IV_LEN + TAG_LEN); - const decipher = crypto.createDecipheriv(ALG, key, iv); - decipher.setAuthTag(tag); - return Buffer.concat([decipher.update(ct), decipher.final()]); -}; - -const sha256Hex = (buffer) => crypto.createHash('sha256').update(buffer).digest('hex'); - -const splitBuffer = (buffer, chunkSize = DEFAULT_CHUNK_SIZE) => { - const size = Math.max(1, Number(chunkSize) || DEFAULT_CHUNK_SIZE); - const out = []; - for (let off = 0; off < buffer.length; off += size) { - out.push(buffer.subarray(off, Math.min(off + size, buffer.length))); - } - if (!out.length) out.push(Buffer.alloc(0)); - return out; -}; - -const buildManifest = ({ filename, mime, size, chunkSize, chunkHashes, plainSha256 }) => ({ - v: 1, - alg: ALG, - filename: String(filename || 'file'), - mime: String(mime || 'application/octet-stream'), - size: Number(size) || 0, - chunkSize: Number(chunkSize) || DEFAULT_CHUNK_SIZE, - chunks: Array.isArray(chunkHashes) ? chunkHashes.slice() : [], - plainSha256: plainSha256 || null -}); - -const encryptManifest = (manifest, key) => encryptChunk(Buffer.from(JSON.stringify(manifest), 'utf8'), key); - -const decryptManifest = (payload, key) => { - const json = decryptChunk(payload, key).toString('utf8'); - const m = JSON.parse(json); - if (!m || m.v !== 1 || !Array.isArray(m.chunks)) throw new Error('Invalid manifest'); - return m; -}; - -module.exports = { - ALG, IV_LEN, TAG_LEN, KEY_LEN, DEFAULT_CHUNK_SIZE, - generateFileKey, keyToHex, keyFromHex, - encryptChunk, decryptChunk, sha256Hex, splitBuffer, - buildManifest, encryptManifest, decryptManifest -}; diff --git a/src/backend/karvan_routes.js b/src/backend/karvan_routes.js deleted file mode 100644 index 7a5c80d..0000000 --- a/src/backend/karvan_routes.js +++ /dev/null @@ -1,237 +0,0 @@ -"use strict"; -// Rutas del modulo Karvan (salas efimeras en RAM, mensajes temporales y -// senalizacion WebRTC), mas las dos pantallas que la rama anade a los ajustes. -// -// Upstream pone todas sus rutas en backend.js: 625 en una sola cadena fluida. -// Estas viven aparte porque ese fichero se reescribe entero en cada release -// (+833/-286 en la 0.9.2, +266/-31 en la 0.9.1), asi que una ruta anadida en -// medio entra en conflicto cada vez. Se montan en un router propio insertado -// antes del de upstream; lo que no casa cae a next() y sigue su curso. -// -// El nombre sigue a la familia del modulo: karvan_model.js, karvan_view.js, -// karvan.js, karvan.css. -// -// Las dependencias se inyectan en lugar de reconstruirse: cooler abre la conexion -// al sbot y duplicarla abriria un segundo cliente muxrpc. - -const koaRouter = require("../server/node_modules/@koa/router"); -const { koaBody } = require("../server/node_modules/koa-body"); -const { stripDangerousTags } = require("./sanitizeHtml"); -const { buildIceServers } = require("./turnCredentials"); -const karvanModel = require('../models/karvan_model')({}); // FORK_IA_UX: salas efímeras en RAM (mensajes temporales + señalización WebRTC) -const { karvanView, karvanRoomView } = require("../views/karvan_view"); - -module.exports = ({ cooler, pull, pmModel, checkMod, getViewerId, - getConfig, saveConfig, isLoopbackRequest, sendErrorPage }) => { -// FORK_IA_UX Fase 2 (cross-device): relay de MENSAJES de Karvan por privados SSB. Funciona vía un pub -// (el privado se replica al otro móvil); los SIGNAL de WebRTC NO se relayan (serían demasiados/lentos por el log). -// No toca el arranque del SSB. Ingesta por stream EN VIVO (no polling) → eficiente. -const karvanSeenRelay = new Set(); // mids ya inyectados (evita duplicados/bucles) -let karvanRelaySub = false; // ¿suscriptor del log en vivo montado? -async function karvanPublishRelay(roomId, mid, from, text) { - try { - const feeds = karvanModel.getRemoteFeeds(roomId); - if (!feeds.length) return; - const ssbClient = await cooler.open(); - const recps = [...new Set([ssbClient.id, ...feeds])].slice(0, 8); - await new Promise((res, rej) => ssbClient.private.publish( - { type: 'karvan-relay', roomId, mid: String(mid || '').slice(0, 60), from: String(from || '?').slice(0, 80), text: String(text || '').slice(0, 2000), private: true }, - recps, (e, m) => e ? rej(e) : res(m))); - } catch (e) {} -} -async function karvanStartRelay() { - if (karvanRelaySub) return; - karvanRelaySub = true; - try { - const ssbClient = await cooler.open(); - const me = ssbClient.id; - pull( - ssbClient.createLogStream({ old: false, live: true }), - pull.drain((m) => { - try { - if (!m || !m.value) return; - let dec; try { dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.timestamp }); } catch (_) { return; } - const c = dec && dec.value && dec.value.content; - if (!c || c.type !== 'karvan-relay' || !c.roomId || !c.mid) return; - if (dec.value.author === me) return; // no reinyectar lo propio (evita bucle A→B→A) - if (karvanSeenRelay.has(c.mid)) return; - if (!karvanModel.getRoom(c.roomId)) return; // solo si tenemos esa sala localmente - karvanSeenRelay.add(c.mid); - if (karvanSeenRelay.size > 5000) karvanSeenRelay.clear(); - karvanModel.postMessage(c.roomId, { from: c.from, text: c.text, mid: c.mid }); - } catch (e) {} - }) - ); - } catch (e) { karvanRelaySub = false; } -} -// ¿quién me invitó a esta sala? (para registrar su feed en el espejo local) — reutiliza el lector de privados -async function karvanInviteAuthor(roomId) { - try { - const msgs = await pmModel.listAllPrivate(); - for (const m of (msgs || [])) { - const c = m && m.value && m.value.content; - if (c && c.subject === 'KARVAN_INVITE' && typeof c.text === 'string' && c.text.indexOf('/karvan/' + roomId) !== -1) return m.value.author; - } - } catch (e) {} - return null; -} - - const forkRouter = new koaRouter(); - forkRouter - // Configuracion ICE para las llamadas. Solo loopback: la credencial TURN va - // firmada con el secreto del nodo y no debe salir de la maquina. - .get('/karvan/ice', async (ctx) => { - if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; } - if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = { error: 'forbidden' }; return; } - let who = ''; try { who = getViewerId() || ''; } catch (e) {} - let rtc = null; try { rtc = (getConfig() || {}).rtc || null; } catch (e) {} - ctx.body = buildIceServers(rtc, who); - }) - // ===== FORK_IA_UX: módulo KARVAN (mensajes temporales RAM + WebRTC data-channel) ===== - .get('/karvan', async (ctx) => { - if (!checkMod(ctx, 'karvanMod')) { ctx.redirect('/modules'); return; } - let selfId = ''; try { selfId = getViewerId(); } catch (e) {} - ctx.body = karvanView(karvanModel.listRooms(), { selfId }); - }) - .post('/karvan/create', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'karvanMod')) { ctx.redirect('/modules'); return; } - const b = ctx.request.body || {}; - const room = karvanModel.createRoom({ - title: stripDangerousTags(String(b.title || '').trim()).slice(0, 80), - ttlMinutes: b.ttl, - }); - ctx.redirect('/karvan/' + room.id); - }) - .get('/karvan/:id', async (ctx) => { - if (!checkMod(ctx, 'karvanMod')) { ctx.redirect('/modules'); return; } - let room = karvanModel.getRoom(ctx.params.id); - // unirse por enlace/invitación crea un espejo local, pero SOLO en una navegación real (documento): - // un /subrecurso de una web maliciosa NO debe poder crear/expulsar salas por GET (CSRF). El navegador - // controla sec-fetch-dest/accept, así que son señales fiables. - let justAdopted = false; - if (!room) { - const isNav = ctx.get('sec-fetch-dest') === 'document' || (ctx.get('accept') || '').includes('text/html'); - if (isNav) { room = karvanModel.adoptRoom({ id: ctx.params.id }); justAdopted = !!room; } - } - if (!room) { ctx.redirect('/karvan'); return; } - if (justAdopted) { // al unirte por enlace, registra el feed de quien te invitó → relay cross-device - const author = await karvanInviteAuthor(ctx.params.id); - if (author) { karvanModel.addRemoteFeed(ctx.params.id, author); karvanStartRelay(); } - } - let selfId = ''; try { selfId = getViewerId(); } catch (e) {} - const msgs = karvanModel.listMessages(room.id, 0) || []; - ctx.body = karvanRoomView(room, msgs, { selfId, invite: ctx.query.invite || '' }); - }) - .post('/karvan/:id/invite', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; } - const id = ctx.params.id; - const room = karvanModel.getRoom(id); - if (!room) { ctx.redirect('/karvan'); return; } - const to = String((ctx.request.body || {}).to || '').trim(); - if (!/^@[A-Za-z0-9+/=]{20,}\.ed25519$/.test(to)) { ctx.redirect('/karvan/' + id + '?invite=bad'); return; } - try { - // reutiliza el patrón de invitación por mensaje privado de Oasis (como el módulo industry): - // le llega al INBOX del contacto con un enlace a la sala. Sin código SSB nuevo. - await pmModel.sendMessage([to], 'KARVAN_INVITE', - 'You have been invited to an ephemeral Karvan room' + (room.title ? ' ("' + room.title + '")' : '') + ' -> /karvan/' + id); - karvanModel.addRemoteFeed(id, to); karvanStartRelay(); // relay cross-device hacia el invitado - ctx.redirect('/karvan/' + id + '?invite=ok'); - } catch (e) { ctx.redirect('/karvan/' + id + '?invite=fail'); } - }) - .post('/karvan/:id/msg', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; } - const b = ctx.request.body || {}; - let selfId = ''; try { selfId = getViewerId(); } catch (e) {} - const msg = karvanModel.postMessage(ctx.params.id, { - from: String(b.from || selfId || '?').slice(0, 80), - text: stripDangerousTags(String(b.text || '')).slice(0, 2000), - mid: b.mid, - }); - if (!msg) { ctx.status = 404; ctx.body = { error: 'no room' }; return; } - karvanPublishRelay(ctx.params.id, msg.mid || (msg.from + '-' + msg.ts + '-' + msg.seq), msg.from, msg.text); // → feeds remotos (si hay) - const wantsJson = (ctx.get('accept') || '').indexOf('application/json') !== -1 - || (ctx.get('content-type') || '').indexOf('application/json') !== -1; - if (wantsJson) ctx.body = { ok: true, seq: msg.seq }; - else ctx.redirect('/karvan/' + ctx.params.id); - }) - .get('/karvan/:id/msgs', async (ctx) => { - if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; } - const msgs = karvanModel.listMessages(ctx.params.id, ctx.query.since || 0); - ctx.body = { messages: msgs || [] }; - }) - .post('/karvan/:id/signal', koaBody(), async (ctx) => { - if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; } - const b = ctx.request.body || {}; - karvanModel.postSignal(ctx.params.id, { from: b.from, to: b.to, payload: b.payload }); - ctx.body = { ok: true }; - }) - .get('/karvan/:id/signal', async (ctx) => { - if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; } - const r = karvanModel.getSignals(ctx.params.id, { forId: ctx.query.for || '', since: ctx.query.since || 0 }); - ctx.body = r || { signals: [], members: [] }; - }) - .get("/settings/bottombar", async (ctx) => { - ctx.body = require("../views/main_views").bottomBarPickerView(); - }) - .post("/settings/bottombar", koaBody(), async (ctx) => { - const cfg = getConfig(); - const MAX = 4; - const body = ctx.request.body || {}; - const action = String(body.action || '').trim(); - const key = String(body.key || '').trim(); - const catalog = require("../views/main_views").PINNABLE_MODULES || []; - let pins = Array.isArray(cfg.bottomBarPins) ? cfg.bottomBarPins.slice() : []; - if (action === 'add') { - if (key && catalog.some(m => m.key === key) && !pins.includes(key) && pins.length < MAX) pins.push(key); - } else if (action === 'remove') { - pins = pins.filter(k => k !== key); - } else if (action === 'clear') { - pins = []; - } - cfg.bottomBarPins = pins.slice(0, MAX); - saveConfig(cfg); - ctx.redirect('/settings/bottombar'); - }) - .post("/settings/identity/use", koaBody(), async (ctx) => { - // solo anota cual sera la del proximo arranque: cambiarla en caliente dejaria - // media aplicacion hablando con el sbot anterior - const accounts = require('./accounts.js'); - const name = String((ctx.request.body || {}).account || '').trim(); - if (!accounts.isValid(name) || !accounts.list().includes(name)) { - sendErrorPage(ctx, require('../views/main_views').i18n.identitiesInvalidName, { status: 400 }); - return; - } - accounts.setActive(name); - ctx.redirect('/settings'); - }) - .post("/settings/identity/create", koaBody(), async (ctx) => { - const accounts = require('./accounts.js'); - const name = String((ctx.request.body || {}).account || '').trim(); - if (!accounts.isValid(name)) { - sendErrorPage(ctx, require('../views/main_views').i18n.identitiesInvalidName, { status: 400 }); - return; - } - // no se genera el secret aqui: lo crea ssb-config al arrancar con esa cuenta, - // que es quien sabe como hacerlo. Basta con anotarla y reiniciar. - accounts.setActive(name); - ctx.redirect('/settings'); - }) - .post("/settings/identity/restart", koaBody(), async (ctx) => { - // Solo en movil: alli el backend vive en un servicio en primer plano que - // sobrevive a cerrar la app, asi que la unica forma de arrancar con otra - // identidad es terminar el proceso. nodejs-mobile no permite volver a - // levantar el runtime dentro del mismo proceso: hay que salir y que la app - // lo relance al abrirse. - if (process.env.OASIS_MOBILE !== '1') { ctx.status = 404; ctx.body = ''; return; } - ctx.body = require("../views/main_views").template( - require('../views/main_views').i18n.identitiesTitle, - require("../server/node_modules/hyperaxe").section( - require("../server/node_modules/hyperaxe").p(require('../views/main_views').i18n.identitiesRestarting) - ) - ); - // se responde primero: si se sale antes, el navegador se queda sin pagina - setTimeout(() => process.exit(0), 500); - }); - - return forkRouter.routes(); -}; diff --git a/src/backend/logsPdf.js b/src/backend/logsPdf.js index 676ac90..ca25ec3 100644 --- a/src/backend/logsPdf.js +++ b/src/backend/logsPdf.js @@ -3,7 +3,7 @@ const path = require('path'); const LOGO_PATH = path.join(__dirname, '..', 'client', 'assets', 'images', 'snh-oasis.jpg'); -const { escapePdf } = require('./pdfDocument'); +const escapePdf = s => String(s || '').replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); const linkPattern = /(?:https?:\/\/[^\s]+|www\.[^\s]+|@[A-Za-z0-9+/=.\-]+\.ed25519|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g; @@ -94,8 +94,8 @@ function buildLogsPdf(entries, oasisId, opts = {}) { const catalogId = addObj(null); const pagesId = addObj(null); - const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>'); - const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold /Encoding /WinAnsiEncoding >>'); + const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>'); + const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold >>'); let logoXObjId = null; if (logoBuf && logoDims) { @@ -156,7 +156,7 @@ function buildLogsPdf(entries, oasisId, opts = {}) { parts.push(`BT\n/F1 8 Tf\n${pageW - marginX - pageLabelW} ${footerH - 10} Td\n(${escapePdf(pageLabel)}) Tj\nET`); const content = parts.join('\n'); - const stream = `<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`; + const stream = `<< /Length ${Buffer.byteLength(content)} >>\nstream\n${content}\nendstream`; const cid = addObj(stream); contentIds.push(cid); const pid = addObj(null); diff --git a/src/backend/content_favorites.js b/src/backend/media-favorites.js similarity index 70% rename from src/backend/content_favorites.js rename to src/backend/media-favorites.js index a3d86b8..78a8923 100644 --- a/src/backend/content_favorites.js +++ b/src/backend/media-favorites.js @@ -1,40 +1,22 @@ const fs = require("fs"); const path = require("path"); -const TEMPLATE = path.join(__dirname, "../configs/content_favorites.json"); - -const storePath = () => { - try { - return require("../server/ssb_config").statePath("content_favorites.json") || TEMPLATE; - } catch (_) { return TEMPLATE; } -}; +const FILE = path.join(__dirname, "../configs/media-favorites.json"); const DEFAULT = { audios: [], - blogs: [], bookmarks: [], calendars: [], chats: [], documents: [], - events: [], - forum: [], - housing: [], images: [], - jobs: [], - logs: [], maps: [], pads: [], - polls: [], - projects: [], - reports: [], shops: [], market: [], shopProducts: [], - tasks: [], torrents: [], - transfers: [], - videos: [], - votes: [] + videos: [] }; const safeArr = (v) => (Array.isArray(v) ? v : []); @@ -57,32 +39,32 @@ const normalize = (raw) => { const ensureFile = async () => { try { - await fs.promises.access(storePath()); + await fs.promises.access(FILE); } catch (e) { - const dir = path.dirname(storePath()); + const dir = path.dirname(FILE); await fs.promises.mkdir(dir, { recursive: true }); - await fs.promises.writeFile(storePath(), JSON.stringify(DEFAULT, null, 2), "utf8"); + await fs.promises.writeFile(FILE, JSON.stringify(DEFAULT, null, 2), "utf8"); } }; const readAll = async () => { await ensureFile(); try { - const txt = await fs.promises.readFile(storePath(), "utf8"); + const txt = await fs.promises.readFile(FILE, "utf8"); return normalize(JSON.parse(txt || "{}")); } catch (e) { const fixed = normalize(DEFAULT); - await fs.promises.writeFile(storePath(), JSON.stringify(fixed, null, 2), "utf8"); + await fs.promises.writeFile(FILE, JSON.stringify(fixed, null, 2), "utf8"); return fixed; } }; const writeAll = async (data) => { - const dir = path.dirname(storePath()); - const tmp = path.join(dir, `.content_favorites.${process.pid}.${Date.now()}.tmp`); + const dir = path.dirname(FILE); + const tmp = path.join(dir, `.media-favorites.${process.pid}.${Date.now()}.tmp`); const txt = JSON.stringify(normalize(data), null, 2); await fs.promises.writeFile(tmp, txt, "utf8"); - await fs.promises.rename(tmp, storePath()); + await fs.promises.rename(tmp, FILE); }; const assertKind = (kind) => { @@ -106,15 +88,6 @@ exports.getFavoriteSet = async (kind) => { return new Set(safeArr(data[k]).map(String)); }; -exports.getFavoriteIndex = async () => { - const data = await readAll(); - const index = new Map(); - for (const kind of Object.keys(DEFAULT)) { - for (const id of safeArr(data[kind])) index.set(String(id), kind); - } - return index; -}; - exports.addFavorite = async (kind, id) => withLock(async () => { const k = assertKind(kind); @@ -150,4 +123,4 @@ exports.removeFromFavorites = async (...args) => { const id = idFromArgs(args); return exports.removeFavorite(kind, id); }; -exports.storePath = storePath; + diff --git a/src/backend/pdfDocument.js b/src/backend/pdfDocument.js deleted file mode 100644 index 4e3dc3e..0000000 --- a/src/backend/pdfDocument.js +++ /dev/null @@ -1,199 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const LOGO_PATH = path.join(__dirname, '..', 'client', 'assets', 'images', 'snh-oasis.jpg'); - -const WIN_ANSI_HIGH = { - '\u20AC': '\x80', '\u201A': '\x82', '\u0192': '\x83', '\u201E': '\x84', - '\u2026': '\x85', '\u2020': '\x86', '\u2021': '\x87', '\u02C6': '\x88', - '\u2030': '\x89', '\u0160': '\x8A', '\u2039': '\x8B', '\u0152': '\x8C', - '\u017D': '\x8E', '\u2018': '\x91', '\u2019': '\x92', '\u201C': '\x93', - '\u201D': '\x94', '\u2022': '\x95', '\u2013': '\x96', '\u2014': '\x97', - '\u02DC': '\x98', '\u2122': '\x99', '\u0161': '\x9A', '\u203A': '\x9B', - '\u0153': '\x9C', '\u017E': '\x9E', '\u0178': '\x9F' -}; - -const escapePdf = s => String(s == null ? '' : s) - .replace(/[\r\n\t]+/g, ' ') - .replace(/[\u0152\u0153\u0160\u0161\u0178\u017D\u017E\u0192\u02C6\u02DC\u2013\u2014\u2018\u2019\u201A\u201C\u201D\u201E\u2020\u2021\u2022\u2026\u2030\u2039\u203A\u20AC\u2122]/g, - (c) => WIN_ANSI_HIGH[c] || '?') - .replace(/[^\x20-\xFF\x80-\x9F]/g, '?') - .replace(/\\/g, '\\\\') - .replace(/\(/g, '\\(') - .replace(/\)/g, '\\)'); - -const wrap = (txt, max = 82) => { - const out = []; - for (const raw of String(txt == null ? '' : txt).split('\n')) { - let line = raw; - while (line.length > max) { - let cut = line.lastIndexOf(' ', max); - if (cut <= 0) cut = max; - out.push(line.slice(0, cut)); - line = line.slice(cut).replace(/^\s+/, ''); - } - out.push(line); - } - return out; -}; - -const readJpegDims = (buf) => { - let i = 2; - while (i < buf.length) { - if (buf[i] !== 0xFF) return null; - const marker = buf[i + 1]; - if (marker === 0xD8 || marker === 0xD9) { i += 2; continue; } - const len = buf.readUInt16BE(i + 2); - if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) { - const h = buf.readUInt16BE(i + 5); - const w = buf.readUInt16BE(i + 7); - const c = buf[i + 9]; - return { w, h, c }; - } - i += 2 + len; - } - return null; -}; - -const flattenSections = (sections) => { - const lines = []; - for (const s of Array.isArray(sections) ? sections : []) { - if (!s) continue; - if (s.kind === 'kv') { - const txt = `${s.label}: ${s.value == null ? '' : s.value}`; - for (const w of wrap(txt, 82)) lines.push({ kind: 'kv', text: w }); - } else if (s.kind === 'text') { - for (const w of wrap(s.text, 82)) lines.push({ kind: 'kv', text: w }); - } else { - lines.push({ kind: s.kind, text: s.text }); - } - } - return lines; -}; - -function buildDocumentPdf({ title, issuedToLabel, issuedTo, sections } = {}) { - const pageW = 612; - const pageH = 792; - const marginX = 50; - const headerH = 90; - const footerH = 40; - const bodyTop = pageH - headerH - 24; - const bodyBottom = footerH + 10; - const lineH = 14; - - let logoBuf = null; - let logoDims = null; - try { - logoBuf = fs.readFileSync(LOGO_PATH); - logoDims = readJpegDims(logoBuf); - } catch (_) {} - - const lines = flattenSections(sections); - - const maxBodyLines = Math.floor((bodyTop - bodyBottom) / lineH); - const pages = []; - for (let i = 0; i < lines.length; i += maxBodyLines) pages.push(lines.slice(i, i + maxBodyLines)); - if (!pages.length) pages.push([]); - - const objects = []; - const addObj = body => { objects.push(body); return objects.length; }; - - const catalogId = addObj(null); - const pagesId = addObj(null); - const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>'); - const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold /Encoding /WinAnsiEncoding >>'); - let logoXObjId = null; - if (logoBuf && logoDims) { - const cs = logoDims.c === 1 ? '/DeviceGray' : '/DeviceRGB'; - const dict = `<< /Type /XObject /Subtype /Image /Width ${logoDims.w} /Height ${logoDims.h} /ColorSpace ${cs} /BitsPerComponent 8 /Filter /DCTDecode /Length ${logoBuf.length} >>`; - logoXObjId = addObj({ dict, stream: logoBuf }); - } - - const exportDate = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; - const footerLeft = `Generated: ${exportDate}`; - - const pageIds = []; - const contentIds = []; - - pages.forEach((pg, pgIdx) => { - const parts = []; - if (logoXObjId) { - const logoH = 60; - const logoW = Math.round((logoDims.w / logoDims.h) * logoH); - parts.push(`q\n${logoW} 0 0 ${logoH} ${marginX} ${pageH - headerH + 15} cm\n/Logo Do\nQ`); - } - const titleX = (logoXObjId ? marginX + 80 : marginX); - const titleY = pageH - 45; - parts.push(`BT\n/F2 16 Tf\n${titleX} ${titleY} Td\n(${escapePdf(title || 'OASIS')}) Tj\nET`); - if (issuedTo) { - const prefix = `${issuedToLabel || 'Issued to'}: `; - const prefixW = prefix.length * 5.4; - parts.push(`BT\n/F1 9 Tf\n${titleX} ${titleY - 16} Td\n(${escapePdf(prefix)}) Tj\nET`); - parts.push(`BT\n/F2 9 Tf\n${titleX + prefixW} ${titleY - 16} Td\n(${escapePdf(String(issuedTo))}) Tj\nET`); - } - - parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${pageH - headerH} m\n${pageW - marginX} ${pageH - headerH} l\nS\nQ`); - - let y = bodyTop; - for (const ln of pg) { - if (ln.kind === 'title') { - parts.push(`BT\n/F2 14 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); - } else if (ln.kind === 'subtitle') { - parts.push(`BT\n/F1 11 Tf\n0.2 0.2 0.2 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); - } else if (ln.kind === 'section') { - parts.push(`BT\n/F2 11 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); - parts.push(`q\n0 0 0 RG\n0.5 w\n${marginX} ${y - 3} m\n${pageW - marginX} ${y - 3} l\nS\nQ`); - } else if (ln.kind === 'kv') { - parts.push(`BT\n/F1 10 Tf\n0 0 0 rg\n${marginX} ${y} Td\n(${escapePdf(ln.text)}) Tj\nET`); - } - y -= lineH; - } - - parts.push(`q\n0.6 0.6 0.6 RG\n0.5 w\n${marginX} ${footerH + 5} m\n${pageW - marginX} ${footerH + 5} l\nS\nQ`); - parts.push(`BT\n/F1 8 Tf\n${marginX} ${footerH - 10} Td\n(${escapePdf(footerLeft)}) Tj\nET`); - const pageLabel = `Page ${pgIdx + 1} of ${pages.length}`; - const pageLabelW = pageLabel.length * 4.8; - parts.push(`BT\n/F1 8 Tf\n${pageW - marginX - pageLabelW} ${footerH - 10} Td\n(${escapePdf(pageLabel)}) Tj\nET`); - - const content = parts.join('\n'); - const stream = `<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`; - const cid = addObj(stream); - contentIds.push(cid); - const pid = addObj(null); - pageIds.push(pid); - }); - - for (let i = 0; i < pageIds.length; i++) { - const resources = logoXObjId - ? `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> /XObject << /Logo ${logoXObjId} 0 R >> >>` - : `<< /Font << /F1 ${fontId} 0 R /F2 ${fontBoldId} 0 R >> >>`; - objects[pageIds[i] - 1] = `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${pageW} ${pageH}] /Contents ${contentIds[i]} 0 R /Resources ${resources} >>`; - } - objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`; - objects[pagesId - 1] = `<< /Type /Pages /Kids [${pageIds.map(id => `${id} 0 R`).join(' ')}] /Count ${pageIds.length} >>`; - - const chunks = []; - const offsets = [0]; - let byteLen = 0; - const push = (buf) => { chunks.push(buf); byteLen += buf.length; }; - push(Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n', 'binary')); - for (let i = 0; i < objects.length; i++) { - offsets.push(byteLen); - const obj = objects[i]; - if (obj && typeof obj === 'object' && obj.dict && obj.stream) { - push(Buffer.from(`${i + 1} 0 obj\n${obj.dict}\nstream\n`, 'binary')); - push(obj.stream); - push(Buffer.from('\nendstream\nendobj\n', 'binary')); - } else { - push(Buffer.from(`${i + 1} 0 obj\n${obj}\nendobj\n`, 'binary')); - } - } - const xrefStart = byteLen; - let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; - for (let i = 1; i <= objects.length; i++) xref += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`; - xref += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\nstartxref\n${xrefStart}\n%%EOF`; - push(Buffer.from(xref, 'binary')); - return Buffer.concat(chunks); -} - -module.exports = { buildDocumentPdf, escapePdf, wrap }; diff --git a/src/backend/pm_policy.js b/src/backend/pm_policy.js deleted file mode 100644 index 6b69b8a..0000000 --- a/src/backend/pm_policy.js +++ /dev/null @@ -1,50 +0,0 @@ -const fs = require('fs'); - -const ANNOUNCE_SEEN_FILE = 'oasis-political-seen'; - -const isMutual = (relationship) => !!(relationship && relationship.following && relationship.followsMe); - -const isRecipientAllowed = ({ pmVisibility, viewerId, recipientId, relationship } = {}) => { - if (recipientId && viewerId && recipientId === viewerId) return true; - if (pmVisibility !== 'mutuals') return true; - return isMutual(relationship); -}; - -const announceSeenPath = () => { - try { return require('../server/ssb_config').statePath(ANNOUNCE_SEEN_FILE); } catch (_) { return null; } -}; - -const readAnnounceSeen = () => { - const file = announceSeenPath(); - if (!file) return {}; - try { - const parsed = JSON.parse(fs.readFileSync(file, 'utf8')); - return parsed && typeof parsed === 'object' ? parsed : {}; - } catch (_) { return {}; } -}; - -const writeAnnounceSeen = (state) => { - const file = announceSeenPath(); - if (!file) return false; - try { fs.writeFileSync(file, JSON.stringify(state || {})); return true; } catch (_) { return false; } -}; - -const everAnnounced = (announced, subject) => { - for (const entry of (announced || [])) { - if (String(entry).startsWith(`${subject}|`)) return true; - } - return false; -}; - -const decideAnnouncement = ({ subject, ref, announced = new Set(), seen = {} } = {}) => { - if (!subject || !ref) return { send: false, remember: false }; - const key = String(ref); - if (announced.has(`${subject}|${key}`)) return { send: false, remember: false }; - if (seen[subject] === key) return { send: false, remember: false }; - if (!everAnnounced(announced, subject) && seen[subject] === undefined) { - return { send: false, remember: true }; - } - return { send: true, remember: true }; -}; - -module.exports = { isRecipientAllowed, isMutual, announceSeenPath, readAnnounceSeen, writeAnnounceSeen, decideAnnouncement }; diff --git a/src/backend/smartContractPdf.js b/src/backend/smartContractPdf.js index e396747..66b8baf 100644 --- a/src/backend/smartContractPdf.js +++ b/src/backend/smartContractPdf.js @@ -3,7 +3,7 @@ const path = require('path'); const LOGO_PATH = path.join(__dirname, '..', 'client', 'assets', 'images', 'snh-oasis.jpg'); -const { escapePdf } = require('./pdfDocument'); +const escapePdf = s => String(s || '').replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); const wrap = (txt, max = 82) => { const out = []; @@ -131,8 +131,8 @@ function buildSmartContractPdf({ transfer, block, viewerId }) { const catalogId = addObj(null); const pagesId = addObj(null); - const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>'); - const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold /Encoding /WinAnsiEncoding >>'); + const fontId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>'); + const fontBoldId = addObj('<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold >>'); let logoXObjId = null; if (logoBuf && logoDims) { const cs = logoDims.c === 1 ? '/DeviceGray' : '/DeviceRGB'; @@ -185,7 +185,7 @@ function buildSmartContractPdf({ transfer, block, viewerId }) { parts.push(`BT\n/F1 8 Tf\n${pageW - marginX - pageLabelW} ${footerH - 10} Td\n(${escapePdf(pageLabel)}) Tj\nET`); const content = parts.join('\n'); - const stream = `<< /Length ${Buffer.byteLength(content, 'latin1')} >>\nstream\n${content}\nendstream`; + const stream = `<< /Length ${Buffer.byteLength(content)} >>\nstream\n${content}\nendstream`; const cid = addObj(stream); contentIds.push(cid); const pid = addObj(null); diff --git a/src/backend/turnCredentials.js b/src/backend/turnCredentials.js deleted file mode 100644 index d1b6ea1..0000000 --- a/src/backend/turnCredentials.js +++ /dev/null @@ -1,50 +0,0 @@ -"use strict"; -// Credenciales efimeras para un TURN propio (coturn con use-auth-secret). -// -// Es el mecanismo REST de coturn: el servidor no guarda usuarios, valida que -// credential == base64(HMAC-SHA1(static-auth-secret, username)) -// y que el timestamp del username no haya caducado. Asi las credenciales se -// generan sin hablar con coturn y caducan solas. -// -// Portado del turn.js de karvan-web, cambiando sessionId por el feed id: lo que -// identifica a quien llama en Oasis es su feed, no una sesion anonima. - -const crypto = require("crypto"); - -const DEFAULT_TTL = 3600; - -// username = ":" -function mintTurnCredentials({ urls, secret, ttlSec = DEFAULT_TTL }, who, now = Date.now()) { - if (!secret || !Array.isArray(urls) || !urls.length) return null; - const expiry = Math.floor(now / 1000) + ttlSec; - const username = expiry + ":" + String(who || "anon").slice(0, 64); - const credential = crypto.createHmac("sha1", secret).update(username).digest("base64"); - return { urls, username, credential, ttl: ttlSec, expiry }; -} - -// Lee la configuracion de rtc y devuelve lo que el navegador espera en iceServers. -// Formas admitidas en oasis-config.json: -// "rtc": { "stun": ["stun:mi.pub:3478"] } -// "rtc": { "turn": { "urls": [...], "secret": "...", "ttlSec": 3600 }, "relayOnly": true } -// "rtc": { "turn": { "urls": [...], "username": "...", "credential": "..." } } (estatico) -function buildIceServers(rtc, who, now = Date.now()) { - const out = []; - if (!rtc || typeof rtc !== "object") return { iceServers: out, relayOnly: false }; - - if (Array.isArray(rtc.stun)) { - for (const u of rtc.stun) if (typeof u === "string" && u) out.push({ urls: u }); - } - - const t = rtc.turn; - if (t && Array.isArray(t.urls) && t.urls.length) { - if (t.secret) { - const c = mintTurnCredentials(t, who, now); - if (c) out.push({ urls: c.urls, username: c.username, credential: c.credential }); - } else if (t.username && t.credential) { - out.push({ urls: t.urls, username: t.username, credential: t.credential }); - } - } - return { iceServers: out, relayOnly: !!rtc.relayOnly && out.some(s => s.credential) }; -} - -module.exports = { mintTurnCredentials, buildIceServers }; diff --git a/src/client/assets/images/ux-ainav.png b/src/client/assets/images/ux-ainav.png deleted file mode 100644 index 022f127..0000000 Binary files a/src/client/assets/images/ux-ainav.png and /dev/null differ diff --git a/src/client/assets/images/ux-blocks.png b/src/client/assets/images/ux-blocks.png deleted file mode 100644 index cfbae8c..0000000 Binary files a/src/client/assets/images/ux-blocks.png and /dev/null differ diff --git a/src/client/assets/images/ux-chats.png b/src/client/assets/images/ux-chats.png deleted file mode 100644 index a7ccde5..0000000 Binary files a/src/client/assets/images/ux-chats.png and /dev/null differ diff --git a/src/client/assets/styles/karvan.css b/src/client/assets/styles/karvan.css deleted file mode 100644 index 2f33974..0000000 --- a/src/client/assets/styles/karvan.css +++ /dev/null @@ -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; } diff --git a/src/client/assets/styles/mobile.css b/src/client/assets/styles/mobile.css index 81cc969..18c1720 100644 --- a/src/client/assets/styles/mobile.css +++ b/src/client/assets/styles/mobile.css @@ -89,6 +89,7 @@ code { } .search-input, +.feed-search-input, .activity-search-input { width: 100% !important; max-width: 100% !important; @@ -509,28 +510,28 @@ h3 { font-size: 1em !important; } } .forum-comment { - margin-left: 0; - padding-left: 8px; + margin-left: 0 !important; + padding-left: 8px !important; } .comment-body-row { - flex-direction: column; + flex-direction: column !important; } .comment-vote-col, .comment-text-col { - width: 100%; + width: 100% !important; } .forum-score-box, .forum-score-form { - flex-direction: row; - justify-content: center; + flex-direction: row !important; + justify-content: center !important; } .new-message-form textarea, .comment-textarea { - width: 100%; + width: 100% !important; } [style*="grid-template-columns: repeat(6"] { @@ -913,32 +914,3 @@ a.create-button { .peers-technical-block .block-info-table td[data-label=""]::before { content: none !important; } .peers-list .block-info-table td .peer-key, .peers-technical-block .block-info-table td .peer-key { text-align: right !important; } - -.relationship-status .relationship-actions { flex-direction: column !important; align-items: stretch !important; gap: 8px !important; } -.relationship-status .relationship-actions form, -.relationship-status .relationship-actions button { width: 100% !important; } - -.profile-sensors-box { align-items: center !important; text-align: center !important; } -.profile-sensors-box > * { margin-left: auto !important; margin-right: auto !important; justify-content: center !important; } - -.tribe-feed .refeed-column, -.tribe-feed-full .refeed-column { align-self: center !important; align-items: center !important; justify-content: center !important; } -.tribe-feed .card-footer, -.tribe-feed-full .card-footer { display: flex !important; flex-direction: column !important; align-items: center !important; text-align: center !important; gap: 2px !important; } - -.invite-qr-card { display: flex !important; justify-content: center !important; border: none !important; background: transparent !important; padding: 0 !important; box-shadow: none !important; margin: 0 auto !important; } -.invite-qr-img { display: block; margin: 0 auto; } - -.stats-kpi { display: flex !important; flex-direction: column !important; align-items: center !important; text-align: center !important; gap: 2px !important; } -.stats-kpi-label, -.stats-kpi-value { width: 100% !important; text-align: center !important; } - -.main-column audio, -.main-column video { width: 100% !important; max-width: 100% !important; } - -.main-column section:has(.chat-messages-list) { border: none !important; background: transparent !important; padding: 0 !important; box-shadow: none !important; } -.tribe-details:has(.chat-messages-list) { display: block !important; border: none !important; background: transparent !important; padding: 6px !important; margin: 0 !important; box-shadow: none !important; } -.chat-full-width { border: none !important; background: transparent !important; padding: 0 !important; } -.chat-messages-list { padding: 0 !important; } -.chat-message { max-width: 92% !important; } -.chat-message.chat-poll { max-width: 97% !important; } diff --git a/src/client/assets/styles/style.css b/src/client/assets/styles/style.css index 1382cff..eca02f8 100644 --- a/src/client/assets/styles/style.css +++ b/src/client/assets/styles/style.css @@ -80,6 +80,12 @@ a:hover { .update-banner-icon { font-size: 1.15em; + animation: update-spin 2.5s linear infinite; +} + +@keyframes update-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } .update-banner-text { @@ -982,7 +988,7 @@ article img { .feed-container .post-image{max-width:100%;width:auto;max-height:480px;height:auto} .feed-container .post-video,.feed-container .video-container video{width:100%;max-width:720px;max-height:480px;height:auto} -.feed-container .post-audio,.feed-container .audio-container audio{width:100%} +.feed-container .post-audio,.feed-container .audio-container audio{width:100%;max-width:640px} .fediverse-network{border-radius:8px;padding:18px 22px;margin-top:10px;display:flex;flex-direction:column;gap:16px} .fediverse-network h3{margin:0} @@ -1129,84 +1135,14 @@ button.create-button:hover { } .filter-box { - display: flex; - flex-wrap: wrap; - gap: 10px; - align-items: center; padding: 16px 18px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(0, 0, 0, 0.18); } -.filter-box input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), -.filter-box select, -.filter-box button { - height: 42px !important; - box-sizing: border-box !important; - padding-top: 0 !important; - padding-bottom: 0 !important; - margin: 0 !important; - vertical-align: middle; -} - -div:has(> .filter-box) { - border: none !important; - background: transparent !important; - padding: 0 !important; - box-shadow: none !important; -} - -.filter-box div { - border: none !important; - background: transparent !important; - box-shadow: none !important; - padding: 0 !important; -} - -.filter-box__controls .filter-box__input, -.filter-box__controls input[type="text"], -.filter-box__controls input[type="number"], -.filter-box__controls input[type="date"], -.filter-box__controls input[type="datetime-local"], -.filter-box__controls select { - width: auto; - flex: 0 1 220px; - min-width: 140px; -} - -.filter-box .transfer-range, -.filter-box__controls .transfer-range { - display: flex; - gap: 8px; - flex: 1 1 100% !important; -} - -.filter-box select { - flex: 1 1 100% !important; - max-width: 340px; -} - -.filter-box .filter-box__button, -.filter-box button { - flex: 0 0 auto; -} - -.filter-box input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -.filter-box select { - box-sizing: border-box; - height: 42px; - padding: 10px 12px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.12); - background: rgba(255, 255, 255, 0.12); - color: #ffd700; - outline: none; -} - .filter-box__input { width: 100%; - flex: 1 1 100%; box-sizing: border-box; height: 42px; padding: 10px 12px; @@ -1225,16 +1161,15 @@ div:has(> .filter-box) { display: flex; gap: 12px; align-items: center; - margin-top: 0; - flex: 1 1 100%; + margin-top: 14px; flex-wrap: wrap; } .filter-box__select { box-sizing: border-box; - height: 42px; + height: 36px; padding: 6px 10px; - border-radius: 4px; + border-radius: 6px; border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.12); color: inherit; @@ -1242,26 +1177,14 @@ div:has(> .filter-box) { } .filter-box__button { - height: 42px; - padding: 0 18px; - border-radius: 4px; + height: 36px; + padding: 0 14px; + border-radius: 6px; border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.16); color: #ffd700; } -.filter-box__controls input:not([type="hidden"]), -.filter-box__controls select, -.filter-box__controls button { - height: 42px !important; - box-sizing: border-box !important; - padding-top: 0 !important; - padding-bottom: 0 !important; - margin: 0 !important; - align-self: center !important; - line-height: normal; -} - .bookmark-topbar { display: flex; gap: 8px; @@ -1595,57 +1518,18 @@ div:has(> .filter-box) { cursor: pointer; font: inherit; padding: 10px 20px; - border: 1px solid rgba(128, 128, 128, .5) !important; + border: 1px solid #ff3300; border-radius: 5px; - background: rgba(128, 128, 128, .12) !important; + background-color: #ff6600; + color: #fff; transition: background-color .2s, border-color .2s; list-style: none; } -.opinions-summary:hover { background: rgba(128, 128, 128, .25) !important; } +.opinions-summary:hover { background-color: #ff9900; border-color: #e68a00; color: #000; } .opinions-summary::-webkit-details-marker { display: none; } .opinions-summary::after { content: " \25BE"; opacity: 0.85; } details[open] > .opinions-summary::after { content: " \25B4"; } -.comments-collapse { margin: 8px 0; } -.comments-summary { - display: inline-block !important; - width: auto !important; - max-width: max-content; - box-sizing: border-box; - cursor: pointer; - font: inherit; - padding: 10px 20px; - border: 1px solid rgba(128, 128, 128, .5); - border-radius: 5px; - background: rgba(128, 128, 128, .12) !important; - list-style: none; -} -.comments-summary:hover { background: rgba(128, 128, 128, .25) !important; } -.comments-summary::-webkit-details-marker { display: none; } -.comments-summary::after { content: " \25BE"; opacity: 0.85; } -details[open] > .comments-summary::after { content: " \25B4"; } -.comments-summary-icon { margin-right: 6px; } - -.comments-link { - text-decoration: none; - text-align: center; -} - -.comments-link::after { content: ""; } - -.engage-on { - border-color: #e68a00 !important; - background: #ff9900 !important; - color: #000 !important; -} - -.engage-on:hover { - background: #ffb733 !important; - border-color: #ff9900 !important; - color: #000 !important; -} -.opinions-summary-icon { margin-right: 6px; } - .message-banner { margin: 12px 0; padding: 12px 16px; @@ -1655,6 +1539,27 @@ details[open] > .comments-summary::after { content: " \25B4"; } } .message-banner p { margin: 0; } +.feed-search-form { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.feed-search-input { + flex: 1 1 260px; + min-width: 220px; + height: 40px; + box-sizing: border-box; +} + +.feed-search-btn { + height: 40px; + display: inline-flex; + align-items: center; + box-sizing: border-box; +} + .type-label { font-weight: bold; margin-top: 0.5em; @@ -1913,19 +1818,13 @@ background:#ffcc00; color:#000; } .buy-btn { - display: block; - width: 100%; - padding: 14px 20px; - font-size: 1.2em; - font-weight: bold; - background: #ffa300; + display: inline-block; + background: #ffcc00; color: #000; text-decoration: none; text-align: center; - border-radius: 8px; } .buy-btn:hover { background: #ffcc00; color: #000; } -form:has(> .buy-btn) { width: 100%; } .gallery { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); gap:10px; padding:10px; @@ -2023,9 +1922,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; border-radius: 6px; } -.zoom-link{display:inline-block;border:none !important;background:transparent !important;padding:0 !important} -.zoom-link img{cursor:zoom-in} - .lightbox-close { position: absolute; top: 15px; @@ -2206,38 +2102,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; margin-top: 10px; } -.doc-export-actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - justify-content: center; - margin-top: 12px; - border: none !important; - background: transparent !important; -} - -.doc-export-actions form { - border: none !important; - background: transparent !important; - margin: 0; -} - -.vote-outcome { - font-weight: bold; -} - -.vote-outcome-yes { - color: #4caf50 !important; -} - -.vote-outcome-no { - color: #e53935 !important; -} - -.vote-outcome-pending { - color: #ffcc00 !important; -} - .event-actions { display: flex; gap: 10px; @@ -2321,24 +2185,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; margin-top: 12px; } -.cv-threshold-row { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.cv-card-fields { - display: flex; - flex-direction: column; - gap: 4px; - margin: 6px 0; -} - -.cv-threshold-row label { - margin: 0; -} - .card-salary { display: inline-block; font-weight: 700; @@ -2346,11 +2192,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; letter-spacing: 0.02em; } -.transfer-amount-centered { - display: block; - text-align: center; -} - .mode-buttons { display: flex; gap: 10px; @@ -2652,8 +2493,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; border: 0 !important; padding: 0 !important; width: 100%; - align-items: center; - justify-content: center; } .inhabitant-left .cv-actions form, @@ -2661,42 +2500,11 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; background: transparent !important; border: 0 !important; padding: 0 !important; - width: auto; -} - -.inhabitant-user-actions { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; width: 100%; - margin-top: 10px; - padding: 10px !important; - border: 1px solid rgba(128, 128, 128, .25) !important; - border-radius: 8px; - background: transparent !important; } -.inhabitant-user-actions form { - background: transparent !important; - border: 0 !important; - padding: 0 !important; - margin: 0; -} - -.inhabitant-relationship { - background: transparent !important; - border: 0 !important; - padding: 0 !important; -} - -.inhabitant-relationship .relationship-status { - justify-content: center; - text-align: center; -} - -.inhabitant-relationship .relationship-actions { - justify-content: center; +.inhabitant-left .cv-actions button.btn { + width: 100%; } .inhabitant-left .cv-actions p { @@ -2734,7 +2542,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; - justify-content: center; } .status { @@ -2797,6 +2604,11 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; gap: 1.1em; background: none; } +.visit-forum-form { + display: flex; + justify-content: center; + margin-top: 10px; +} .forum-score-col { min-width: 78px; display: flex; @@ -3086,7 +2898,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .comment-textarea { resize: vertical; width: 100%; - box-sizing: border-box; padding: 8px; background: #1e1e1e; color: #ffd740; @@ -3186,11 +2997,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; font-weight: bold; } -.forum-negative-votes { - font-weight: bold; - margin-left: 12px; -} - .forum-messages { font-weight: 500; } @@ -3504,8 +3310,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; .pm-card.normal-pm { padding: 10px 12px; border-radius: 8px; margin-bottom: 10px; border: 1px solid #ffa300; background: transparent } .pm-btn { padding: 6px 16px; border-radius: 6px; font-size: 12px; font-weight: 700; letter-spacing: 0.5px; cursor: pointer; border: 1px solid #ffa300; background: transparent; color: #ffa300; transition: background 0.15s, color 0.15s } .pm-btn:hover { background: #ffa300; color: #000 } -a.pm-btn { text-decoration: none; display: inline-block; line-height: 1.2 } -a.pm-btn:hover { text-decoration: none } .pm-thread { margin-bottom: 12px } .pm-thread-details { margin-top: 0 } .pm-thread-details[open] .pm-thread-icon { display: inline-block; transform: rotate(90deg) } @@ -3529,10 +3333,7 @@ a.pm-btn:hover { text-decoration: none } .pm-actions { display: flex; gap: 10px; - align-items: center; - flex-wrap: wrap; } -.pm-actions .pm-btn { margin: 0 } .pm-crypter-row { border: none; background: transparent; @@ -3628,8 +3429,7 @@ a.pm-btn:hover { text-decoration: none } display: flex; align-items: center; gap: 10px; - flex-wrap: wrap; - max-width: 100%; + flex-wrap: nowrap; } .project-control-select, @@ -3640,10 +3440,7 @@ a.pm-btn:hover { text-decoration: none } .project-control-select, .project-control-input { - min-width: 0; - flex: 1 1 160px; - max-width: 100%; - box-sizing: border-box; + min-width: 220px; } .project-control-btn { @@ -3658,7 +3455,7 @@ a.pm-btn:hover { text-decoration: none } @media (max-width: 820px) { .project-control-select, .project-control-input { - flex-basis: 100%; + min-width: 180px; } .project-progress-input { @@ -3838,7 +3635,7 @@ a.pm-btn:hover { text-decoration: none } .melody-player-card{border:1px solid #2a2a2a;border-radius:8px;padding:16px;background:#111;margin:16px 0} .melody-meta{margin-bottom:12px;display:flex;flex-wrap:wrap;align-items:center;gap:6px} .melody-meta-sep{color:#555} -.melody-audio{width:100%} +.melody-audio{width:100%;max-width:520px} .melody-section-title{font-size:16px;color:#FFA500;margin:16px 0 8px 0} .melody-help{color:#888;font-size:13px;margin:0 0 12px 0} .melody-notes-grid{display:flex;flex-wrap:wrap;gap:6px} @@ -3884,8 +3681,7 @@ a.pm-btn:hover { text-decoration: none } .melody-upload-submit{width:auto;display:inline-block;padding:6px 18px;align-self:flex-start} .melody-upload-label{color:#FFD700;font-size:13px;font-weight:600;display:inline-block;margin-top:6px} .melody-bcs-list{display:flex;flex-direction:column;gap:14px;margin-top:14px;background:transparent !important;border:0 !important;padding:0 !important} -.audio-container{display:flex;flex-direction:column;gap:8px;align-items:stretch;width:100%;background:transparent !important;border:0 !important;padding:0 !important} -.audio-container audio{width:100%} +.audio-container{display:flex;gap:8px;align-items:center;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important} .audio-transcode-form{display:inline-block;background:transparent !important;border:0 !important;padding:0 !important;margin:0} .melody-bcs-card{background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:12px 16px} .melody-bcs-head{display:flex;flex-direction:column;gap:4px;background:transparent !important;border:0 !important;padding:0 !important} @@ -3932,7 +3728,7 @@ a.pm-btn:hover { text-decoration: none } .shop-detail-media img{max-width:100%;border-radius:6px} .shop-detail-desc{margin:8px 0} .shop-share{margin:8px 0;display:flex;align-items:center;gap:8px} -.shop-share-input{flex:1;background:rgba(128,128,128,.12);border:1px solid rgba(255,163,0,.6);color:#ffa300;font-weight:bold;padding:6px 10px;border-radius:4px;font-family:monospace;font-size:13px;margin:0} +.shop-share-input{flex:1;background:#222;border:1px solid #555;color:#ccc;padding:6px 10px;border-radius:4px;font-family:monospace;font-size:13px} .shop-products-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;margin:12px 0} .shop-product-card{border:1px solid #333;border-radius:8px;overflow:hidden;display:flex;flex-direction:column} .shop-product-media{max-height:180px;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#111} @@ -3959,14 +3755,13 @@ a.pm-btn:hover { text-decoration: none } /* carbon footprint chart */ .carbon-section-title{font-size:18px;color:#555;margin:8px 0;font-weight:600} -#carbon{scroll-margin-top:16px} .carbon-chart{margin:12px 0;background:transparent !important;border:none !important} .carbon-bar-label{display:flex;justify-content:space-between;font-size:14px;margin:4px 0 2px;background:transparent !important;border:none !important} .carbon-bar-track{background:#3a3a3a !important;border-radius:6px;height:22px;overflow:hidden;border:none !important} .carbon-bar-fill{height:100%;border-radius:6px;min-width:2px;transition:width 0.4s ease;border:none !important} .carbon-bar-network{background:#2ecc71 !important} .carbon-bar-mine{background:#3498db !important} -.carbon-bar-max{background:#ffa300 !important;border:none !important} +.carbon-bar-max{background:#555 !important;border:none !important} .carbon-bar-note{font-size:13px;color:#ffd700;margin:6px 0 2px} .carbon-bar-formula{font-size:12px;color:#ffd700;margin:2px 0 10px} @@ -4197,16 +3992,6 @@ a.pm-btn:hover { text-decoration: none } .tribe-card-title { margin: 0; font-size: 1.15em; - overflow-wrap: anywhere; -} - -.jobs-grid .tribe-card-title, -.tribe-grid .tribe-card-title, -.trending-card .tribe-card-title { - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; } .tribe-card-footer { @@ -4273,26 +4058,7 @@ a.pm-btn:hover { text-decoration: none } .tribe-side-actions select, .tribe-side-actions input[type="number"], .tribe-side-actions input[type="text"] { height: 36px; box-sizing: border-box } -.tribe-side-actions button, -.tribe-side-actions > a, -.tribe-side-actions form > a { - height: 36px; - box-sizing: border-box; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.tribe-side-actions a > button { - height: auto; - padding: 0; - border: 0; - background: transparent; - color: inherit; - font: inherit; - letter-spacing: inherit; - text-transform: inherit; -} +.tribe-side-actions button { height: 36px; box-sizing: border-box; display: inline-flex; align-items: center } .tribe-side-subtribes { margin: 8px 0; @@ -4463,30 +4229,59 @@ a.pm-btn:hover { text-decoration: none } color: #ffa300; transition: background 0.15s, color 0.15s; } +.tribe-action-btn, +.tribe-side-actions button, +.tribe-side-actions a, +.map-detail-actions button, +.map-detail-actions a { + display: inline-block; + padding: 6px 16px; + border-radius: 6px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.5px; + line-height: 1.2; + text-transform: uppercase; + text-decoration: none; + text-align: center; + cursor: pointer; + border: 1px solid #ffa300; + background: transparent; + color: #ffa300; + transition: background 0.15s, color 0.15s; +} +.tribe-side-actions button:hover, +.tribe-side-actions a:hover, +.map-detail-actions button:hover, +.map-detail-actions a:hover { + background: #ffa300; + color: #000; +} +.tribe-side-actions .tribe-action-btn.danger-btn, +.tribe-side-actions .delete-btn, +.map-detail-actions .delete-btn { + border-color: #d9534f; + color: #d9534f; +} +.tribe-side-actions .tribe-action-btn.danger-btn:hover, +.tribe-side-actions .delete-btn:hover, +.map-detail-actions .delete-btn:hover { + background: #d9534f; + color: #000; +} .tribe-action-btn.danger-btn, .tribe-card-actions .tribe-action-btn.danger-btn, -.tribe-side-actions .tribe-action-btn.danger-btn, -.tribe-side-actions .delete-btn { +.tribe-side-actions .tribe-action-btn.danger-btn { border: 1px solid #d9534f !important; color: #d9534f !important; background: transparent !important; } .tribe-action-btn.danger-btn:hover, .tribe-card-actions .tribe-action-btn.danger-btn:hover, -.tribe-side-actions .tribe-action-btn.danger-btn:hover, -.tribe-side-actions .delete-btn:hover { +.tribe-side-actions .tribe-action-btn.danger-btn:hover { background: #d9534f !important; color: #000 !important; } -.tribe-side-actions .update-btn { - border: 1px solid #ffa300; - color: #ffa300; - background: transparent; -} -.tribe-side-actions .update-btn:hover { - background: #ffa300; - color: #000; -} .tribe-action-btn:hover { background: #ffa300; color: #000; @@ -4496,8 +4291,9 @@ a.pm-btn:hover { text-decoration: none } .calendar-interval-block { margin: 8px 0 16px; } .calendar-interval-label { display: block; margin-bottom: 6px; } -.calendar-interval-until { margin-top: 14px; } -.calendar-interval-select { margin: 0; } +.calendar-interval-until { margin-top: 10px; } +.calendar-interval-row { display: flex; gap: 16px; flex-wrap: wrap; } +.calendar-interval-option { display: flex; align-items: center; gap: 4px; cursor: pointer; white-space: nowrap; } .tribe-thumb-grid { display: grid; @@ -5094,6 +4890,14 @@ a.pm-btn:hover { text-decoration: none } margin-bottom: 16px; } +.map-card { + background-color: #222; + border: 1px solid #444; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; +} + .map-card-info { display: flex; gap: 12px; @@ -5144,6 +4948,14 @@ a.pm-btn:hover { text-decoration: none } word-break: break-all; } +.map-detail-info { + display: flex; + gap: 16px; + align-items: center; + flex-wrap: wrap; + margin-bottom: 12px; +} + .map-wrap { margin-right: 30px; position: relative; @@ -5204,6 +5016,10 @@ a.pm-btn:hover { text-decoration: none } font-size: 1.2rem; } +.map-create-layout { + width: 100%; +} + .map-form { margin-top: 12px; } @@ -5227,29 +5043,86 @@ a.pm-btn:hover { text-decoration: none } display: flex; border: 0px; gap: 12px; - align-items: center; -} - -.map-form-row .filter-btn { - height: 36px; - box-sizing: border-box; - display: inline-flex; - align-items: center; - justify-content: center; - white-space: nowrap; - text-decoration: none; } .map-form-field { flex: 1; } +.map-card { + background-color: #222; + border: 1px solid #444; + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 12px; + display: flex; + gap: 16px; + align-items: flex-start; +} + +.map-card-thumb-link { + flex-shrink: 0; +} + +.map-card-thumb { + width: 120px !important; + height: 120px !important; + max-width: none !important; + border-radius: 6px !important; + border: 1px solid #555 !important; + object-fit: cover; +} + +.map-card-body { + flex: 1; + min-width: 0; +} + +.map-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 8px; +} + +.map-card-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; + align-items: center; +} + .map-click-hint { color: #FFA500; font-size: 0.85rem; padding: 6px 0; } +.map-detail { + background-color: #222; + border: 1px solid #444; + border-radius: 8px; + padding: 16px; +} + +.map-detail-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 12px; +} + +.map-detail-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; + align-items: center; +} + .map-marker-form { margin-top: 20px; padding-top: 16px; @@ -5361,31 +5234,6 @@ a.pm-btn:hover { text-decoration: none } font-size: 0.9rem; } -.search-card { - min-width: 0; - overflow-wrap: anywhere; - word-break: break-word; -} - -.search-card .card-value, -.search-card .styled-text, -.search-card p { - min-width: 0; - overflow-wrap: anywhere; - word-break: break-word; -} - -.search-card img, -.search-card video { - max-width: 100%; - height: auto; -} - -.search-card audio { - width: 100%; - max-width: 100%; -} - .search-result-image { max-width: 100%; max-height: 300px; @@ -5420,6 +5268,12 @@ a.pm-btn:hover { text-decoration: none } margin: 16px 0; } +.maps-list { + display: flex; + flex-direction: column; + gap: 16px; +} + .maps-search { margin-bottom: 16px; } @@ -5481,6 +5335,12 @@ a.pm-btn:hover { text-decoration: none } .map-markers-list h3 { margin-bottom: 4px; } +.map-form-full { + width: 100%; +} +.map-form-full form { + width: 100%; +} .map-osm-embed { width: 100%; height: 450px; @@ -5494,6 +5354,10 @@ a.pm-btn:hover { text-decoration: none } overflow-y: hidden; border: 0px; } +.map-form-full, .map-form-full form { + box-sizing: border-box; + overflow: hidden; +} .map-popup { display: none; position: fixed; @@ -5610,37 +5474,14 @@ a.pm-btn:hover { text-decoration: none } .chat-participants-list li{padding:4px 0;font-size:0.85rem} .chat-message-form{background:#1a1a1a;border:1px solid #333;border-radius:4px;padding:10px} .chat-message-form textarea{width:100%;box-sizing:border-box;background:#111;color:#eee;border:1px solid #555;border-radius:4px;padding:8px;resize:vertical;font-family:inherit} -.main-content.chatsux-only .main-column{max-width:100%;width:100%} -.chat-workspace{display:flex;gap:14px;align-items:stretch;min-height:72vh} -.chat-topics{width:280px;flex-shrink:0;display:flex;flex-direction:column;gap:4px;background:#161616;border:1px solid #2a2a2a;border-radius:8px;padding:8px;max-height:82vh;overflow-y:auto} -.chat-topics-head{display:flex;justify-content:space-between;align-items:center;font-weight:bold;padding:4px 6px;border:none !important;background:transparent !important} -.chat-topics-new{padding:2px 10px;line-height:1.3} -.chat-topics-title-link{font-weight:bold;color:#ffa300;text-decoration:none} -.chat-topics-title-link:hover{text-decoration:underline} -.main-column .chat-workspace-section{background:transparent !important;border:none !important;padding:0 !important;box-shadow:none !important;margin-bottom:0} -.tribe-details.chat-workspace-details{background:transparent !important;border:none !important;padding:0 !important;box-shadow:none !important} -.chat-topic{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:8px;text-decoration:none;border:none;background:transparent} -.chat-topic:hover{background:#222} -.chat-topic-active{background:#2f2510} -.chat-topic-title{font-weight:bold;color:#ffa300;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.chat-topic-meta{font-size:0.75rem;color:#888} -.chat-workspace-main{flex:1;min-width:0} -.chat-workspace-empty{display:flex;align-items:center;justify-content:center;border:1px dashed #333;border-radius:8px} -@media (max-width: 900px){.chat-workspace{flex-direction:column}.chat-topics{width:100%;max-height:220px}} -.chat-messages-list{display:flex;flex-direction:column;gap:6px} -.chat-bubble-row{display:flex;justify-content:flex-start;border:none !important;background:transparent !important;padding:0 !important} -.chat-bubble-row-self{justify-content:flex-end} -.chat-message{max-width:85%;padding:7px 12px 5px;border-radius:14px 14px 14px 4px;background:#23242a;word-break:break-word;border:none} -.chat-message-author{background:#1f1800} -.chat-bubble-row-self .chat-message{border-radius:14px 14px 4px 14px;background:#2f2510} -.chat-bubble-sender{display:block;font-size:0.78rem;font-weight:bold;color:#7fb3ff;text-decoration:none;margin-bottom:2px;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.chat-bubble-sender:hover{text-decoration:underline} -.chat-bubble-sender-owner{color:#ffa300} -.chat-bubble-time{display:block;font-size:0.68rem;color:#888;text-align:right;margin-top:2px;line-height:1} -.chat-day-separator{display:flex;justify-content:center;margin:10px 0 4px;border:none !important;background:transparent !important;padding:0 !important} -.chat-day-chip{font-size:0.72rem;padding:2px 12px;border-radius:10px;background:#1a1a1a;color:#aaa;border:1px solid #333} -.chat-message.chat-poll{max-width:92%} -.chat-message-text{display:block;font-size:0.9rem;white-space:pre-wrap;word-break:break-word} +.chat-messages-list{display:flex;flex-direction:column;gap:4px} +.chat-message{padding:6px 10px;border-radius:4px;background:#1a1a1a;word-break:break-word;border:none} +.chat-message-author{border-left-color:#FFA500;background:#1f1800} +.chat-message-self{border-left-color:#888;background:#181818} +.chat-message-meta{font-size:0.8rem;color:#888;border:none} +.chat-message-date{color:#666} +.chat-message-sender a{color:#aaa} +.chat-message-text{display:block;margin-top:2px;font-size:0.9rem;white-space:pre-wrap;word-break:break-word} .calendar-note-card{position:relative;padding:10px 80px 10px 10px;min-height:56px} .calendar-note-delete{position:absolute;top:8px;right:8px;margin:0} .calendar-note-delete button{padding:4px 10px;font-size:0.8rem} @@ -5743,23 +5584,20 @@ a.pm-btn:hover { text-decoration: none } .torrent-table{width:100%;border-collapse:collapse;margin-top:12px} .torrent-table th,.torrent-table td{padding:8px 12px;text-align:left} -.torrent-table td:has(.filter-btn){width:1%;white-space:nowrap} -.torrent-table td .filter-btn{margin:0} .torrent-table th{background:#1a1a1a;color:#FFA500;font-weight:bold} .torrent-download{margin:12px 0} .torrent-download .filter-btn{border:none} .logs-table{width:100%;border-collapse:collapse;margin-top:12px} .logs-table th,.logs-table td{padding:8px 12px;text-align:left;vertical-align:middle;border-bottom:1px solid #222} -.logs-col-actions{vertical-align:middle;text-align:right;white-space:nowrap;background:transparent;width:1%} -.logs-row-actions{display:flex;gap:8px;justify-content:flex-end;align-items:center;border:0 !important;background:transparent !important;padding:0 !important} -.logs-col-actions form{margin:0;display:inline-flex} -.logs-col-actions .filter-btn{min-width:0;white-space:nowrap} +.logs-col-actions{vertical-align:middle;text-align:center;white-space:nowrap;background:transparent} +.logs-col-actions form{margin:0;display:inline-block} +.logs-col-actions .filter-btn{min-width:110px} .logs-table th{background:#1a1a1a;color:#FFA500;font-weight:bold} -.logs-col-date{white-space:nowrap;text-align:left;font-family:monospace;background:transparent;width:1%} +.logs-col-date{white-space:nowrap;text-align:center;font-family:monospace;background:transparent} .logs-date-day{color:#FFA500;font-weight:bold;font-size:0.9rem} .logs-date-time{color:#ddd;font-size:0.85rem} -.logs-col-log{width:100%;vertical-align:middle;background:transparent;padding:8px 12px;overflow-wrap:anywhere} +.logs-col-log{max-width:560px;vertical-align:middle;background:transparent;padding:8px 12px} .logs-col-log>*{background:transparent;border:0;padding:0;margin:0} .logs-entry-label{color:#FFA500;font-weight:bold;margin-bottom:4px !important;background:transparent;border:0} .logs-entry-meta{color:#888;font-size:0.85rem;margin-bottom:6px} @@ -5770,7 +5608,7 @@ a.pm-btn:hover { text-decoration: none } .logs-mode-form{margin:12px 0} .logs-mode-group{display:flex;gap:10px;align-items:center;flex-wrap:wrap} .logs-ai-disabled{color:#e74c3c;font-style:italic;margin:8px 0} -.logs-col-type{white-space:nowrap;text-align:left;font-family:monospace;background:transparent;width:1%} +.logs-col-type{white-space:nowrap;text-align:center;font-family:monospace;background:transparent} .logs-type-text{font-weight:bold;letter-spacing:0.5px;text-transform:uppercase;font-family:monospace;font-size:0.85rem} .logs-type-ai{color:#00b4ff} .logs-type-manual{color:#FFA500} @@ -5861,14 +5699,12 @@ a.pm-btn:hover { text-decoration: none } .larp-my-house{display:flex;align-items:center;gap:8px;margin:8px 0 16px 0;background:transparent !important;border:0 !important;padding:0 !important} .larp-my-house-label{color:#FFD700;font-size:12px;text-transform:uppercase;letter-spacing:1px} .larp-my-house-link{color:#FFA500;font-weight:700} -.larp-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin-top:12px;align-items:stretch} -.larp-card{display:flex;flex-direction:column} -.larp-card .larp-card-body{flex:1 1 auto;display:flex;flex-direction:column} +.larp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;margin-top:12px} .larp-card{background:#1A1A1A;border:1px solid #333;border-radius:8px;overflow:hidden;display:flex;flex-direction:column} .larp-card-mine{border:3px solid #FFA500} .larp-card-ruling{border:3px solid #ff6b6b} -.larp-card-image-link{display:block;width:100%} -.larp-card-image{width:100%;aspect-ratio:16/10;height:auto;object-fit:cover;object-position:center;display:block;background:#0a0a0a} +.larp-card-image-link{display:block} +.larp-card-image{width:100%;height:160px;object-fit:cover;display:block;background:#0a0a0a} .larp-card-body{padding:10px 12px;display:flex;flex-direction:column;gap:4px;background:transparent !important;border:0 !important} .larp-card-title-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;background:transparent !important;border:0 !important;padding:0 !important} .larp-card-title{color:#FFA500;font-weight:700;font-size:16px;letter-spacing:0.5px} @@ -5886,10 +5722,8 @@ a.pm-btn:hover { text-decoration: none } .larp-detail-body{flex:1 1 320px;display:flex;flex-direction:column;gap:10px;background:transparent !important;border:0 !important;padding:0 !important} .larp-info-table{width:100%;border-collapse:collapse} .larp-info-table td{padding:4px 8px;border-bottom:1px solid #2a2a2a;vertical-align:top} -.larp-detail-description{color:#FFDD44;line-height:1.6;font-size:15px;margin:0} -.larp-actions{margin-top:12px;background:transparent !important;border:0 !important;padding:0 !important;display:flex;flex-wrap:wrap;gap:8px;align-items:center} -.larp-actions form{margin:0;display:inline-flex;align-items:center} -.larp-actions .filter-btn,.larp-actions button{height:36px;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;white-space:nowrap;text-decoration:none} +.larp-detail-description{color:#ccc;line-height:1.5;margin:0} +.larp-actions{margin-top:10px;background:transparent !important;border:0 !important;padding:0 !important;display:flex;flex-wrap:wrap;gap:8px;align-items:center} .larp-members-block{margin-top:24px} .larp-members-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:6px;background:transparent !important;border:0 !important;padding:0 !important} .larp-member-row{padding:4px 8px;background:#1A1A1A;border:1px solid #333;border-radius:4px} @@ -5914,6 +5748,9 @@ a.pm-btn:hover { text-decoration: none } .larp-post-badge{display:inline-block;padding:2px 8px;border-radius:10px;background:#3a2300;color:#FFD700;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px} .larp-post-text{color:#ddd;line-height:1.5;margin:0;white-space:pre-wrap;word-break:break-word} .larp-rules{margin-top:14px;display:flex;flex-direction:column;gap:18px;background:transparent !important;border:0 !important;padding:0 !important} +.larp-rules-section{background:#1A1A1A;border:1px solid #2a2a2a;border-radius:6px;padding:12px 16px} +.larp-rules-section h3{color:#FFA500;margin:0 0 6px 0;font-size:15px} +.larp-rules-section p{color:#ccc;line-height:1.55;margin:6px 0} .larp-rules-list{display:flex;flex-direction:column;gap:6px;background:transparent !important;border:0 !important;padding:0 !important} .larp-rules-list strong{color:#FFD700} .lan-checkbox-label{display:inline-flex;align-items:center;gap:10px;color:#FFD700;font-size:14px;margin:10px 0;background:transparent !important;border:0 !important;padding:0 !important;cursor:pointer} @@ -5934,9 +5771,9 @@ a.pm-btn:hover { text-decoration: none } .larp-last-attempt-fail{color:#ff8a8a;font-weight:700} .larp-test-form-compact{margin:10px 0 0 0;gap:8px} .larp-test-form-compact .larp-test-question{padding:8px 12px} -.larp-test-form-compact .larp-test-q-text{font-size:15px;margin:0 0 8px 0} -.larp-test-form-compact .larp-test-option{font-size:14px;padding:4px 6px} -.larp-will-test-hint{color:#FFA500;opacity:.85;font-size:14px;line-height:1.5;margin:4px 0 12px 0} +.larp-test-form-compact .larp-test-q-text{font-size:13px;margin:0 0 6px 0} +.larp-test-form-compact .larp-test-option{font-size:12px;padding:2px 6px} +.larp-will-test-hint{color:#bbb;font-size:13px;line-height:1.4;margin:4px 0 10px 0} .larp-academia-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:12px;background:transparent !important;border:0 !important;padding:0 !important} .larp-invite-redeem-form,.larp-invite-create-form,.larp-card-invite-form{display:inline-flex;gap:6px;align-items:center;background:transparent !important;border:0 !important;padding:0 !important;margin:0} .larp-invite-redeem-form input[type=text],.larp-card-invite-form input[type=text]{padding:4px 8px;font-family:monospace;font-size:12px;min-width:280px;width:100%;max-width:360px;box-sizing:border-box} @@ -5947,15 +5784,11 @@ a.pm-btn:hover { text-decoration: none } .larp-invite-banner-code{color:#9aff9a;font-family:monospace;font-size:15px;letter-spacing:1px;margin:0;word-break:break-all} .larp-test-ranking{margin-top:14px;background:transparent !important;border:0 !important;padding:0 !important} .larp-test-ranking-title{color:#FFA500;font-size:15px;margin:0 0 8px 0} -.larp-academia-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;background:transparent !important;border:0 !important;padding:0 !important} -.larp-academia-thumb{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:6px;text-decoration:none;background:#1A1A1A;border:1px solid #333;border-radius:8px;padding:10px;transition:border-color 0.2s} +.larp-academia-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;background:transparent !important;border:0 !important;padding:0 !important} +.larp-academia-thumb{display:flex;flex-direction:column;align-items:center;gap:6px;text-decoration:none;background:#1A1A1A;border:1px solid #333;border-radius:8px;padding:10px;transition:border-color 0.2s} .larp-academia-thumb:hover{border-color:#FFA500} .larp-academia-thumb-mine{border:3px solid #FFA500} -.larp-academia-thumb-image{width:100%;max-width:72px;aspect-ratio:1;height:auto;object-fit:cover;border-radius:6px;border:1px solid #FFA500;display:block;margin:0 auto} -.larp-academia-thumb-table{margin-top:2px} -.larp-academia-thumb-table td{padding:2px 6px;font-size:12px} -.larp-academia-thumb-table .card-label{color:#FFA500;white-space:nowrap} -.larp-academia-thumb-table .card-value{color:#ccc} +.larp-academia-thumb-image{width:100%;max-width:128px;height:128px;object-fit:cover;border-radius:6px;border:1px solid #FFA500;display:block} .larp-academia-thumb-name{color:#FFA500;font-weight:700;font-size:14px;text-transform:uppercase;letter-spacing:0.5px;text-align:center} .larp-test-header{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-start;margin:14px 0;background:transparent !important;border:0 !important;padding:0 !important} .larp-test-image{width:180px;height:180px;object-fit:cover;border-radius:8px;border:2px solid #FFA500;display:block;flex:0 0 180px} @@ -5967,10 +5800,10 @@ a.pm-btn:hover { text-decoration: none } .larp-test-cooldown p{color:#FFD700;margin:0 0 8px 0;font-size:14px} .larp-test-form{margin:14px 0;background:transparent !important;border:0 !important;padding:0 !important;display:flex;flex-direction:column;gap:14px} .larp-test-question{background:#1A1A1A;border:1px solid #2a2a2a;border-radius:6px;padding:12px 16px} -.larp-test-q-text{color:#FFDD44;line-height:1.5;margin:0 0 10px 0;font-size:15px;font-weight:600} -.larp-test-q-text strong{color:#FFDD44} +.larp-test-q-text{color:#ddd;line-height:1.5;margin:0 0 10px 0;font-size:14px} +.larp-test-q-text strong{color:#FFA500} .larp-test-options{display:flex;flex-direction:column;gap:6px;background:transparent !important;border:0 !important;padding:0 !important;align-items:flex-start} -.larp-test-option{color:#FFA500;font-size:14px;display:inline-flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px;cursor:pointer;padding:4px 6px;border-radius:3px;width:auto;max-width:100%} +.larp-test-option{color:#ccc;font-size:13px;display:inline-flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px;cursor:pointer;padding:4px 6px;border-radius:3px;width:auto;max-width:100%} .larp-test-option:hover{background:#222} .larp-test-option input[type=radio]{accent-color:#FFA500;margin:0;flex:none;width:16px;height:16px} .larp-test-result{background:#1A1A1A;border:1px solid #FFA500;border-radius:8px;padding:18px 22px;margin:14px 0} @@ -6005,11 +5838,7 @@ a.pm-btn:hover { text-decoration: none } .graphos-node-link:hover .graphos-node-label{font-weight:600} .graphos-node-label{font-size:12px;fill:#ddd;text-anchor:middle;pointer-events:none} .graphos-node-label-me{font-weight:600;fill:#ffa500} -.graphos-legend{display:flex;gap:18px;flex-wrap:wrap;align-items:center;justify-content:center;font-size:14px;color:#ddd;margin:14px 0 8px} -.graphos-stats .stats-grid{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:18px} -.graphos-stats .stats-kpi{background:#1c1c1c;border:1px solid #444;border-radius:8px;padding:12px 14px;box-shadow:0 1px 3px rgba(0,0,0,0.35)} -.graphos-stats .stats-kpi-label{font-size:14px} -.graphos-stats .stats-kpi-inline .stats-kpi-value{font-size:20px} +.graphos-legend{display:flex;gap:18px;flex-wrap:wrap;align-items:center;font-size:13px;color:#ddd;margin:0 0 8px} .graphos-legend-item{display:flex;align-items:center;gap:6px} .graphos-legend-dot{display:inline-block;width:12px;height:12px;border-radius:50%;border:2px solid #1a1a1a} .graphos-legend-dot.graphos-node-circle-me{background:#ffa500;border-color:#cc7700} @@ -6198,15 +6027,14 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .search-filters-table td{padding:6px 10px;border:none;vertical-align:middle;background:transparent} .search-filters-table td.card-label{white-space:nowrap;font-weight:600;color:#ffa500} .search-filters-table input[type="datetime-local"], -.search-filters-table input.search-oasis-id, -.search-filters-table input[type="search"], -.search-filters-table select{min-width:280px;width:100%;box-sizing:border-box} +.search-filters-table input.search-oasis-id{min-width:280px} .transfer-category-select{width:auto;min-width:160px;max-width:200px;padding:6px 10px} .spread-row{margin:8px 0;display:flex;justify-content:flex-start} .spread-form{display:inline-block} -.btn-spread-on{background:#ffa300;color:#000} -.btn-spread-on:hover{background:#ffb733;color:#000} -.spread-form .btn-singleview{border-radius:16px;padding:7px 12px} +.spread-btn{cursor:pointer;font:inherit;padding:4px 10px;border:1px solid #555;border-radius:4px;background:#1a1a1a;color:#ffa500;font-size:14px} +.spread-btn:hover{background:#2a2a2a;border-color:#ffa500} +.spread-btn-on{background:#ffa500;color:#1a1a1a;border-color:#ff7300} +.spread-btn-on:hover{background:#ffb733;color:#000} .welcome-header{margin-bottom:18px} .welcome-chat{max-width:760px;margin:0 auto;padding:8px} .welcome-ai-header{display:flex;align-items:center;gap:10px;margin-bottom:10px;color:#ffa500;font-weight:600} @@ -6281,7 +6109,7 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .pm-exposition-text{line-height:1;} .shop-title-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px;padding:0;background:transparent;border-radius:0;box-shadow:none} .shop-title-row h2{margin:0} -.card-chips-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 10px 0;padding:0;border:none;background:transparent;border-radius:0;box-shadow:none} +.card-chips-row{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px 0;padding:0;border:none;background:transparent;border-radius:0;box-shadow:none} .tribe-card-body .job-price-line,.tribe-card-body .job-meta-line,.tribe-card-body .job-card-view-btn,.tribe-card-body .card-comments-summary,.tribe-card-body .tribe-card-members{padding:0;background:transparent;border-radius:0;box-shadow:none;border:none} .tribe-card-body .job-price-line{margin:4px 0} .tribe-card-body .job-card-view-btn{margin-top:8px;margin-bottom:0} @@ -6328,6 +6156,7 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .chat-card, .opinions-card, .post-card, +.map-card, .mention-card, .trending-card, .bank-claim-card, @@ -6491,6 +6320,7 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .chat-card .card-field, .opinions-card .card-field, .post-card .card-field, +.map-card .card-field, .mention-card .card-field, .trending-card .card-field, .bank-claim-card .card-field, @@ -6505,8 +6335,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} border-bottom: 1px solid #2a2a2a; align-items: baseline; } -.card-field > .card-value:only-child { grid-column: 1 / -1; min-width: 0; } - .card .card-field:last-child, .card-section .card-field:last-child, .card-fields-container .card-field:last-child, @@ -6565,6 +6393,8 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .card-date-highlight{color:#ffd740;font-size:18px;font-weight:700;margin:6px 0;letter-spacing:.5px;background:transparent !important;border:0 !important;padding:0 !important} .vote-buttons-row-single{display:flex;flex-wrap:wrap;gap:6px;background:transparent !important;border:0 !important;padding:0 !important} .vote-buttons-row-single form{margin:0;display:inline-block} +.card-visit-btn-centered{display:flex;justify-content:center;margin-top:10px;background:transparent !important;border:0 !important;padding:0 !important} +.card-visit-btn-centered form{margin:0} .tribe-card-body .voting-buttons{padding:0;background:transparent !important;border:0 !important;margin:8px 0 0 0} .card-assigned-list{display:flex;flex-direction:column;gap:4px;background:transparent !important;border:0 !important;padding:0 !important;margin:6px 0} .card-assigned-list a{display:block} @@ -6583,8 +6413,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .activity-card-header{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px;border:none;background:transparent;padding:0} .activity-card-header .card-chips-row{margin:0} .content-actions{display:flex;justify-content:right;align-items:center;gap:12px;flex-wrap:wrap;border:none;background:transparent;padding:0} -.tribe-side .content-actions{flex-wrap:nowrap;gap:6px} -.tribe-side .content-actions .btn-singleview{padding:6px 9px;font-size:1em} .trending-card.own-content{box-shadow:inset 3px 0 0 #ffa300} .trending-dominant-sep{margin:0 8px;opacity:0.5} .trending-dominant-text,.trending-total-count{font-weight:700} @@ -6611,993 +6439,3 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol .activitySpreadInhabitant2,.activityVotePost{padding:8px 16px;border-radius:5px;font-weight:bold;text-decoration:none;display:inline-block;border:2px solid transparent} .danger-btn,.delete-btn{border:1px solid #d9534f;color:#d9534f;background:transparent} .danger-btn:hover,.delete-btn:hover{background:#d9534f;color:#000} -.pm-fileshare-form-wrap{margin-top:16px} -.pm-fileshare-form input[type=file]{display:block;margin:10px 0 15px} -.pm-fileshare-info{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:8px 0} -.pm-fileshare-icon{font-size:1.2em} -.pm-fileshare-name{font-weight:bold;word-break:break-all} -.pm-fileshare-meta{font-size:.85em;opacity:.75} -.pm-fileshare-download-form{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px;border:0;background:transparent;padding:0} -.pm-fileshare-download{text-decoration:none;display:inline-block} -.industry-form{margin:0 auto} -.industry-governance{margin-top:16px} -.industry-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0} -.industry-build-vote{display:flex;flex-wrap:wrap;gap:8px;align-items:center} -.industry-estimate,.industry-materials,.industry-blueprint-meta,.industry-meta{border:none;background:transparent;padding:0;box-shadow:none;border-radius:0;margin:8px 0} -.industry-estimate p{margin:2px 0} -.industry-estimate-total{font-weight:700;margin-top:6px} -.industry-section{margin-top:20px} -.industry-applicants,.industry-members-list{list-style:none;padding:0;margin:8px 0} -.industry-applicants li,.industry-members-list li{padding:6px 0;display:flex;flex-wrap:wrap;gap:8px;align-items:center} -.industry-vote-form,.industry-invite-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;border:0;background:transparent;padding:0} -.industry-invite-form input{flex:1 1 240px} -.industry-vote-count{opacity:.85} -.industry-hint{opacity:.75;font-style:italic} -.industry-meta{margin:12px 0} -.industry-meta p{margin:4px 0} -.industry-meta-label{font-weight:bold} -.industry-card-desc,.industry-detail-desc{margin:8px 0} -.industry-side-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px} -.search-industry .card-field{margin:4px 0} -.industry-form select,.industry-form input[type="number"],.industry-form input[type="file"],.industry-form input[type="date"]{margin-bottom:15px} -.industry-form-media,.industry-detail-media{margin:8px 0} -.industry-hero-image,.tribe-card-hero-image{max-width:100%;height:auto;border-radius:6px} -.industry-search{margin:8px 0} -.industry-card-noimage{display:flex;align-items:center;justify-content:center;min-height:120px;font-size:48px;opacity:.5} -.industry-bot-notification .pm-title,.housing-bot-notification .pm-title{margin-top:4px} -.industry-table{width:100%;border-collapse:collapse;margin:8px 0} -.industry-table th,.industry-table td{text-align:left;padding:6px 8px;border-bottom:1px solid rgba(128,128,128,.3)} -.industry-contrib-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:6px 0} -.industry-contrib-form-single label{display:block;margin:10px 0 4px 0} -.industry-contrib-form-single input,.industry-contrib-form-single textarea,.industry-contrib-form-single select{width:100%;box-sizing:border-box;display:block;margin:0} -.industry-contrib-form-single .industry-input-num{width:auto;max-width:220px} -.industry-contrib-form-single button{margin-top:14px} -.industry-contrib-kind{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin:0 0 6px 0;border:none;background:transparent;padding:0;box-shadow:none} -.industry-contrib-kind label{margin:0} -.industry-contrib-kind select{width:auto;min-width:190px;margin:0;height:38px;box-sizing:border-box} -.industry-contrib-kind button{margin:0;height:38px;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center} -.industry-blueprint-card{border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:10px;margin:8px 0} -.industry-card-wrap{position:relative;border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:10px;margin:8px 0;background:transparent;box-shadow:none} -.industry-card-wrap .industry-blueprint-card{border:none;background:transparent;padding:0;margin:0;box-shadow:none;border-radius:0} -.industry-card-wrap > .card-header{position:absolute;top:8px;right:10px;margin:0;padding:0;border:none;background:transparent;box-shadow:none;z-index:1} -.industry-card-wrap > .card-header > span:empty{display:none} -.industry-card-wrap > .industry-blueprint-card{padding:0 96px 0 0} -.industry-actions form,.industry-actions .project-control-form--status{margin:0;padding:0;border:none;background:transparent;display:flex;align-items:center;gap:10px} -.industry-actions .project-control-select,.industry-actions .project-control-btn,.industry-actions select,.industry-actions button{margin:0} -.industry-actions button,.industry-actions .project-control-select{display:inline-flex;align-items:center;justify-content:center;height:38px;box-sizing:border-box} -.card-chips-row .spread-form{margin:0;padding:0;border:none;background:transparent;display:flex;align-items:center} -.card-chips-row .spread-form .btn-singleview{margin:0} -.industry-build-notes{margin:22px 0} -.industry-dates-row{border:none;background:transparent;padding:0;box-shadow:none;border-radius:0;margin:8px 0} -.industry-dates-row p{margin:0} -.dev-breadcrumb,.dev-stats,.dev-file-meta,.dev-file-head,.dev-listing,.dev-pager,.dev-actions,.dev-result-text,.dev-result-head,.dev-desktop{border:none;background:transparent;padding:0;box-shadow:none;border-radius:0} -.dev-stats{margin:12px 0} -.dev-stats p,.dev-file-meta p{margin:4px 0;line-height:1.9} -.dev-label{font-weight:bold} -.dev-sep{opacity:.45} -.dev-hint{opacity:.75;font-style:italic} -.dev-breadcrumb{margin:14px 0;word-break:break-all} -.dev-breadcrumb-sep{opacity:.45} -.dev-breadcrumb-current{font-weight:bold} -.dev-desktop{display:grid;grid-template-columns:repeat(auto-fill,minmax(118px,1fr));gap:12px;margin:16px 0} -.dev-icon{display:flex;flex-direction:column;align-items:center;justify-content:flex-start;text-align:center;gap:5px;padding:12px 6px;border:1px solid rgba(128,128,128,.28);border-radius:8px;text-decoration:none;word-break:break-word;min-height:104px} -.dev-icon:hover{border-color:rgba(128,128,128,.7)} -.dev-icon-glyph{font-size:30px;line-height:1} -.dev-icon-name{font-size:.9em;line-height:1.25} -.dev-icon-meta{font-size:.76em;opacity:.6} -.dev-icon-off{opacity:.5;cursor:default} -.dev-icon-off:hover{border-color:rgba(128,128,128,.28)} -.dev-file-head{display:flex;align-items:center;gap:10px;margin:14px 0 4px 0} -.dev-file-glyph{font-size:26px} -.dev-file-name{margin:0;word-break:break-all} -.dev-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:14px 0} -.dev-actions form{margin:0;padding:0;border:none;background:transparent} -.dev-actions button{margin:0} -.dev-code{overflow-x:auto;border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:8px;margin:12px 0} -.dev-code-table{border-collapse:collapse;width:100%} -.dev-code-num a{text-decoration:none} -.dev-code-line pre{margin:0;white-space:pre} -.dev-code-line code{font-family:monospace;font-size:.92em} -.dev-pager{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0} -.dev-list{list-style:none;padding:0;margin:14px 0} -.dev-entry{padding:9px 0;border-bottom:1px solid rgba(128,128,128,.2);word-break:break-all} -.dev-result-head{display:flex;align-items:center;gap:8px} -.dev-result-glyph{font-size:18px} -.dev-result-text{margin:5px 0 0 26px;overflow-x:auto} -.dev-result-text pre{margin:0} -.dev-table{width:100%;border-collapse:collapse;margin:8px 0} -.dev-table th,.dev-table td{text-align:left;padding:6px 8px;border-bottom:1px solid rgba(128,128,128,.3);word-break:break-all} -.filters .dev-toolbar{border:none !important;background:transparent !important;padding:0 !important;margin:0 !important;box-shadow:none !important;border-radius:0 !important} -.dev-search{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;border-radius:0 !important;margin:14px 0} -.dev-search .filter-box{margin:0} -.dev-search .filter-box__controls{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;border-radius:0 !important;margin-top:12px;display:flex;flex-wrap:wrap;gap:10px;align-items:center} -.dev-search .filter-box__select{width:auto;min-width:170px;margin:0;height:38px} -.dev-search .filter-btn{margin:0;height:38px;padding:0 20px;display:inline-flex;align-items:center;justify-content:center} -.dev-code-row td{border:none;vertical-align:top} -.dev-code-num{text-align:right;padding:0 14px 0 4px;width:1%;min-width:52px;white-space:nowrap;opacity:.5;user-select:none;border-right:1px solid rgba(128,128,128,.25) !important} -.dev-code-line{padding-left:16px} -.ai-suggestion-banner .update-banner-link{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60vw} -.welcome-banner .welcome-banner-close{margin:0;padding:0;border:none;background:transparent;box-shadow:none;display:inline-flex;align-items:center} -.welcome-banner-close-btn{background:transparent;border:none;color:inherit;cursor:pointer;font-size:1em;line-height:1;padding:0 4px;margin:0;opacity:.6} -.welcome-banner-close-btn:hover{opacity:1} -.welcome-ux-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;border:none !important;background:transparent !important} -.welcome-ux-option{display:flex;flex-direction:column;gap:6px;align-items:center;border:1px solid rgba(128,128,128,.35);border-radius:8px;padding:10px;cursor:pointer;background:transparent} -.welcome-ux-option:has(input:checked){border-color:#ffa300;background:rgba(255,163,0,.07)} -.welcome-ux-option input{margin:0} -.welcome-ux-shot{width:100%;border-radius:6px;border:1px solid rgba(128,128,128,.25)} -.welcome-ux-label{font-weight:bold} -@media (max-width: 700px){.welcome-ux-grid{grid-template-columns:1fr}} -.welcome-progress,.welcome-steps,.welcome-complete,.welcome-footer,.welcome-action,.welcome-step-head{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;border-radius:0 !important} -.welcome-progress{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;margin:12px 0} -.welcome-progress p{margin:0} -.welcome-progress form{margin:0;padding:0;border:none;background:transparent} -.welcome-progress-label{font-weight:bold} -.welcome-optional{opacity:.75;font-style:italic;margin:0 0 18px 0} -.welcome-steps{display:flex;flex-direction:column;gap:14px;margin:16px 0} -.welcome-step{border:1px solid rgba(128,128,128,.3) !important;border-radius:8px;padding:14px 16px !important;margin:0} -.welcome-step-done{opacity:.72} -.welcome-step-head{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 6px 0} -.welcome-step-number{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;border:1px solid rgba(128,128,128,.5);font-size:.85em;flex:0 0 auto} -.welcome-step-title{margin:0;font-size:1.05em} -.welcome-step-text{margin:0 0 12px 0} -.welcome-chip{border:1px solid rgba(128,128,128,.4);border-radius:12px;padding:2px 10px;font-size:.78em;opacity:.8} -.welcome-chip-done{opacity:1;font-weight:bold} -.welcome-action{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0} -.welcome-action form,.welcome-language-form{margin:0;padding:0;border:none;background:transparent} -.welcome-language-form select{width:auto;min-width:170px;height:38px;box-sizing:border-box;margin:0} -.welcome-action .filter-btn{margin:0;height:38px;padding:0 20px;display:inline-flex;align-items:center;justify-content:center} -.welcome-complete{margin:16px 0} -.welcome-footer{margin:20px 0 0 0} -.welcome-footer form{margin:0;padding:0;border:none;background:transparent} -.welcome-profile-form,.welcome-greeting-form{margin:0;padding:0;border:none;background:transparent;box-shadow:none} -.welcome-profile-form label{display:block;margin:10px 0 4px 0} -.welcome-profile-form input[type="text"],.welcome-profile-form textarea,.welcome-greeting-form textarea{width:100%;box-sizing:border-box;display:block;margin:0} -.welcome-profile-form input[type="file"]{margin:0;width:auto} -.welcome-profile-form .welcome-action,.welcome-greeting-form .welcome-action{margin-top:14px} -.welcome-profile-avatar{display:block;width:96px;height:96px;object-fit:cover;border-radius:6px;margin:0 0 10px 0} -.welcome-step-warning{margin:0 0 12px 0} -.welcome-oasisid{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;margin:0 0 14px 0;word-break:break-all} -.welcome-oasisid .user-link{user-select:all} -.housing-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:20px} -.housing-grid .tribe-card{margin-bottom:0} -.housing-card .card-field{border-bottom:none} -.housing-info-table{table-layout:auto} -.housing-info-table .tribe-info-label{width:1%;white-space:nowrap} -.housing-info-table .tribe-info-value{white-space:normal;word-break:normal;overflow-wrap:anywhere} -.housing-stars{color:#FFA500;font-size:14px;letter-spacing:1px} -.media-gallery{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;margin:0 0 12px 0} -.media-gallery .gallery{padding:0} -.housing-search .filter-box{margin:0} -.housing-place-input{height:36px;min-width:170px;width:auto;flex:1 1 170px} -.media-form-gallery{padding:0;margin:10px 0} -.media-form-gallery .gallery-item{cursor:default} -.housing-keep-photos{display:inline-flex;align-items:center;gap:6px;margin:0 0 10px 0} -.housing-keep-photos input[type="checkbox"]{width:auto;margin:0} -.tribe-main .housing-opinions{border:none;background:transparent} -.housing-form input[type="file"]{width:auto} -.vote-form-error{margin:0 0 14px 0} -.vote-form-error .error-title{margin:0 0 4px 0;font-weight:bold} -.vote-form-error p{margin:0} -.media-file-row{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 6px 0} -.media-file-row input[type="file"]{width:auto;margin:0} -.media-add-btn{margin:0} -.media-video{border:none !important;background:transparent !important;padding:0 !important;box-shadow:none !important;margin:0 0 10px 0} -.media-video-player{max-width:100%;border-radius:6px;display:block} -.media-form-video{max-width:320px} -.media-video-centered{display:flex;justify-content:center;margin-top:12px} -.media-video-centered .media-video-player{max-width:720px;width:100%} -.housing-card-video{display:flex;justify-content:center} -.housing-card-hero-video{width:100%;max-height:220px;border-radius:6px;display:block} -.housing-status-row,.housing-visibility-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px} -.housing-status-row form,.housing-visibility-row form{flex-basis:100%;margin:4px 0 0 0} -.housing-stars{display:block;margin:2px 0 6px 0} -.media-form-gallery .gallery-item{display:flex;flex-direction:column;align-items:center;gap:6px} -.media-remove-btn{height:28px;padding:0 10px;border:1px solid rgba(128,128,128,.5);border-radius:4px;background:transparent;color:inherit;cursor:pointer;font-size:.8em} -.media-form-video{display:flex;flex-direction:column;align-items:flex-start;gap:8px} -.spread-edit-warning{margin:0 0 14px 0} -.spread-edit-warning p{margin:0} -.market-opinions{border:none !important;background:transparent !important;padding:0 !important;margin:12px 0} -.housing-status-row .project-control-form{flex-basis:100%;margin:4px 0 0 0} -.task-assign-row{margin:8px 0} -.task-assign-row form{margin:0} -.reports-subfilters{margin-top:8px;align-items:center} -.reports-subfilters .card-label{margin:0 4px 0 8px;white-space:nowrap} -.stats-kpi-inline{display:flex;flex-direction:row;align-items:baseline;gap:6px;flex-wrap:wrap} -.stats-kpi-inline .stats-kpi-value{font-size:16px} -.stats-kpi-inline .stats-kpi-label{white-space:nowrap} -.content-actions .content-action-form{margin:0;display:inline-flex} -.content-actions .btn-pin-on{background:#ffa300;color:#000} -.content-actions .btn-pin-on:hover{background:#ffb733;color:#000} -.content-actions .spread-form{margin:0;display:inline-flex} - -.blog-card-body, -.blog-detail-body, -.mention-card-body { - border: none !important; - background: transparent !important; -} - -.blog-card-text, -.blog-detail-text, -.mention-text { - line-height: 1.6; - word-wrap: break-word; - overflow-wrap: anywhere; - border: none !important; - background: transparent !important; -} - -.blog-card-text img, -.blog-detail-text img { - max-width: 100%; - height: auto; - display: block; - margin: 10px 0; -} - -.blog-card-text pre, -.blog-detail-text pre { - overflow-x: auto; - white-space: pre-wrap; - word-break: break-word; -} - -.blog-card-meta { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 6px 0; - border: none !important; - background: transparent !important; -} - -.blog-allow-comments { - display: inline-flex; - align-items: center; - gap: 6px; - border: none !important; - background: transparent !important; -} - -.blog-allow-comments input[type="checkbox"] { - width: auto; - margin: 0; -} - -.logs-public-list { - display: flex; - flex-direction: column; - gap: 12px; - border: none !important; - background: transparent !important; -} - -.logs-public-body { - border: none !important; - background: transparent !important; -} - -.logs-public-meta { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - margin: 6px 0; - border: none !important; - background: transparent !important; -} - -.logs-published-hint { - margin-top: 10px; - opacity: .8; - font-size: .9em; -} - -.poll-card-body, -.poll-detail-body, -.poll-card-meta, -.poll-switch, -.poll-ballot, -.poll-option-row { - border: none !important; - background: transparent !important; -} - -.poll-results { - width: 100%; - border-collapse: collapse; - margin: 8px 0; -} - -.poll-results td { - padding: 4px 6px; - vertical-align: middle; -} - -.poll-result-option { - white-space: normal; - overflow-wrap: anywhere; -} - -.poll-result-count { - text-align: right; - white-space: nowrap; -} - -.poll-bar { - width: 100%; - min-width: 60px; - height: 10px; - border: 1px solid rgba(128, 128, 128, .5); - border-radius: 5px; - overflow: hidden; - background: transparent !important; -} - -.poll-bar-fill { - height: 100%; - background: rgba(128, 128, 128, .55) !important; - border: none !important; -} - -.poll-bar-fill-0 { width: 0%; } -.poll-bar-fill-5 { width: 5%; } -.poll-bar-fill-10 { width: 10%; } -.poll-bar-fill-15 { width: 15%; } -.poll-bar-fill-20 { width: 20%; } -.poll-bar-fill-25 { width: 25%; } -.poll-bar-fill-30 { width: 30%; } -.poll-bar-fill-35 { width: 35%; } -.poll-bar-fill-40 { width: 40%; } -.poll-bar-fill-45 { width: 45%; } -.poll-bar-fill-50 { width: 50%; } -.poll-bar-fill-55 { width: 55%; } -.poll-bar-fill-60 { width: 60%; } -.poll-bar-fill-65 { width: 65%; } -.poll-bar-fill-70 { width: 70%; } -.poll-bar-fill-75 { width: 75%; } -.poll-bar-fill-80 { width: 80%; } -.poll-bar-fill-85 { width: 85%; } -.poll-bar-fill-90 { width: 90%; } -.poll-bar-fill-95 { width: 95%; } -.poll-bar-fill-100 { width: 100%; } - -.poll-option-row { - margin: 4px 0; -} - -.poll-option-row input[type="radio"], -.poll-option-row input[type="checkbox"] { - width: auto; - margin-right: 6px; -} - -.poll-switch { - display: flex; - align-items: center; - margin: 6px 0; - padding: 0 !important; - border: none !important; - background: transparent !important; -} - -.poll-switch input[type="checkbox"] { - width: auto; - margin: 0 6px 0 0; -} - -.chat-poll-form form { - display: flex; - flex-direction: column; - align-items: flex-start; -} - -.chat-poll-form input[type="text"], -.chat-poll-form textarea { - width: 100%; - max-width: 640px; - margin: 0; -} - -.chat-poll { - display: block; -} - -.chat-poll-head { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 6px; -} - -.chat-poll-tag { - font-size: .75em; - text-transform: uppercase; - border: 1px solid rgba(128, 128, 128, .5); - border-radius: 4px; - padding: 1px 6px; -} - -.chat-poll-question { - font-weight: bold; - margin: 4px 0 8px 0; -} - -.chat-poll-meta { - margin: 6px 0; -} - -.chat-poll-form { - border: 1px solid rgba(128, 128, 128, .4); - border-radius: 6px; - padding: 8px 12px; - margin-bottom: 12px; - background: transparent !important; -} - -.chat-poll-summary { - display: inline-block !important; - width: auto !important; - max-width: max-content; - box-sizing: border-box; - cursor: pointer; - font: inherit; - font-weight: bold; - padding: 10px 20px; - margin-bottom: 10px; - border: 1px solid rgba(128, 128, 128, .5) !important; - border-radius: 5px; - background: rgba(128, 128, 128, .12) !important; - transition: background-color .2s, border-color .2s; - list-style: none; -} - -.chat-poll-summary:hover { background: rgba(128, 128, 128, .25) !important; } -.chat-poll-summary::-webkit-details-marker { display: none; } -.chat-poll-summary::after { content: " \25BE"; opacity: 0.85; } -details[open] > .chat-poll-summary::after { content: " \25B4"; } - -.poll-form-fields { - display: flex; - flex-direction: column; - gap: 4px; -} - -.larp-poll { - display: block; -} - -.reply-context { - border-left: 3px solid rgba(128, 128, 128, .6); - padding-left: 10px; - margin-bottom: 8px; - opacity: .85; -} - -.reply-context-meta { - font-size: .85em; -} - -.reply-context-text { - font-size: .85em; - max-height: 60px; - overflow: hidden; - margin-top: 4px; - font-weight: bold; -} - -.forum-reply-body { - margin-bottom: 12px; -} - -.forum-reply-text { - margin: 0 0 8px 0; - overflow-wrap: anywhere; -} - -.chat-thread-details, -.chat-thread-meta { - border: none !important; - background: transparent !important; -} - -.chat-thread-summary { - cursor: pointer; - font-weight: bold; - margin: 6px 0 6px 16px; -} - -.thread-reply-text { - white-space: pre-wrap; -} - -.event-recurrence-block, -.event-occurrences { - border: none !important; - background: transparent !important; - margin: 8px 0; -} - -.event-occurrence-list { - padding-left: 22px; - margin: 6px 0; -} - -.cv-ai-box, -.cv-match-row, -.inhabitant-subfilters { - border: none !important; - background: transparent !important; -} - -.cv-ai-box { - border: 1px solid rgba(128, 128, 128, .4) !important; - border-radius: 6px; - padding: 10px 14px; - margin-bottom: 12px; -} - -.cv-ai-box input[type="number"] { - width: auto; - min-width: 90px; -} - -.inhabitant-subfilters { - margin-top: 4px; -} - -.pads-grid { - grid-template-columns: repeat(auto-fill, minmax(min(100%, 520px), 1fr)); -} - -.poll-hue-0 { background: rgba(76,175,80,.75) !important; } -.poll-hue-1 { background: rgba(33,150,243,.75) !important; } -.poll-hue-2 { background: rgba(255,152,0,.75) !important; } -.poll-hue-3 { background: rgba(156,39,176,.75) !important; } -.poll-hue-4 { background: rgba(0,188,212,.75) !important; } -.poll-hue-5 { background: rgba(233,30,99,.75) !important; } -.poll-hue-6 { background: rgba(139,195,74,.75) !important; } -.poll-hue-7 { background: rgba(121,85,72,.75) !important; } - -.poll-result-mine .poll-result-option { - font-weight: bold; -} - -.poll-deadline { - margin: 6px 0; - border: none !important; - background: transparent !important; -} - -.chat-poll-actions { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - gap: 10px; - border: none !important; - background: transparent !important; -} - -.report-category-select { - width: auto; - max-width: 260px; -} - -.data-search { - margin-top: 10px; -} - -.data-cohesion { - display: flex; - flex-direction: column; - gap: 10px; - border: none !important; - background: transparent !important; -} - -.data-cohesion-value { - font-size: 2em; - font-weight: bold; - margin: 0; -} - -.data-cohesion-hint { - opacity: .75; - margin: 0; -} - -.data-cohesion-figures { - display: flex; - flex-wrap: wrap; - gap: 24px; - border: none !important; - background: transparent !important; -} - -.data-figure { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; - padding: 6px 14px; - border-radius: 6px; -} - -.data-figure-value { - font-size: 1.4em; - font-weight: bold; -} - -.data-figure-label { - font-size: .85em; - opacity: .7; - text-transform: uppercase; -} - -.bookmark-subtitle{color:#FFDD44;margin:2px 0 6px 0;font-size:15px} -.data-common-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:6px 0;background:transparent !important;border:0 !important;padding:0 !important} -.data-common-row .card-label{margin:0} -.data-common-row .tag-link{margin:0;padding:0.15em 0.55em;line-height:1.4} -.data-you-chip{margin-left:8px} -.data-top-terms { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 10px; - border: none !important; - background: transparent !important; -} - -.data-top-terms .tag-link { - font-size: 1.15em; -} - -.data-scale-0 { background: rgba(233, 30, 99, .75) !important; } -.data-scale-1 { background: rgba(255, 152, 0, .75) !important; } -.data-scale-2 { background: rgba(255, 214, 0, .75) !important; } -.data-scale-3 { background: rgba(139, 195, 74, .75) !important; } -.data-scale-4 { background: rgba(76, 175, 80, .75) !important; } - -.data-list { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 14px; - border: none !important; - background: transparent !important; -} - -@media (max-width: 900px) { - .data-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} - -@media (max-width: 600px) { - .data-list { grid-template-columns: 1fr; } -} - - -.data-card { - display: flex; - flex-direction: column; - gap: 6px; - padding: 12px 14px; - border-radius: 8px; - position: relative; -} - -.data-score-value, -.data-links-count { - font-weight: bold; - white-space: nowrap; -} - -.data-side-top { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; - padding-right: 40px; - border: none !important; - background: transparent !important; -} - -.data-side-top .data-score-label { - margin-left: auto; - margin-bottom: 0; - white-space: nowrap; -} - -.data-side-top .data-kind-chip { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -} - -.data-card .data-side-visit { - position: absolute; - top: 10px; - right: 12px; - margin: 0; -} - -.data-card .data-side-visit { - background: transparent !important; - border: 1px solid rgba(128, 128, 128, .45) !important; - border-radius: 10px; - padding: 2px 9px !important; - font-size: .9em; - line-height: 1.4; - display: inline-flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; -} - -.data-kind-chip { - display: inline-block; - font-size: .68em; - letter-spacing: .04em; - padding: 2px 7px; - border: 1px solid rgba(128, 128, 128, .45); - border-radius: 10px; - opacity: .85; - white-space: nowrap; -} - -.data-card-title { - font-weight: bold; - font-size: .95em; - line-height: 1.25; - min-width: 0; - overflow-wrap: anywhere; -} - -.data-best-card { - background: #26200f !important; - border: 1px solid rgba(255, 163, 0, .35) !important; -} - -.data-best-score { - font-size: 1.15em; -} - - -.data-card-author { - font-size: .78em; - opacity: .7; - max-width: 100%; - padding-left: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.data-cohesion-label { - font-size: .5em; - vertical-align: middle; - opacity: .75; -} - -.card-engage { - display: grid; - grid-template-columns: 1fr 1fr; - align-items: start; - gap: 8px; - margin: 8px 0; - border: none !important; - background: transparent !important; - padding: 0 !important; -} - -.card-engage > * { - min-width: 0; -} - -.card-engage > details[open] { - grid-column: 1 / -1; -} - -.card-engage > details[open] ~ details { - grid-column: 1 / -1; -} - -.card-engage .opinions-voting-collapse, -.card-engage .comments-collapse { - margin: 0; -} - -.card-engage .opinions-summary, -.card-engage .comments-summary { - width: 100% !important; - max-width: none; - text-align: center; -} - -@media (max-width: 600px) { - .card-engage { grid-template-columns: 1fr; } -} - -.activity-search input[type="text"], -.blockexplorer-search-pair input[type="text"], -.blockexplorer-search-dates input[type="text"], -.blockexplorer-search-actions input[type="text"], -.logs-toolbar input[type="text"], -.search-filters-row input[type="text"], -.tribe-search-form input[type="text"], -.filter-box input[type="text"], -.ui-toolbar input[type="text"] { - margin: 0; -} - -.calendar-invite-actions, -.calendar-owner-actions { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 10px; -} - -.calendar-open-invite-block { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; - border: none !important; - background: transparent !important; - padding: 0 !important; -} - -.image-container { - border: none !important; - background: transparent !important; - padding: 0 !important; -} - -.activity-sub-filter { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - margin: 4px 0 14px 0; - border: none !important; - background: transparent !important; - padding: 0 !important; -} - -.activity-sub-filter .sub-filter-form { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 0; -} - -.larp-academia-hint .larp-my-house-label { - opacity: .85; -} - -.larp-academia-hint .larp-my-house-link { - font-weight: bold; -} - -.oasis-footer-version { - font-weight: bold; -} - -.larp-test-panel { - border: 1px solid rgba(128, 128, 128, .4) !important; - border-radius: 6px; - padding: 12px 16px; - margin-top: 16px; - background: transparent !important; -} - -.larp-test-panel-hint { - color: #FFA500; - opacity: .85; - font-size: 14px; - line-height: 1.5; - margin: 4px 0 12px 0; -} - -@media (max-width: 900px) { - .larp-academia-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} - -@media (max-width: 560px) { - .larp-academia-grid { grid-template-columns: 1fr; } -} - -@media (max-width: 900px) { - .larp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} - -@media (max-width: 560px) { - .larp-grid { grid-template-columns: 1fr; } -} - -.chat-jump-row { - display: flex; - justify-content: flex-end; - margin-bottom: 8px; - border: none !important; - background: transparent !important; - padding: 0 !important; -} - -.chat-latest-anchor { - scroll-margin-top: 16px; - border: none !important; - background: transparent !important; - padding: 0 !important; -} - -.ai-prompt-form{margin-bottom:0} -.ai-submit-row{display:flex;gap:1.5em;justify-content:flex-end;align-items:center;margin-top:.7em} -.ai-clear-row{display:flex;justify-content:flex-end;margin-bottom:2em} -.ai-clear-form{display:inline} -.ai-clear-btn{background:#b80c09;color:#fff;border:none;padding:.4em 1.2em;border-radius:6px;cursor:pointer;font-size:1em;margin-left:1em} -.user-prompt{margin-bottom:2em;font-size:.95em;color:#888} -.user-prompt-text{font-style:italic} -.chat-entry{margin-bottom:2em;position:relative;background:#191919;border-radius:10px;box-shadow:0 0 8px #0004;padding-top:1.8em} -.chat-entry-timestamp{position:absolute;top:.5em;right:1.3em;font-size:.92em;color:#888} -.user-question{margin-bottom:.75em} -.ai-response{max-width:800px;margin:auto;background:#111;padding:1.25em;border-radius:6px;font-family:sans-serif;line-height:1.6;color:#ffcc00} -.ai-response p{margin-bottom:1.2em} -.ai-train-bar{display:flex;align-items:center;gap:12px;margin:12px auto 8px auto;max-width:800px;padding:8px 0;border-top:1px solid #2a2a2a;flex-wrap:wrap} -.ai-snippets-used{color:#9aa;font-size:.95em} -.ai-train-approved{color:#5ad25a;font-weight:600} -.ai-train-rejected{color:#ff6b6b;font-weight:600} - -.modules-counts{margin-bottom:16px} -.modules-count-value{color:#888} -.modules-count-list{list-style-type:none;padding:0;margin:0} -.modules-count-item{font-size:18px;color:#555;margin:8px 0} -.modules-preset-form{display:inline;margin:0} -.module-status-cell{text-align:center} -.save-button-container{margin-top:20px;text-align:center} - -.votes-count-negative{margin-left:12px} -.forum-thread-header{display:flex;align-items:flex-start} -.root-vote-col{width:60px;text-align:center} -.forum-thread-header .forum-main-col{flex:1;padding-left:10px} -.new-message-wrapper{margin-top:12px} -.new-message-textarea{width:100%} -.new-message-form .forum-send-btn{margin-top:4px} - -.card-field-row{display:flex;gap:.6em;flex-wrap:wrap} -.card-section-action{margin-top:6px} -.card-section-text{margin:.4rem 0} -.post-text-clamped{max-height:180px;overflow:hidden} -.reply-context{border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:.85} -.reply-context-label{font-size:.85em} -.reply-context-text{font-size:.85em;max-height:80px;overflow:hidden;margin-top:4px} -.activity-spread-clamped{max-height:200px;overflow:hidden} -.activity-spread-missing{opacity:.6;font-style:italic} - -.search-select{position:relative;z-index:10} -.card-field-stacked{display:flex;flex-direction:column} -.card-value-pre{white-space:pre-wrap;margin-top:4px} - -.trending-container{display:grid;grid-template-columns:1fr;gap:20px} -.feed-vote-sep{margin:0 8px;opacity:.5} -.feed-vote-dominant{font-weight:700} -.image-container-row,.video-container-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} -.video-container-row{flex-direction:column;align-items:stretch;width:100%} -.video-container-row video{width:100%;max-height:640px} -.vote-no-quorum{color:#ffcc00;font-weight:bold} - -.pixelia-swatch-000000{background-color:#000000} -.pixelia-swatch-ffffff{background-color:#ffffff} -.pixelia-swatch-17f018{background-color:#17f018} -.pixelia-swatch-ffbb00{background-color:#ffbb00} -.pixelia-swatch-ff0000{background-color:#ff0000} -.pixelia-swatch-0000ff{background-color:#0000ff} -.pixelia-swatch-ffff00{background-color:#ffff00} -.pixelia-swatch-00ff00{background-color:#00ff00} -.pixelia-swatch-00ffff{background-color:#00ffff} -.pixelia-swatch-ff00ff{background-color:#ff00ff} -.pixelia-swatch-a52a2a{background-color:#a52a2a} -.pixelia-swatch-800080{background-color:#800080} -.pixelia-swatch-808000{background-color:#808000} -.pixelia-swatch-00bfff{background-color:#00bfff} -.pixelia-swatch-d3d3d3{background-color:#d3d3d3} -.pixelia-swatch-ff6347{background-color:#ff6347} diff --git a/src/client/assets/themes/Clear-SNH.css b/src/client/assets/themes/Clear-SNH.css deleted file mode 100644 index a04c3f3..0000000 --- a/src/client/assets/themes/Clear-SNH.css +++ /dev/null @@ -1,385 +0,0 @@ -body { - background-color: #F9F9F9 !important; - color: #2C2C2C !important; - font-family: 'Roboto', sans-serif !important; -} - -.main-column { - background-color: #FFFFFF !important; - border: 1px solid #E0E0E0 !important; -} - -button, input[type="submit"], input[type="button"] { - background-color: #FF6F00 !important; - color: #FFFFFF !important; - border: none !important; - border-radius: 6px !important; - padding: 10px 20px !important; - cursor: pointer !important; - font-weight: 600 !important; -} - -button:hover, input[type="submit"]:hover, input[type="button"]:hover { - background-color: #FF8F00 !important; -} - -input, textarea, select { - background-color: #FFFFFF !important; - color: #2C2C2C !important; - border: 1px solid #E0E0E0 !important; - border-radius: 4px !important; - padding: 8px !important; - font-size: 16px !important; -} - -a { - color: #007BFF !important; - text-decoration: none !important; -} - -a:hover { - text-decoration: underline !important; -} - -p { - color: black !important; - text-decoration: none !important; -} - -.created-at, .about-time, .time { - font-size: 0.9rem; - color: black; -} - -table { - background-color: #FFFFFF !important; - color: #2C2C2C !important; - width: 100% !important; - border-collapse: collapse !important; -} - -table th { - background-color: #F8F8F8 !important; - padding: 12px 15px !important; - text-align: left !important; - font-weight: 600 !important; -} - -table tr:nth-child(even) { - background-color: #FAFAFA !important; -} - -table td { - padding: 12px 15px !important; -} - -.profile { - background-color: #FFFFFF !important; - padding: 20px !important; - border-radius: 8px !important; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important; -} - -.profile .name { - color: #FF6F00 !important; - font-size: 20px !important; - font-weight: 700 !important; -} - -.avatar { - border: 3px solid #FF6F00 !important; - border-radius: 50% !important; - width: 60px !important; - height: 60px !important; -} - -article, section { - background-color: #FFFFFF !important; - color: #2C2C2C !important; - padding: 20px !important; - border-radius: 8px !important; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important; -} - -.post-preview img { - border-radius: 8px !important; - max-width: 100% !important; - height: auto !important; -} - -div { - background-color: #FFFFFF !important; - border: 1px solid #E0E0E0 !important; -} - -::-webkit-scrollbar-thumb { - background-color: #B0B0B0 !important; -} - -::-webkit-scrollbar-track { - background-color: #F9F9F9 !important; -} - -.action-container { - background-color: #FFFFFF !important; - border-color: #E0E0E0 !important; - color: #2C2C2C !important; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important; -} - -footer { - background-color: #FFFFFF !important; - border-top: 1px solid #E0E0E0 !important; - padding: 15px 0 !important; -} - -footer a { - background-color: #007BFF !important; - color: #FFFFFF !important; - padding: 10px 20px !important; - border-radius: 6px !important; - text-decoration: none !important; - font-weight: 600 !important; -} - -footer a:hover { - background-color: #0056b3 !important; -} - -.sidebar-left nav ul, -.sidebar-right nav ul { - display: flex !important; - flex-direction: column !important; -} - -.sidebar-left nav ul, -.sidebar-right nav ul { - display: flex; - flex-direction: column; - margin: 0; -} - -.sidebar-left nav ul li, -.sidebar-right nav ul li { - width: 100%; - display: block; -} - -.sidebar-left nav ul li a, -.sidebar-right nav ul li a, -.header nav ul li a { - display: block; - width: 100%; - padding: 12px 16px; - font-size: 15px; - font-weight: 500; - border-radius: 6px; - background-color: #ffffff !important; - color: #2C2C2C !important; - border: 1px solid #D0D0D0 !important; - text-align: left; - box-sizing: border-box; -} - -.sidebar-left nav ul li a:hover, -.sidebar-right nav ul li a:hover, -.header nav ul li a:hover { - background-color: #f0f0f0 !important; -} - -.filter-btn, -.create-button, -.edit-btn, -.delete-btn, -.join-btn, -.leave-btn, -.buy-btn { - background-color: #FF6F00 !important; - color: #FFFFFF !important; - border: none !important; -} - -.filter-btn:hover, -.create-button:hover { - background-color: #FF8F00 !important; - color: #FFFFFF !important; -} - -.card { - color: #4A4A4A; - box-shadow: 0 4px 30px 0 rgba(0, 0, 0, 0.1); - background-color: #F4F4F4; -} - -.card-label { - color: #2D2D2D; -} - -.card-footer { - color: #6C6C6C; - background: none; -} - -.card-field { - background: none; -} - -.card-tags a.tag-link { - color: #181818; - background: #D94F4F; -} - -.card-tags a.tag-link:hover { - background: #D94F4F; - color: #111; - cursor: pointer; -} - -a.user-link { - background-color: #FFD600; - color: #FFFFFF; - border-color: #FFD600; - transition: background-color 0.3s, color 0.3s, border-color 0.3s; -} - -a.user-link:hover { - background-color: #FFD600; - border-color: #FFD600; - color: #FFFFFF; - cursor: pointer; -} - -a.user-link:focus { - background-color: #9A2F2F; - border-color: #9A2F2F; - color: #FFFFFF; -} - -.date-link { - background-color: #2F3C32; - color: #fff; -} - -.date-link:hover { - background-color: #3E4A3D; - color: #fff; -} - -.activitySpreadInhabitant2 { - background-color: #3E4A3D; - color: #fff; -} - -.activityVotePost { - background-color: #3B5C42; - color: #fff; -} - - -.update-banner { - background-color: #FFF3E0 !important; - border-bottom-color: #FFE0B2 !important; - color: #E65100 !important; -} - -.update-banner-link { - color: #FF6F00 !important; -} - -.snh-invite-code { - color: #007BFF !important; -} - -.carbon-bar-track { - background: #e0e0e0 !important; -} - -.carbon-bar-max { - background: #cc7700 !important; -} - -.stats-kpi-label { color: #2D2D2D !important; } -.stats-kpi-value { color: #007BFF !important; } -.carbon-bar-note, .carbon-bar-formula { color: #007BFF !important; } -.graphos-node-label { fill: #2C2C2C !important; } -.graphos-node-label-me { fill: #cc7700 !important; } -.data-best-label { color: #cc7700 !important; } -.data-best-card { background: #FFF3E0 !important; border: 1px solid rgba(204, 119, 0, .4) !important; } -.graphos-legend { color: #2C2C2C !important; } -.graphos-edge { stroke: #BBB !important; } -.graphos-edge-discovered { stroke: #b8860b !important; } -.graphos-edge-unknown { stroke: #999 !important; } -.graphos-node-circle-discovered { fill: #ffd700 !important; stroke: #b8860b !important; } -.graphos-legend-dot.graphos-node-circle-discovered { background: #ffd700 !important; border-color: #b8860b !important; } -/* Blockexplorer */ -.blockchain-view { background-color: #F4F4F4 !important; color: #2C2C2C !important; } -.block { background: #FFFFFF !important; box-shadow: 0 2px 12px rgba(0,0,0,0.06) !important; } -.block:hover { box-shadow: 0 8px 32px rgba(0,0,0,0.10) !important; } -.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #2D2D2D !important; } -.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #007BFF !important; } -.block-content-preview, .block-content { background: #F8F8F8 !important; color: #2C2C2C !important; } -.block-author { color: #FF6F00 !important; background: rgba(255,111,0,0.08) !important; } -.block-author:hover { color: #FF8F00 !important; } -.block-url { color: #007BFF !important; } -.block-row--details .block-url { background: #F0F0F0 !important; } -.block-row--details .block-url:hover { background: #E0E0E0 !important; color: #FF6F00 !important; } -.btn-singleview { background: #F0F0F0 !important; color: #FF6F00 !important; } -.btn-singleview:hover { background: #E0E0E0 !important; color: #FF8F00 !important; } -.btn-spread-on, .content-actions .btn-pin-on { background: #FF6F00 !important; color: #FFFFFF !important; } -.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #FF8F00 !important; color: #FFFFFF !important; } -.btn-back { background: #FF6F00 !important; color: #FFFFFF !important; } -.btn-back:hover { background: #FF8F00 !important; color: #FFFFFF !important; } -.block-info-table td, .pm-info-table td { border-color: #E0E0E0 !important; } -.block-diagram { border-color: #E0E0E0 !important; background: #FFFFFF !important; } -.block-diagram-ruler { color: #2D2D2D !important; background: #F8F8F8 !important; border-bottom-color: #E0E0E0 !important; } -.block-diagram-ruler span { color: #2D2D2D !important; } -.block-diagram-cell { border-color: #E0E0E0 !important; background: #FFFFFF !important; } -.bd-label { color: #2D2D2D !important; } -.bd-value { color: #007BFF !important; } -.deleted-label { color: #D32F2F !important; } - -/* Tribes */ -.tribe-card { background: #FFFFFF !important; border-color: #E0E0E0 !important; } -.tribe-card:hover { border-color: #007BFF !important; } -.tribe-card-title { color: #2D2D2D !important; } -.tribe-card-description { color: #555 !important; } -.tribe-info-table td { border-color: #E0E0E0 !important; } -.tribe-info-label { color: #2D2D2D !important; background: #FFFFFF !important; } -.tribe-info-value { color: #007BFF !important; background: #FFFFFF !important; } -.tribe-info-empty { color: #999 !important; } -.tribe-card-members { border-color: #E0E0E0 !important; background: #F8F8F8 !important; } -.tribe-members-count { color: #FF6F00 !important; } -.tribe-card-actions { border-color: #E0E0E0 !important; background: #F8F8F8 !important; } -.tribe-action-btn { border-color: #FF6F00 !important; color: #FF6F00 !important; } -.tribe-action-btn:hover { background: #FF6F00 !important; color: #fff !important; } -.tribe-subtribe-link { background: #F0F0F0 !important; border-color: #E0E0E0 !important; color: #FF6F00 !important; } -.tribe-thumb-link { border-color: #E0E0E0 !important; } -.tribe-thumb-link:hover { border-color: #FF6F00 !important; } -.tribe-subtribe-link:hover { background: #E0E0E0 !important; } -.tribe-parent-image { border-color: #E0E0E0 !important; } -.tribe-parent-box { background: #FFFFFF !important; } -.chat-message { background: #FFFFFF !important; border: 1px solid #E0E0E0 !important; } -.chat-bubble-row-self .chat-message { background: #FFF3E0 !important; border-color: rgba(204, 119, 0, .35) !important; } -.chat-message-author { background: #FDF6EC !important; } -.chat-bubble-sender { color: #1565C0 !important; } -.chat-bubble-sender-owner { color: #cc7700 !important; } -.chat-bubble-time { color: #999 !important; } -.chat-day-chip { background: #F0F0F0 !important; color: #666 !important; border-color: #DDD !important; } -.chat-topics { background: #FAFAFA !important; border-color: #E0E0E0 !important; } -.chat-topic:hover { background: #F0F0F0 !important; } -.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; } diff --git a/src/client/assets/themes/Dark-SNH.css b/src/client/assets/themes/Dark-SNH.css deleted file mode 100644 index 7167ffe..0000000 --- a/src/client/assets/themes/Dark-SNH.css +++ /dev/null @@ -1,364 +0,0 @@ -body { - background-color: #121212; - color: #FFD700; -} - -header, footer { - background-color: #1F1F1F; -} - -.sidebar-left, .sidebar-right { - background-color: #1A1A1A; - border: 1px solid #333; -} - -.main-column { - background-color: #1C1C1C; -} - -button, input[type="submit"], input[type="button"] { - background-color: #444; - color: #FFD700; - border: 1px solid #444; -} - -button:hover, input[type="submit"]:hover, input[type="button"]:hover { - background-color: #333; - border-color: #666; -} - -input, textarea, select { - background-color: #333; - color: #FFD700; - border: 1px solid #555; -} - -a { - color: #FFD700; -} - -a:hover { - color: #FFDD44; -} - -table { - background-color: #222; - color: #FFD700; -} - -table th { - background-color: #333; -} - -table tr:nth-child(even) { - background-color: #2A2A2A; -} - -nav ul li a:hover { - color: #FFDD44; - text-decoration: underline; -} - -.profile { - background-color: #222; - padding: 15px; - border-radius: 8px; -} - -.profile .name { - color: #FFD700; -} - -.avatar { - border: 3px solid #FFD700; -} - -article, section { - background-color: #1C1C1C; - color: #FFD700; -} - -.article img { - border: 3px solid #FFD700; -} - -.post-preview img { - border: 3px solid #FFD700; -} - -.post-preview .image-container { - max-width: 100%; - overflow: hidden; - display: block; - margin: 0 auto; -} - -div { - background-color: #1A1A1A; - border: 1px solid #333; -} - -div .header-content { - width: 100%; -} - -::-webkit-scrollbar { - width: 10px; -} - -::-webkit-scrollbar-thumb { - background-color: #444; - border-radius: 10px; -} - -::-webkit-scrollbar-track { - background-color: #222; -} - -.action-container { - background-color: #1A1A1A; - border: 1px solid #333; - padding: 10px; - border-radius: 8px; - color: #FFD700; -} - -footer { - background-color: #1F1F1F; - padding: 10px 0; -} - -footer a { - background-color: #444; - color: #FFD700; - text-align: center; - padding: 8px 16px; - border-radius: 5px; - text-decoration: none; -} - -footer a:hover { - background-color: #333; - color: #FFDD44; -} - -.card { - border-radius: 16px; - padding: 0px 24px 10px 24px; - margin-bottom: 16px; - color: #FFD600; - font-family: inherit; - box-shadow: 0 2px 20px 0 #FFD60024; -} - -.card-section { - border:none; - padding: 10px 0 0 16px; -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: 0px; - margin-top: 8px; - padding-top: 0px; - border: none; -} - -.card-label { - color: #ffa300; - font-weight: bold; - letter-spacing: 1.5px; - line-height: 1.2; - margin-bottom: 0; -} - -.card-footer { - margin-top: 6px; - font-weight: 500; - color: #ff9900; - font-size: 1.07em; - display: flex; - align-items: center; - gap: 8px; - background: none; - border: none; - padding-top: 0; - margin-bottom: 6; -} - -.card-body { - margin-top: 0; - margin-bottom: 4; - padding: 0; -} - -.card-field { - display: flex; - align-items: baseline; - padding: 0; - margin-bottom: 0; - border: none; - background: none; -} - -.card-tags { - margin: 5px 0 3px 0; - display: flex; - flex-wrap: wrap; - gap: 9px; -} - -.card-tags a.tag-link { - text-decoration: none; - color: #181818; - background: #FFD600; - padding: 5px 13px 4px 13px; - border-radius: 7px; - font-size: .98em; - border: none; - font-weight: bold; -} - -.card-tags a.tag-link:hover { - background: #ffe86a; - color: #111; - cursor: pointer; -} - -a.user-link { - background-color: #FFA500; - color: #000; - padding: 8px 16px; - border-radius: 5px; - text-align: center; - font-weight: bold; - text-decoration: none; - display: inline-block; - border: 2px solid transparent; - transition: background-color 0.3s, color 0.3s, border-color 0.3s; - font-size: 0.8em; -} - -a.user-link:hover { - background-color: #FFD700; - border-color: #FFD700; - color: #000; - cursor: pointer; -} - -a.user-link:focus { - background-color: #007B9F; - border-color: #007B9F; - color: #fff; -} - -.date-link { - background-color: #444; - color: #FFD600; - padding: 8px 16px; - border-radius: 5px; - margin-left: 8px; -} - -.date-link:hover { - background-color: #555; - color: #FFD700; -} - -.activitySpreadInhabitant2 { - background-color: #007B9F; - color: #fff; - padding: 8px 16px; - border-radius: 5px; - font-weight: bold; - text-decoration: none; - display: inline-block; - border: 2px solid transparent; -} - -.activityVotePost { - background-color: #557d3b; - color: #fff; - padding: 8px 16px; - border-radius: 5px; - font-weight: bold; - text-decoration: none; - display: inline-block; - border: 2px solid transparent; -} - -.update-banner { - background-color: #1a1400; - border-bottom-color: #3a2e00; - color: #FFD700; -} - -.update-banner-link { - color: #FFD700; -} - -.oasis-footer-center a { - color: #FFA500; -} - -.oasis-footer-center a:hover { - color: #FFD700; -} - -.snh-invite-code { - color: #FFA500 !important; -} - -/* Blockexplorer */ -.stats-kpi-label { color: #ffa300 !important; } -.stats-kpi-value { color: #FFD700 !important; } -.carbon-bar-note, .carbon-bar-formula { color: #FFD700 !important; } -.graphos-node-label { fill: #ddd !important; } -.graphos-node-label-me { fill: #ffa500 !important; } -.graphos-legend { color: #ddd !important; } -.blockchain-view { background-color: #191b20 !important; color: #FFD700 !important; } -.block { background: #23242a !important; } -.block:hover { box-shadow: 0 8px 32px rgba(35,40,50,0.18) !important; } -.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #ffa300 !important; } -.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #FFD700 !important; } -.block-content-preview, .block-content { background: #222326 !important; color: #FFD700 !important; } -.block-author { color: #FFD700 !important; background: rgba(255,163,0,0.08) !important; } -.block-author:hover { color: #FFDD44 !important; } -.block-url { color: #FFD700 !important; } -.block-row--details .block-url { background: #1f2023 !important; } -.block-row--details .block-url:hover { background: #292b36 !important; color: #ffa300 !important; } -.btn-singleview { background: #1e1f23 !important; color: #ffa300 !important; } -.btn-singleview:hover { background: #2d2e34 !important; color: #FFD700 !important; } -.btn-spread-on, .content-actions .btn-pin-on { background: #ffa300 !important; color: #000000 !important; } -.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #ffb733 !important; color: #000000 !important; } -.btn-back { background: #21232b !important; color: #ffa300 !important; } -.btn-back:hover { background: #292b36 !important; color: #FFD700 !important; } -.block-info-table td, .pm-info-table td { border-color: #444 !important; } -.block-diagram { border-color: #555 !important; background: #191b20 !important; } -.block-diagram-ruler { color: #ffa300 !important; background: #111 !important; border-bottom-color: #555 !important; } -.block-diagram-ruler span { color: #ffa300 !important; } -.block-diagram-cell { border-color: #555 !important; background: #1e1f23 !important; } -.bd-label { color: #ffa300 !important; } -.bd-value { color: #FFD700 !important; } - -/* Tribes */ -.tribe-card { background: #23242a !important; border-color: #444 !important; } -.tribe-card:hover { border-color: #ffa300 !important; } -.tribe-card-description { color: #cfd3e1 !important; } -.tribe-info-table td { border-color: #444 !important; } -.tribe-info-label { color: #ffa300 !important; background: #1e1f23 !important; } -.tribe-info-value { color: #FFD700 !important; background: #1e1f23 !important; } -.tribe-info-empty { color: #9aa3b2 !important; } -.tribe-card-members { border-color: #444 !important; background: #1e1f23 !important; } -.tribe-members-count { color: #ffa300 !important; } -.tribe-card-actions { border-color: #444 !important; background: #1e1f23 !important; } -.tribe-action-btn { border-color: #ffa300 !important; color: #ffa300 !important; } -.tribe-action-btn:hover { background: #ffa300 !important; color: #000 !important; } -.tribe-subtribe-link { background: #191b20 !important; border-color: #444 !important; color: #ffa300 !important; } -.tribe-thumb-link { border-color: #444 !important; } -.tribe-thumb-link:hover { border-color: #ffa300 !important; } -.tribe-subtribe-link:hover { background: #333 !important; } -.tribe-parent-image { border-color: #ffa300 !important; } -.tribe-parent-box { background: #1e1f23 !important; } diff --git a/src/client/assets/themes/Matrix-SNH.css b/src/client/assets/themes/Matrix-SNH.css deleted file mode 100644 index a0cb38d..0000000 --- a/src/client/assets/themes/Matrix-SNH.css +++ /dev/null @@ -1,382 +0,0 @@ -body { - background-color: #000000 !important; - color: #00FF00 !important; - font-family: 'Courier New', monospace; -} - -header, footer { - background-color: #000000; - color: #00FF00; - padding: 20px; - text-align: center; - font-size: 18px; -} - -footer { - border-top: 2px solid #00FF00; -} - -.sidebar-left, .sidebar-right { - background-color: #000000; - color: #00FF00; - border-right: 2px solid #00FF00; - padding: 15px; - margin-bottom: 10px; -} - -.main-column { - background-color: #000000; - color: #00FF00; - padding: 20px; - border-left: 2px solid #00FF00; - margin-bottom: 20px; -} - -button, input[type="submit"], input[type="button"] { - background-color: #000000; - color: #00FF00; - border: 2px solid #00FF00; - border-radius: 8px; - padding: 10px 20px; - cursor: pointer; - font-family: 'Courier New', monospace; - text-transform: none; - font-size: 16px; -} - -button:hover, input[type="submit"]:hover, input[type="button"]:hover { - background-color: #00FF00; - color: #000000; - border-color: #00FF00; - box-shadow: 0 0 10px rgba(0, 255, 0, 0.8); -} - -input, textarea, select { - background-color: #1A1A1A; - color: #00FF00; - border: 1px solid #00FF00; - border-radius: 5px; - padding: 10px; - font-family: 'Courier New', monospace; - font-size: 16px; -} - -a { - color: #00FF00; - text-decoration: none; - font-weight: normal; - font-size: 16px; -} - -table { - background-color: #000000; - color: #00FF00; - width: 100%; - border: 1px solid #00FF00; - font-size: 16px; -} - -table th { - background-color: #00FF00; - color: #000000; -} - -table tr:nth-child(even) { - background-color: #1A1A1A; -} - -nav ul { - background-color: #000000; - padding: 0; - margin: 0; -} - -nav ul li { - display: inline-block; - margin-right: 10px; -} - -nav ul li a { - color: #00FF00; - padding: 10px; - display: inline-block; - font-family: 'Courier New', monospace; - text-transform: none; - font-size: 16px; -} - -.profile { - background-color: #1A1A1A; - color: #00FF00; - padding: 20px; - border-radius: 8px; - border: 1px solid #00FF00; - box-shadow: 0 2px 5px rgba(0,255,0,0.5); -} - -.profile .name { - color: #00FF00; - font-size: 18px; - font-weight: bold; -} - -.avatar { - border: 4px solid #00FF00; - border-radius: 50%; - width: 80px; - height: 80px; - margin-bottom: 10px; -} - -article, section { - background-color: #1A1A1A; - color: #00FF00; - padding: 20px; - border-radius: 10px; - border: 1px solid #00FF00; - box-shadow: 0 4px 10px rgba(0, 255, 0, 0.5); -} - -.post-preview { - background-color: #00FF00; - padding: 15px; - border-radius: 8px; - color: #000000; -} - -.post-preview img { - border-radius: 8px; - max-width: 100%; -} - -::-webkit-scrollbar-thumb { - background-color: #00FF00; -} - -::-webkit-scrollbar-track { - background-color: #000000; -} - -.action-container { - background-color: #1A1A1A; - border-color: #00FF00; - color: #00FF00; -} - -footer a { - background-color: #00FF00; - color: #000000; - padding: 8px 20px; - border-radius: 5px; - text-decoration: none; -} - -.top-bar-left, -.top-bar-mid, -.top-bar-right { - background-color: #000000 !important; - border: 2px solid #00FF00 !important; - padding: 12px 16px; - box-shadow: 0 0 12px #00FF00; - border-radius: 8px; - display: flex; - gap: 12px; -} - -.sidebar-left, -.sidebar-right { - background-color: #000000 !important; - border: 2px solid #00FF00 !important; - box-shadow: 0 0 15px #00FF00; - padding: 16px; -} - -.sidebar-left nav ul, -.sidebar-right nav ul { - display: flex; - flex-direction: column; - gap: 10px; - margin: 0; - padding: 0; -} - -.sidebar-left nav ul li, -.sidebar-right nav ul li { - width: 100%; -} - -.sidebar-left nav ul li a, -.sidebar-right nav ul li a, -.header nav ul li a { - background-color: #000000 !important; - color: #00FF00 !important; - border: 1px solid #00FF00 !important; - font-weight: bold; - border-radius: 6px; - padding: 10px 14px; - display: flex; - justify-content: flex-start; - box-shadow: 0 0 6px #00FF00; - transition: background-color 0.3s ease, color 0.3s ease; -} - -.sidebar-left nav ul li a:hover, -.sidebar-right nav ul li a:hover, -.header nav ul li a:hover { - background-color: #00FF00 !important; - color: #000000 !important; -} - -.card { - color: #00FF00; - font-family: 'Courier New', monospace; - background-color: #1A1A1A; -} - -.card-label { - color: #00FF00; -} - -.card-footer { - color: #00FF00; -} - -.card-tags a.tag-link { - color: #00FF00; - background: #000000; -} - -.card-tags a.tag-link:hover { - background: #00FF00; - color: #000000; - cursor: pointer; -} - -a.user-link { - background-color: #00FF00; - color: #000000; - border-color: #00FF00; - transition: background-color 0.3s, color 0.3s, border-color 0.3s; -} - -a.user-link:hover { - background-color: #000000; - border-color: #00FF00; - color: #00FF00; - cursor: pointer; -} - -a.user-link:focus { - background-color: #00FF00; - border-color: #00FF00; - color: #000000; -} - -.date-link { - background-color: #00FF00; - color: #000000; -} - -.date-link:hover { - background-color: #000000; - color: #00FF00; -} - -.activitySpreadInhabitant2 { - background-color: #1A1A1A; - color: #00FF00; -} - -.activityVotePost { - background-color: #00FF00; - color: #000000; -} - -.update-banner { - background-color: #001a00; - border-bottom-color: #003300; - color: #00FF00; -} - -.update-banner-link { - color: #00FF00; -} - -.snh-invite-code { - color: #00FF00 !important; -} - -.stats-kpi-label { color: #00FF00 !important; } -.stats-kpi-value { color: #00FF00 !important; } -.carbon-bar-note, .carbon-bar-formula { color: #00FF00 !important; } -.graphos-node-label { fill: #00FF00 !important; } -.graphos-node-label-me { fill: #ffa500 !important; font-weight: bold !important; } -.graphos-legend { color: #00FF00 !important; } -.graphos-edge { stroke: #003300 !important; } -/* Blockexplorer */ -.blockchain-view { background-color: #000000 !important; color: #00FF00 !important; } -.block { background: #1A1A1A !important; border: 1px solid #00FF00 !important; } -.block:hover { box-shadow: 0 0 15px rgba(0,255,0,0.3) !important; } -.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #00FF00 !important; } -.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #00FF00 !important; } -.block-content-preview, .block-content { background: #1A1A1A !important; color: #00FF00 !important; } -.block-author { color: #00FF00 !important; background: rgba(0,255,0,0.08) !important; } -.block-author:hover { color: #00FF00 !important; } -.block-url { color: #00FF00 !important; } -.block-row--details .block-url { background: #1A1A1A !important; } -.block-row--details .block-url:hover { background: #00FF00 !important; color: #000000 !important; } -.btn-singleview { background: #000000 !important; color: #00FF00 !important; border: 1px solid #00FF00 !important; } -.btn-singleview:hover { background: #00FF00 !important; color: #000000 !important; } -.btn-spread-on, .content-actions .btn-pin-on { background: #00FF00 !important; color: #000000 !important; } -.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #66FF66 !important; color: #000000 !important; } -.btn-back { background: #000000 !important; color: #00FF00 !important; border: 1px solid #00FF00 !important; } -.btn-back:hover { background: #00FF00 !important; color: #000000 !important; } -.block-info-table td, .pm-info-table td { border-color: #00FF00 !important; } -.block-diagram { border-color: #00FF00 !important; background: #000000 !important; } -.block-diagram-ruler { color: #00FF00 !important; background: #000000 !important; border-bottom-color: #00FF00 !important; } -.block-diagram-ruler span { color: #00FF00 !important; } -.block-diagram-cell { border-color: #00FF00 !important; background: #1A1A1A !important; } -.bd-label { color: #00FF00 !important; } -.bd-value { color: #00FF00 !important; } -.deleted-label { color: #ff3333 !important; } - -/* Tribes */ -.tribe-card { background: #1A1A1A !important; border-color: #00FF00 !important; } -.tribe-card:hover { box-shadow: 0 0 15px rgba(0,255,0,0.3) !important; } -.tribe-card-title { color: #00FF00 !important; } -.tribe-card-description { color: #00FF00 !important; } -.tribe-info-table td { border-color: #00FF00 !important; } -.tribe-info-label { color: #00FF00 !important; background: #1A1A1A !important; } -.tribe-info-value { color: #00FF00 !important; background: #1A1A1A !important; } -.tribe-info-empty { color: #006600 !important; } -.tribe-card-members { border-color: #00FF00 !important; background: #1A1A1A !important; } -.tribe-members-count { color: #00FF00 !important; } -.tribe-card-actions { border-color: #00FF00 !important; background: #1A1A1A !important; } -.tribe-action-btn { border-color: #00FF00 !important; color: #00FF00 !important; } -.tribe-action-btn:hover { background: #00FF00 !important; color: #000 !important; } -.tribe-subtribe-link { background: #000000 !important; border-color: #00FF00 !important; color: #00FF00 !important; } -.tribe-thumb-link { border-color: #00FF00 !important; } -.tribe-thumb-link:hover { box-shadow: 0 0 10px rgba(0,255,0,0.3) !important; } -.tribe-subtribe-link:hover { background: #00FF00 !important; color: #000 !important; } -.tribe-parent-image { border-color: #00FF00 !important; } -.tribe-parent-box { background: #1A1A1A !important; } - -.oasis-nav-header { color: #00FF00 !important; border-color: #00FF00 !important; background: #000000 !important; } -.oasis-nav-header:hover { color: #000000 !important; background: #00FF00 !important; } -.oasis-nav-header .emoji, .oasis-nav-header .nav-text, .oasis-nav-arrow { color: inherit !important; } -.oasis-nav-list li a { color: #00FF00 !important; } -.oasis-nav-list li a:hover { color: #00FF00 !important; opacity: 1 !important; } -.oasis-nav-list .emoji, .oasis-nav-list .nav-text { color: inherit !important; } -nav, .sidebar-left nav, .sidebar-right nav, .sidebar-left ul, .sidebar-right ul { color: #00FF00 !important; } -.sidebar-left a, .sidebar-right a { color: #00FF00 !important; } - -.ai-ask-form .ai-ask-input, .ai-ask-form .ai-ask-btn { color: #00FF00 !important; border-color: #00FF00 !important; background: #000000 !important; } -.ai-ask-form .ai-ask-input::placeholder { color: rgba(0,255,0,0.55) !important; } -.ai-ask-form .ai-ask-input:focus { border-color: #00FF00 !important; background: #1A1A1A !important; box-shadow: 0 0 6px rgba(0,255,0,0.4) !important; } -.ai-ask-form .ai-ask-btn:hover { background: #00FF00 !important; color: #000000 !important; border-color: #00FF00 !important; } -.ai-nav-results, .ai-nav-result-card { color: #00FF00 !important; border-color: #00FF00 !important; background: #000000 !important; } -.ai-nav-result-card .card-label { color: #00FF00 !important; } -.ai-nav-result-card a { color: #00FF00 !important; } -.ai-nav-result-card a:hover { background: #00FF00 !important; color: #000000 !important; } -.ai-nav-query { color: #00FF00 !important; } -.trending-card.own-content { box-shadow: inset 3px 0 0 #00FF00; } diff --git a/src/client/assets/themes/OasisMobile.css b/src/client/assets/themes/OasisMobile.css index 4e93351..7726265 100644 --- a/src/client/assets/themes/OasisMobile.css +++ b/src/client/assets/themes/OasisMobile.css @@ -1,1115 +1,1005 @@ -html { overflow-x: clip; overflow-y: visible; height: auto; } -body { overflow-x: clip; overflow-y: visible; } - - -.oasis-bottombar { - display: flex; - position: fixed; - left: 0; right: 0; bottom: 0; - z-index: 1000; - justify-content: space-around; - align-items: center; - gap: 4px; - background: #1F1F1F; - border-top: 1px solid #333; - padding: 8px 6px calc(8px + env(safe-area-inset-bottom, 0px)); +body { + background-color: #121212; + color: #FFD700; } -.oasis-bottombar .bb-item { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - gap: 3px; - flex: 1 1 0; - min-width: 0; - padding: 2px 0; + +header, footer { + background-color: #1F1F1F; +} + +.sidebar-left, .sidebar-right { + background-color: #1A1A1A; + border: 1px solid #333; +} + +.main-column { + background-color: #1C1C1C; +} + +button, input[type="submit"], input[type="button"] { + background-color: #444; + color: #FFD700; + border: 1px solid #444; +} + +button:hover, input[type="submit"]:hover, input[type="button"]:hover { + background-color: #333; + border-color: #666; +} + +input, textarea, select { + background-color: #333; + color: #FFD700; + border: 1px solid #555; +} + +a { + color: #FFD700; +} + +a:hover { + color: #FFDD44; +} + +table { + background-color: #222; + color: #FFD700; +} + +table th { + background-color: #333; +} + +table tr:nth-child(even) { + background-color: #2A2A2A; +} + +nav ul li a:hover { + color: #FFDD44; + text-decoration: underline; +} + +.profile { + background-color: #222; + padding: 15px; + border-radius: 8px; +} + +.profile .name { + color: #FFD700; +} + +.avatar { + border: 3px solid #FFD700; +} + +article, section { + background-color: #1C1C1C; + color: #FFD700; +} + +.article img { + border: 3px solid #FFD700; +} + +.post-preview img { + border: 3px solid #FFD700; +} + +.post-preview .image-container { + max-width: 100%; + overflow: hidden; + display: block; + margin: 0 auto; +} + +div { + background-color: #1A1A1A; + border: 1px solid #333; +} + +div .header-content { + width: 100%; +} + +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-thumb { + background-color: #444; + border-radius: 10px; +} + +::-webkit-scrollbar-track { + background-color: #222; +} + +.action-container { + background-color: #1A1A1A; + border: 1px solid #333; + padding: 10px; + border-radius: 8px; + color: #FFD700; +} + +footer { + background-color: #1F1F1F; + padding: 10px 0; +} + +footer a { + background-color: #444; + color: #FFD700; + text-align: center; + padding: 8px 16px; + border-radius: 5px; text-decoration: none; } -.oasis-bottombar .bb-ico { font-size: 18px; line-height: 1; color: #FFD700; } -.oasis-bottombar .bb-lbl { font-size: 9px; color: #FFD700; white-space: nowrap; } -.oasis-bottombar .bb-badge { - position: absolute; - top: -4px; - left: calc(50% + 6px); - min-width: 15px; height: 15px; - padding: 0 3px; - border-radius: 8px; - background: #FFD700; - color: #121212; - font-size: 9px; font-weight: bold; - display: flex; align-items: center; justify-content: center; + +footer a:hover { + background-color: #333; + color: #FFDD44; } -body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); } - -/* topbar FIJA arriba: [logo] [Personal] [Community] [avatar]; el hive scrollea por debajo */ -.oasis-mobile-topbar { - position: fixed; top: 0; left: 0; right: 0; z-index: 100; - display: flex; align-items: center; gap: 8px; - padding: 8px 12px; - background: #121212; - border-bottom: 1px solid #333; -} -body { padding-top: 84px; background: #121212; } - -.oasis-mobile-topbar .omt-logo, .oasis-mobile-topbar .omt-avatar { flex: 0 0 auto; } -.oasis-mobile-topbar .omt-logo img, -.oasis-mobile-topbar .omt-avatar img { - width: 42px; height: 42px; - border-radius: 50%; - border: 2px solid #FFD700; - object-fit: cover; display: block; +.card { + border-radius: 16px; + padding: 0px 24px 10px 24px; + margin-bottom: 16px; + color: #FFD600; + font-family: inherit; + box-shadow: 0 2px 20px 0 #FFD60024; } -.oasis-mobile-topbar .omt-menus { - flex: 1 1 auto; min-width: 0; - display: flex; justify-content: center; gap: 8px; -} -.oasis-mobile-topbar .omt-menus .oasis-menu-btn { - display: flex; align-items: center; justify-content: center; - padding: 7px 14px; - border: 1px solid #FFD700; border-radius: 8px; - background: #161616; color: #FFD700; - font-weight: 600; letter-spacing: .5px; text-decoration: none; - font-size: 13px; white-space: nowrap; cursor: pointer; -} -.oasis-mobile-topbar .omt-menus .oasis-menu-btn.active { - background: #FFD700; color: #121212; border-color: #FFD700; -} -.oasis-mobile-topbar .omt-menus .oasis-menu-btn span { text-decoration: none; } - -/* ANULAR el "card" global de style.css `div{background:#222;border-radius:10px;box-shadow;padding:20px;margin-bottom:20px}` - en nuestro chrome móvil propio: era el 2º color (caja mas clara) detras de los botones, - la caja redondeada de la topbar y el fondo #222 detras de los hexagonos. */ -.header, .hive-nav, .hive-grid, .hive-cats, .hive-row, -.oasis-mobile-topbar, .oasis-mobile-topbar .omt-menus { - background-color: transparent; box-shadow: none; border-radius: 0; -} -.oasis-mobile-topbar { background-color: #121212; } /* fija: fondo opaco (el contenido scrollea por debajo) */ -.oasis-mobile-topbar .omt-menus { padding: 0; margin: 0; } /* sin caja ni relleno detras de los botones */ - -.header .top-bar-left > a.logo-icon { display: none; } - -.oasis-mobile-menus { display: flex; gap: 8px; padding: 0 10px 6px; } -.oasis-mobile-menus .oasis-menu-ico { display: none; } -.oasis-mobile-menus .oasis-menu-btn { - flex: 1 1 0; - display: flex; align-items: center; justify-content: center; - padding: 10px 12px; - border: 1px solid #FFD700; border-radius: 8px; - background: #161616; color: #FFD700; - font-weight: 600; letter-spacing: 1px; text-decoration: none; cursor: pointer; -} -.oasis-mobile-menus .oasis-menu-btn span { text-decoration: none; } - -.oasis-mobile-menus .oasis-menu-btn.active { - background: #FFD700; color: #121212; border-color: #FFD700; +.card-section { + border:none; + padding: 10px 0 0 16px; } -:root { - --space-1: 4px; --space-2: 8px; --space-3: 16px; --space-4: 24px; --space-5: 32px; - --fs-caption: 12px; --fs-body: 14px; --fs-body-lg: 16px; --fs-title: 20px; - --lh-body: 1.5; --measure: 66ch; - --c-muted: #8c8c8c; --radius-md: 12px; -} - -.oasis-bottombar .bb-fab .bb-ico { - width: 46px; height: 46px; - margin-top: -22px; - border-radius: 50%; - background: #FFD700; color: #121212; - display: flex; align-items: center; justify-content: center; - font-size: 22px; - box-shadow: 0 2px 8px rgba(0,0,0,.4); -} -.oasis-bottombar .bb-fab .bb-lbl { color: #FFD700; font-weight: bold; } - -.main-column, .main-column p, .main-column li, .main-column a, -.main-column span, .main-column td, .main-column h1, .main-column h2, .main-column h3 { - overflow-wrap: anywhere; - word-break: break-word; -} -.main-column { padding: var(--space-3); } -.main-column p { line-height: var(--lh-body); } - -.main-column > * + * { margin-top: var(--space-3); } - -.main-column img, .main-column video, .main-column table, .main-column pre, .main-column iframe { - max-width: 100%; height: auto; -} -.main-column pre { white-space: pre-wrap; } - -.oasis-nav-group { - list-style: none; - background: #1c1c1c; - border: 1px solid #333; - border-radius: var(--radius-md); - overflow: hidden; - margin-bottom: var(--space-2); -} -.oasis-nav-header { - display: flex; align-items: center; gap: var(--space-2); - min-height: 48px; padding: 0 var(--space-3); - cursor: pointer; - color: #FFD700; font-weight: bold; - text-transform: uppercase; letter-spacing: .04em; - font-size: var(--fs-caption); -} -.oasis-nav-header .nav-text { flex: 1 1 auto; } -.oasis-nav-header .oasis-nav-arrow { color: var(--c-muted); transition: transform 150ms ease; } -.oasis-nav-toggle:checked + .oasis-nav-header .oasis-nav-arrow { transform: rotate(180deg); } -.oasis-nav-header:active { background: #262626; } -.oasis-nav-list { list-style: none; padding: 0; margin: 0; } -.oasis-nav-list li a { - display: flex; align-items: center; gap: var(--space-3); - min-height: 48px; padding: 0 var(--space-3); - color: #FFD700; text-decoration: none; - border-top: 1px solid #262626; -} -.oasis-nav-list li a .emoji { flex: 0 0 auto; width: 22px; text-align: center; color: #FFD700; } -.oasis-nav-list li a .nav-text { font-size: var(--fs-body); } -.oasis-nav-list li a:active { background: #262626; } -.oasis-nav-list li a.current { color: #FFD700; font-weight: bold; } - -.main-column h1 { font-size: var(--fs-title); font-weight: 700; line-height: 1.25; margin: 0 0 var(--space-2); } -.main-column h2 { font-size: 18px; font-weight: 700; line-height: 1.3; margin: 0 0 var(--space-2); } -.main-column h3 { font-size: var(--fs-body-lg); font-weight: 600; margin: 0 0 var(--space-1); } -.main-column p { font-size: var(--fs-body-lg); } -.main-column .created-at, .main-column .small, .main-column .time, -.main-column .meta, .main-column .author-meta, .main-column .subtitle { - color: var(--c-muted); font-size: var(--fs-caption); -} - -.main-column article, -.main-column [class*="-card"] { - background: #1c1c1c; - border: 1px solid #333; - border-radius: var(--radius-md); - padding: var(--space-3); -} -.main-column article:active, -.main-column [class*="-card"]:active { background: #202020; } - -.btn, button, input[type="submit"], input[type="button"], .submit-button, -.filter-btn, .create-button, .edit-btn, .delete-btn, .join-btn, .leave-btn, .buy-btn { - min-height: 48px; -} -button:active, input[type="submit"]:active, .submit-button:active, -.filter-btn:active, .create-button:active, .edit-btn:active, -.delete-btn:active, .join-btn:active, .leave-btn:active, .buy-btn:active { - filter: brightness(.85); -} -.main-column a:active { opacity: .6; } - -* { -webkit-tap-highlight-color: transparent; } -a, button, label, .tap, .oasis-menu-btn, .bb-item, .oasis-nav-header, .oasis-nav-list li a { - touch-action: manipulation; -} -body { overscroll-behavior: none; } - -.main-content, .main-column, .header { overflow: visible; } - -/* === Usabilidad 0.9.1: Personal/Community filtran el hive server-side (enlaces ?hive=) + desplegable FILTER === */ - -/* desplegable FILTER de la ristra de filtros de Activity (oculto por defecto) */ -.activity-filter-toggle { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; margin: 0; } -.activity-filter-btn { - display: flex; align-items: center; justify-content: center; gap: 8px; - width: 100%; min-height: 44px; margin: 4px 0 8px; padding: 10px 12px; - border: 1.5px solid #333; border-radius: 10px; - background: #121212; color: #FFD700; - font-weight: bold; letter-spacing: 1px; text-transform: uppercase; cursor: pointer; -} -.activity-filter-caret { transition: transform 150ms ease; } -#activity-filter-toggle:checked ~ .activity-filter-btn { background: #FFD700; color: #121212; border-color: #FFD700; } -#activity-filter-toggle:checked ~ .activity-filter-btn .activity-filter-caret { transform: rotate(180deg); } -.activity-filter-panel { display: none; } -#activity-filter-toggle:checked ~ .activity-filter-panel { display: block; } -.activity-filter-panel .activity-filter-grid { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 8px; } -.activity-filter-panel .activity-filter-col { display: contents; } -.activity-filter-panel form { margin: 0; } - -.bb-manage { margin: 8px 0 16px; } -.bb-manage h3, .bb-manage-add h3 { - font-size: 13px; text-transform: uppercase; letter-spacing: .05em; - color: #FFD700; margin: 14px 0 8px; -} -.bb-manage-list { display: flex; flex-direction: column; gap: 8px; } -.bb-manage-form { margin: 0; width: 100%; } -.bb-manage-btn { - width: 100%; - display: flex; align-items: center; gap: 12px; - min-height: 52px; padding: 0 14px; - background: #1c1c1c; border: 1px solid #333; border-radius: 10px; - color: #FFD700; cursor: pointer; text-align: left; -} -.bb-manage-ico { font-size: 20px; color: #FFD700; width: 26px; text-align: center; } -.bb-manage-lbl { flex: 1 1 auto; font-size: 15px; } -.bb-manage-x { font-size: 16px; color: #FFD700; font-weight: bold; } -.bb-manage-empty, .bb-manage-full { color: #999; font-size: 14px; padding: 8px 2px; } -.bb-manage-done { - display: block; text-align: center; - margin-top: 16px; min-height: 48px; line-height: 48px; -} - -.hive-nav { --hx: min(64px, calc((100vw - 40px) / 4)); --hxm: min(58px, calc((100vw - 36px) / 5)); margin: 8px 0 16px; overflow: visible; background: transparent; padding: 0; border: 0; box-shadow: none; } -.hive-radio { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; } -.hive-grid { display: flex; flex-direction: column; align-items: center; overflow: visible; background: transparent; padding: 0; margin: 0; box-shadow: none; } -.hive-row { display: flex; justify-content: center; gap: 3px; overflow: visible; background: transparent; padding: 0; margin: 0; box-shadow: none; } -.hive-cats .hive-row + .hive-row { margin-top: calc(var(--hx) * -0.30); } -.hex-cat, .hex-mod { - position: relative; - -webkit-clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); - clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); - display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; - text-decoration: none; cursor: pointer; box-sizing: border-box; - background: #FFD700; - filter: drop-shadow(0 1px 2px rgba(0,0,0,.6)); -} -.hex-cat::before, .hex-mod::before { - content: ''; position: absolute; inset: 1.5px; z-index: 0; - -webkit-clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); - clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); - background: #161616; -} -.hex-cat { width: var(--hx); height: calc(var(--hx) * 1.15); } -.hex-mod { width: var(--hxm); height: calc(var(--hxm) * 1.15); } -.hex-cat .hx-ico, .hex-mod .hx-ico { color: #FFD700; line-height: 1; position: relative; z-index: 1; } -.hex-cat .hx-ico { font-size: 17px; } -.hex-mod .hx-ico { font-size: 16px; } -.hex-cat .hx-lbl, .hex-mod .hx-lbl { color: #FFD700; text-align: center; line-height: 1; font-weight: bold; overflow: hidden; position: relative; z-index: 1; } -.hex-cat .hx-lbl { font-size: 8px; max-width: 88%; } -.hex-mod .hx-lbl { font-size: 8px; max-width: 92%; } -.hex-cat:active::before, .hex-mod:active::before { background: #242424; } -#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"], -#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"], -#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"], -#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"], -#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"], -#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"], -#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"], -#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"], -#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"], -#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"] { background: #FFD700; } -#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"]::before, -#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"]::before, -#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"]::before, -#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"]::before, -#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"]::before, -#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"]::before, -#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"]::before, -#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"]::before, -#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"]::before, -#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"]::before { background: #FFD700; } -#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"] :is(.hx-ico,.hx-lbl), -#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"] :is(.hx-ico,.hx-lbl), -#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"] :is(.hx-ico,.hx-lbl), -#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"] :is(.hx-ico,.hx-lbl), -#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"] :is(.hx-ico,.hx-lbl), -#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"] :is(.hx-ico,.hx-lbl), -#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"] :is(.hx-ico,.hx-lbl), -#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"] :is(.hx-ico,.hx-lbl), -#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"] :is(.hx-ico,.hx-lbl), -#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"] :is(.hx-ico,.hx-lbl) { color: #121212; } -.hive-panel { display: none; margin: 12px auto 0; max-width: 340px; background: #101010; border: 1px solid #242424; border-radius: 16px; padding: 12px 8px; overflow: visible; box-shadow: none; } -#hive-personal:checked ~ #hive-panel-personal, -#hive-governance:checked ~ #hive-panel-governance, -#hive-office:checked ~ #hive-panel-office, -#hive-economy:checked ~ #hive-panel-economy, -#hive-tools:checked ~ #hive-panel-tools, -#hive-network:checked ~ #hive-panel-network, -#hive-blogs:checked ~ #hive-panel-blogs, -#hive-creative:checked ~ #hive-panel-creative, -#hive-media:checked ~ #hive-panel-media, -#hive-fediverse:checked ~ #hive-panel-fediverse { display: block; } -.hive-mods { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px; margin: 0; padding: 0; background: transparent; overflow: visible; } - -.header .top-bar-left nav, -.header .top-bar-right nav { display: none; } - -.oasis-mobile-topbar { padding-top: calc(env(safe-area-inset-top, 0px) + 10px); } - -.oasis-bottombar .bb-pair { - display: flex; align-items: center; - flex: 0 0 auto; - gap: 2px; padding: 3px; - background: #262626; - border: 1px solid #3a3a3a; - border-radius: 999px; -} -.oasis-bottombar .bb-pair .bb-item { flex: 0 0 auto; padding: 2px 8px; } -.oasis-bottombar .bb-pair .bb-lbl { font-size: 8px; } - -.oasis-bottombar a.bb-item.current .bb-ico { - background: #FFD700; color: #121212; - border-radius: 999px; padding: 4px 12px; -} -.oasis-bottombar a.bb-item.current .bb-lbl { color: #FFD700; font-weight: bold; } -.oasis-bottombar a.bb-item.current .bb-badge { background: #121212; color: #FFD700; } - -.main-column, .main-column p, .main-column li, .main-column a, -.main-column span, .main-column td, .main-column th, .main-column label, -.main-column h1, .main-column h2, .main-column h3, .main-column h4, -.main-column strong, .main-column em, .main-column small, -.created-at, .post-meta, .feed-post-meta, -.feed-row .small, .feed-row .time, .feed-row .created-at, -.profile-sensors-box > *, -.card-field .card-value, .card-field .card-label { - overflow-wrap: break-word; - word-break: normal; - -webkit-hyphens: none; - hyphens: none; -} - -.main-column pre, .main-column code, kbd, samp, -.peer-key, .user-link.peer-key, .oasis-id-copy, .search-oasis-id, -.pm-sent-key-value, .kpi__value, -.block-info-table td .peer-key, .block-info-table td .user-link, -.pm-info-table td .user-link, -.main-column a[href*="ed25519"], .main-column a[href^="ssb:"], -.main-column a[href^="/author/"] { - overflow-wrap: anywhere; - word-break: break-all; -} - -button, input[type="submit"], input[type="button"], -.filter-btn, a.filter-btn, .create-button, .edit-btn, .delete-btn, -.join-btn, .leave-btn, .buy-btn, .submit-button, .btn, -.conn-actions button, .peers-conn-actions button, -.mode-buttons button, .mode-buttons .filter-btn, -.filter-group button, .filter-group .filter-btn, -.main-column button, .main-column input[type="submit"] { - white-space: normal; - word-break: normal; - overflow-wrap: break-word; - -webkit-hyphens: none; - hyphens: none; -} - -.conn-actions, .peers-conn-actions { +.card-header { display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - align-items: stretch; + justify-content: space-between; + align-items: center; + padding-bottom: 0px; + margin-top: 8px; + padding-top: 0px; + border: none; +} + +.card-label { + color: #ffa300; + font-weight: bold; + letter-spacing: 1.5px; + line-height: 1.2; + margin-bottom: 0; +} + +.card-footer { + margin-top: 6px; + font-weight: 500; + color: #ff9900; + font-size: 1.07em; + display: flex; + align-items: center; gap: 8px; - width: 100%; - max-width: 100%; - overflow-x: hidden; + background: none; + border: none; + padding-top: 0; + margin-bottom: 6; } -.conn-actions > form, .peers-conn-actions > form { - flex: 0 1 auto; - width: auto; - min-width: 0; - margin: 0; + +.card-body { + margin-top: 0; + margin-bottom: 4; + padding: 0; +} + +.card-field { display: flex; + align-items: baseline; + padding: 0; + margin-bottom: 0; + border: none; + background: none; } -.pm-info-table, .pm-info-table tbody, -.block-info-table, .block-info-table tbody { - display: block; - width: 100%; - max-width: 100%; - border: 0; - background: transparent; - box-shadow: none; - table-layout: auto; -} -.pm-info-table thead, .block-info-table thead { display: none; } -.pm-info-table tr, .block-info-table tr { - display: block; - width: 100%; - box-sizing: border-box; - border: 0; - border-bottom: 1px solid rgba(255,215,0,0.18); - border-radius: 0; - margin: 0 0 8px 0; - padding: 6px 0; - background: transparent; -} -.pm-info-table td, .block-info-table td { - display: block; - width: 100%; - max-width: 100%; - box-sizing: border-box; - border: 0; - background: transparent; - padding: 1px 0; - text-align: left; - overflow-wrap: break-word; - word-break: normal; +.card-tags { + margin: 5px 0 3px 0; + display: flex; + flex-wrap: wrap; + gap: 9px; } -.pm-info-table td.card-label, .block-info-table td.card-label { - color: var(--c-muted, #8c8c8c); - font-size: 11px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: .03em; - margin-bottom: 2px; - padding-bottom: 0; -} -.pm-info-table td.card-value, .block-info-table td.card-value { - font-size: 15px; - line-height: 1.5; - margin-bottom: 6px; +.card-tags a.tag-link { + text-decoration: none; + color: #181818; + background: #FFD600; + padding: 5px 13px 4px 13px; + border-radius: 7px; + font-size: .98em; + border: none; + font-weight: bold; } -.block-info-table tr:first-child:has(> td.card-label) { display: none; } -.block-info-table td[data-label]:not([data-label=""])::before { - content: attr(data-label); - display: block; - color: var(--c-muted, #8c8c8c); - font-size: 10px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: .03em; - margin-bottom: 1px; - white-space: nowrap; - word-break: normal; -} -.block-info-table td[data-label=""]::before { content: none; } - -.peers-list .block-info-table td, -.peers-technical-block .block-info-table td, -.gov-overview tbody td { - word-break: normal; - overflow-wrap: break-word; +.card-tags a.tag-link:hover { + background: #ffe86a; + color: #111; + cursor: pointer; } -.pm-info-table td .user-link, .pm-info-table td .peer-key, .pm-info-table td a[href], -.block-info-table td .peer-key, .block-info-table td .user-link, .block-info-table td a[href] { +a.user-link { + background-color: #FFA500; + color: #000; + padding: 8px 16px; + border-radius: 5px; + text-align: center; + font-weight: bold; + text-decoration: none; display: inline-block; - max-width: 100%; - overflow-wrap: anywhere; - word-break: break-all; + border: 2px solid transparent; + transition: background-color 0.3s, color 0.3s, border-color 0.3s; + font-size: 0.8em; } -.pm-info-table td form, .block-info-table td form, -.pm-info-table td button, .block-info-table td button { - width: 100%; - margin: 0; +a.user-link:hover { + background-color: #FFD700; + border-color: #FFD700; + color: #000; + cursor: pointer; } -.main-content { width: 100%; max-width: 100%; } +a.user-link:focus { + background-color: #007B9F; + border-color: #007B9F; + color: #fff; +} + +.date-link { + background-color: #444; + color: #FFD600; + padding: 8px 16px; + border-radius: 5px; + margin-left: 8px; +} + +.date-link:hover { + background-color: #555; + color: #FFD700; +} + +.activitySpreadInhabitant2 { + background-color: #007B9F; + color: #fff; + padding: 8px 16px; + border-radius: 5px; + font-weight: bold; + text-decoration: none; + display: inline-block; + border: 2px solid transparent; +} + +.activityVotePost { + background-color: #557d3b; + color: #fff; + padding: 8px 16px; + border-radius: 5px; + font-weight: bold; + text-decoration: none; + display: inline-block; + border: 2px solid transparent; +} +html { + -webkit-text-size-adjust: 100%; + box-sizing: border-box; + overflow-x: hidden !important; +} + +*, *::before, *::after { + box-sizing: inherit; +} + +body { + font-size: 15px; + overflow-x: hidden; + max-width: 100vw; +} + +img, +video, +canvas, +iframe, +table { + max-width: 100% !important; +} + +pre, +code { + white-space: pre-wrap !important; + word-break: break-word !important; + overflow-wrap: anywhere !important; +} + +.header { + display: flex !important; + flex-direction: column !important; + width: 100% !important; + max-width: 100% !important; + padding: 4px !important; + gap: 4px !important; + overflow: visible !important; +} + +.header-content { + display: flex !important; + flex-wrap: wrap !important; + width: 100% !important; + max-width: 100% !important; + padding: 2px !important; + gap: 4px !important; + overflow: visible !important; +} + +.top-bar-left, +.top-bar-mid, +.top-bar-right { + display: flex !important; + flex-direction: column !important; + width: 100% !important; + max-width: 100% !important; + align-items: stretch !important; + gap: 6px !important; +} + +.top-bar-left nav ul, +.top-bar-mid nav ul, +.top-bar-right nav ul { + display: flex !important; + flex-wrap: wrap !important; + gap: 6px !important; + padding: 0 !important; + margin: 0 !important; + overflow: visible !important; +} + +.top-bar-left nav ul li, +.top-bar-mid nav ul li, +.top-bar-right nav ul li { + margin: 0 !important; +} + +.top-bar-left nav ul li a, +.top-bar-mid nav ul li a, +.top-bar-right nav ul li a { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + padding: 6px 8px !important; + font-size: 0.9rem !important; + white-space: nowrap !important; +} + +.search-input, +.feed-search-input, +.activity-search-input { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: 40px !important; + font-size: 16px !important; +} + +.main-content { + display: flex !important; + flex-direction: column !important; + width: 100% !important; + max-width: 100% !important; + gap: 12px !important; +} + +.sidebar-left, +.sidebar-right, .main-column { - width: 100%; max-width: 100%; - padding: 16px; box-sizing: border-box; -} -.main-column .card-field { - flex-direction: column; - align-items: stretch; - gap: 2px; -} -.main-column .card-field .card-value { - width: 100%; max-width: 100%; font-size: 15px; -} -.main-column .card-field .card-label { - color: var(--c-muted, #8c8c8c); font-size: 11px; -} -.main-column img, .main-column video, .main-column iframe, .main-column canvas { - max-width: 100%; height: auto; -} -.main-column hr { - width: 100%; border: 0; - border-top: 1px solid #333; margin: 16px 0; + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + padding: 10px !important; + border-left: none !important; + border-right: none !important; } -.activity-filter-acc { width: 100%; margin-bottom: 8px; } - -.activity-quick-modes { - display: flex; flex-wrap: nowrap; gap: 6px; margin-bottom: 8px; -} -.activity-quick-modes form { flex: 1 1 0; min-width: 0; margin: 0; display: flex; } -.activity-quick-modes .filter-btn { - flex: 1 1 0; width: 100%; min-height: 44px; - white-space: nowrap; word-break: keep-all; overflow-wrap: normal; - padding-left: 6px; padding-right: 6px; font-size: 14px; +.sidebar-left { + order: 1 !important; } -.activity-groups { list-style: none; padding: 0; margin: 0; } - -.activity-groups .oasis-nav-header .emoji { display: none; } -.activity-groups .oasis-nav-header .nav-text { flex: 1 1 auto; } -.activity-groups .act-count { - flex: 0 0 auto; min-width: 20px; text-align: center; - font-size: 11px; font-weight: 700; color: #121212; - background: #FFD700; border-radius: 999px; padding: 1px 7px; - margin: 0 8px 0 0; +.main-column { + order: 3 !important; } -.activity-groups .oasis-nav-list li { list-style: none; margin: 0; padding: 0; } -.activity-groups .oasis-nav-list form { margin: 0; display: block; width: 100%; } -.activity-groups .oasis-nav-list .filter-btn { - display: block; width: 100%; text-align: left; - min-height: 44px; margin: 2px 0; box-sizing: border-box; -} -.activity-groups .oasis-nav-list .filter-btn.active { - outline: 2px solid #FFD700; outline-offset: -2px; +.sidebar-right { + order: 2 !important; } -.message-list { display: flex; flex-direction: column; gap: 4px; width: 100%; } -.message-list .pm-thread { display: flex; flex-direction: column; width: 100%; align-self: stretch; } -.pm-thread-replies { display: flex; flex-direction: column; gap: 4px; } - -.pm-card.pm-bubble { - max-width: 82%; width: fit-content; min-width: 0; - box-sizing: border-box; padding: 8px 12px; margin: 2px 0; - border-radius: 16px; border: 1px solid #333; - background: #1e1e1e; box-shadow: none; cursor: default; +.sidebar-left nav ul, +.sidebar-right nav ul { + display: flex !important; + flex-direction: column !important; } -.pm-card.pm-bubble.pm-recv { - align-self: flex-start; margin-left: 0; margin-right: auto; - border-bottom-left-radius: 4px; background: #1e1e1e; border-color: #343434; +.oasis-nav-header { + font-size: 0.85rem !important; } -.pm-card.pm-bubble.pm-sent { - align-self: flex-end; margin-left: auto; margin-right: 0; - border-bottom-right-radius: 4px; background: #2a2413; border-color: #5c4c1f; +.oasis-nav-list li a { + font-size: 0.9rem !important; + padding: 8px 12px !important; } -.pm-bubble .pm-bubble-sender { font-size: 11px; font-weight: 700; color: #FFD700; margin-bottom: 3px; } -.pm-bubble .pm-bubble-sender a { color: #FFD700; } - -.pm-bubble .message-text { font-size: 15px; line-height: 1.42; color: #FFD700; margin: 0; } -.pm-bubble .message-text a { color: #FFD700; } - -.pm-bubble .pm-bubble-meta { - display: flex; flex-wrap: wrap; align-items: center; - gap: 6px; margin-top: 4px; font-size: 10px; color: var(--c-muted, #8c8c8c); -} -.pm-card.pm-sent .pm-bubble-meta { justify-content: flex-end; } -.pm-card.pm-recv .pm-bubble-meta { justify-content: flex-start; } -.pm-bubble .pm-bubble-subject { font-style: italic; opacity: .75; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.pm-bubble .pm-bubble-lock { opacity: .85; } -.pm-bubble .pm-bubble-eco { font-size: 9px; opacity: .9; } -.pm-bubble .pm-bubble-eco * { font-size: 9px; } - -.pm-bubble .pm-actions { margin-top: 6px; display: flex; gap: 6px; } -.pm-card.pm-sent .pm-actions { justify-content: flex-end; } -.pm-bubble .pm-actions .pm-btn { font-size: 10px; padding: 3px 8px; min-height: 28px; } -.pm-bubble .pm-actions form { margin: 0; } - -.pm-bubble .pm-info-table { display: none; } - -.pm-thread .pm-thread-details { width: 100%; } -.pm-thread .pm-thread-toggle { font-size: 12px; color: var(--c-muted, #8c8c8c); padding: 4px 0; cursor: pointer; } - -.main-column h1, .main-column h2, .main-column h3, .main-column h4, -.tags-header h2, .tags-header h3, .title-with-chip h2, .pm-title, h2.pm-title, -.cn-name, h1.cn-name, .cn-section, h2.cn-section, .profile-side-name, h2.profile-side-name { - text-wrap: balance; - overflow-wrap: break-word; - word-break: keep-all; - -webkit-hyphens: none; hyphens: none; - text-align: left; -} -.main-column h1, .tags-header h2, .cn-name { - font-size: var(--fs-title, 20px); font-weight: 700; - line-height: 1.22; letter-spacing: -0.01em; - margin: 0 0 var(--space-2, 8px); -} -.main-column h2, .pm-title, .title-with-chip h2, .cn-section { - font-size: 18px; font-weight: 700; - line-height: 1.28; margin: var(--space-1, 4px) 0 var(--space-2, 8px); -} -.main-column h3 { - font-size: var(--fs-body-lg, 16px); font-weight: 600; - line-height: 1.35; margin: var(--space-1, 4px) 0; -} -.main-column h1, .main-column h2, .main-column h3, -.tags-header h2, .pm-title, .cn-name, .cn-section { - max-width: 40ch; width: auto; min-width: 0; +button, +input[type="submit"], +input[type="button"], +.filter-btn, +.create-button, +.edit-btn, +.delete-btn, +.join-btn, +.leave-btn, +.buy-btn { + min-height: 44px !important; + font-size: 16px !important; + white-space: normal !important; + text-align: center !important; } -.main-column h1 code, .main-column h2 code, .main-column h3 code, -.tags-header h2 code, .pm-title code, -.main-column h1 a[href*="ed25519"], .main-column h2 a[href*="ed25519"], -.main-column h1 a[href^="ssb:"], .main-column h2 a[href^="ssb:"] { - word-break: break-all; overflow-wrap: anywhere; +.feed-row, +.comment-body-row, +table { + display: block !important; + width: 100% !important; + overflow-x: auto !important; } -.main-column .tags-header { - display: flex; flex-direction: column; align-items: flex-start; - gap: var(--space-1, 4px); width: 100%; max-width: 100%; - margin: 0 0 var(--space-2, 8px); -} -.main-column .tags-header h2 { margin: 0; } - -.main-column .title-with-chip { - display: flex; flex-wrap: wrap; align-items: center; - gap: var(--space-2, 8px); width: 100%; max-width: 100%; -} -.main-column .title-with-chip h2 { flex: 1 1 auto; min-width: 0; margin: 0; max-width: none; } -.main-column .title-with-chip > :not(h2) { flex: 0 0 auto; } - -.main-column p, .main-column li, -.tags-header p, .tags-header .prefs-help, .indexing-note, .indexing-percent, -.error-page-message, .welcome-text, .message-text, .pm-bubble .message-text { - text-wrap: pretty; - line-height: 1.55; - max-width: min(66ch, 100%); - overflow-wrap: anywhere; - word-break: normal; - min-width: 0; - -webkit-hyphens: none; hyphens: none; -} -.main-column p + p { margin-top: var(--space-3, 16px); } -.tags-header p, .tags-header .prefs-help, .indexing-note { line-height: 1.5; margin: 4px 0 0 0; } -.pm-bubble .message-text { line-height: 1.45; max-width: 100%; } - -[style*="auto-fill"], -[style*="minmax(240px"], [style*="minmax(280px"], [style*="minmax(300px"], -[style*="minmax( 240px"], [style*="minmax( 280px"], [style*="minmax( 300px"], -.trending-container, .cn-hub-grid, .cn-products { - display: grid !important; grid-template-columns: 1fr !important; gap: 12px !important; - width: 100% !important; max-width: 100% !important; -} -.trending-container > *, .games-grid > *, .market-grid > *, .tribe-grid > *, -.feed-container, .feed-card, .trending-card, -.main-column article, .main-column [class*="-card"], .main-column [class*="-item"], -.main-column [class*="-entry"], .main-column .card { - width: 100%; max-width: 100%; min-width: 0; - box-sizing: border-box; margin-left: 0; margin-right: 0; -} -.feed-card .feed-text, .trending-card .feed-text, -.main-column article p, .main-column [class*="-card"] p { max-width: var(--measure, 66ch); } -.main-column form, .comment-form, .comment-form-wrapper, .market-form, -.new-message-form, .project-control-form, .filter-box { - display: block; width: 100%; max-width: 100%; - min-width: 0; box-sizing: border-box; -} -.main-column input:not([type="checkbox"]):not([type="radio"]):not(.oasis-nav-toggle):not(.oasis-nav-side-toggle), -.main-column textarea, .main-column select { - width: 100%; max-width: 100%; min-width: 0; box-sizing: border-box; -} -.main-column form > button:only-of-type, .main-column form > .create-button, -.comment-form button[type="submit"], .market-form button[type="submit"], -.new-message-form button[type="submit"] { - display: block; width: 100%; max-width: 100%; - box-sizing: border-box; margin-top: 8px; -} -[style*="width:33%"], [style*="width: 33%"], [style*="width:25%"], [style*="width: 25%"], -[style*="width:60%"], [style*="width: 60%"], [style*="width:40%"], [style*="width: 40%"] { - width: 100% !important; max-width: 100% !important; -} -.main-column [style*="max-width: 800px"], .main-column [style*="max-width:800px"], -.ai-response, .ai-train-bar { - max-width: 100% !important; width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; box-sizing: border-box !important; -} -.message-list, .pm-thread, .pm-thread-replies { width: 100%; max-width: 100%; box-sizing: border-box; } - -.main-column p .user-link, .main-column p a[href^="/author/"], -.main-column li .user-link, .main-column span .user-link, -.main-column .message-text .user-link, .main-column .message-text a[href^="/author/"], -.pm-bubble .message-text .user-link, .pm-bubble .message-text a[href^="/author/"], -.pm-quote a[href^="/author/"] { - display: inline; white-space: normal; - overflow-wrap: anywhere; word-break: keep-all; - -webkit-hyphens: none; hyphens: none; +textarea, +input, +select { + width: 100% !important; + max-width: 100% !important; + font-size: 16px !important; } -.peer-key, .user-link.peer-key, .block-info-table td .peer-key, .pm-info-table td .peer-key, -.pm-info-table td .user-link, .block-info-table td .user-link, -.search-oasis-id, .pm-sent-key-value, .kpi__value { - display: inline-block; max-width: 100%; - overflow-wrap: anywhere; word-break: break-all; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-variant-numeric: tabular-nums; line-height: 1.45; +.gallery { + display: grid !important; + grid-template-columns: 1fr 1fr !important; + gap: 8px !important; } -.oasis-id-copy { - display: block; width: 100%; white-space: nowrap; - overflow: hidden; text-overflow: ellipsis; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; -} -.invites-list .user-link.peer-key, .peers-list .block-info-table td .peer-key { - display: block; max-width: 100%; white-space: nowrap; - overflow: hidden; text-overflow: ellipsis; word-break: normal; +footer, +.footer { + display: block !important; + width: 100% !important; + max-width: 100% !important; + padding: 12px !important; + overflow-x: auto !important; } -.main-column .message-text a[href^="http"], .main-column p a[href^="http"], -.pm-bubble .message-text a[href^="http"], .main-column .message-text a.oasis-path-link, -.main-column .message-text a.job-link, .main-column .message-text a.project-link, -.main-column .message-text a.market-link { - overflow-wrap: anywhere; word-break: keep-all; - -webkit-hyphens: none; hyphens: none; -} -.main-column code:not(pre code), .pm-bubble .message-text code { - display: inline; max-width: 100%; - overflow-wrap: anywhere; word-break: break-word; white-space: pre-wrap; +footer div { + display: flex !important; + flex-direction: column !important; + gap: 8px !important; } -.main-column .tags-header { margin-bottom: 6px; gap: 2px; } -.main-column .tags-header h2, .tags-header h2 { - font-size: 17px; line-height: 1.2; letter-spacing: 0; -} -.main-column .tags-header p { font-size: 13px; line-height: 1.35; margin: 0; } +h1 { font-size: 1.35em !important; } +h2 { font-size: 1.2em !important; } +h3 { font-size: 1em !important; } -.main-column { padding: 10px 12px 16px 12px; } -.activity-filter-acc { margin-bottom: 4px; } - -.empty-state { - display: flex; flex-direction: column; align-items: center; - justify-content: center; text-align: center; - padding: 40px 16px; gap: 8px; width: 100%; box-sizing: border-box; -} -.empty-state-icon { font-size: 44px; line-height: 1; opacity: 0.55; } -.empty-state-title { font-size: 16px; font-weight: 700; margin: 4px 0 0 0; color: #FFD700; } -.empty-state-text { color: var(--c-muted, #8c8c8c); font-size: 14px; line-height: 1.4; max-width: 40ch; margin: 0; } -.empty-state-cta { margin-top: 10px; min-height: 48px; display: inline-flex; align-items: center; } - -.activity-filter-collapsed { - display: flex; align-items: center; gap: 10px; - flex-wrap: wrap; margin-bottom: 8px; width: 100%; -} -.activity-back { - display: inline-flex; align-items: center; gap: 4px; - min-height: 44px; padding: 6px 14px 6px 10px; flex: 0 0 auto; -} -.activity-back-arrow { font-size: 22px; line-height: 1; font-weight: bold; } -.activity-active-filter { - font-weight: 700; color: #FFD700; text-transform: uppercase; - font-size: 13px; letter-spacing: .03em; +.small, +.time { + font-size: 0.8rem !important; } -.oasis-bottombar .bb-item { min-height: 48px; } -.oasis-bottombar .bb-pair .bb-item { min-height: 44px; padding: 4px 10px; } -.oasis-mobile-menus .oasis-menu-btn { min-height: 48px; } -.pm-bubble .pm-actions .pm-btn { min-height: 40px; padding: 6px 12px; } -.score-btn, .vote-btn, .feed-action-btn { - min-height: 44px; min-width: 44px; - display: inline-flex; align-items: center; justify-content: center; +.created-at { + display: block !important; + width: 100% !important; + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: anywhere !important; + line-height: 1.5 !important; + font-size: 0.8rem !important; } -.score-btn:active, .vote-btn:active, .feed-action-btn:active { filter: brightness(.85); } -.oasis-bottombar .bb-add .bb-ico { - background: #FFD700; color: #121212; border-radius: 999px; - width: 30px; height: 30px; display: inline-flex; - align-items: center; justify-content: center; - font-weight: bold; font-size: 20px; line-height: 1; +.header-content .created-at { + display: block !important; + flex: 1 1 100% !important; + min-width: 0 !important; + max-width: 100% !important; } -.bb-pick-grid { - display: grid; grid-template-columns: repeat(3, 1fr); - gap: 8px; width: 100%; margin-top: 8px; -} -.bb-pick-form { margin: 0; display: block; } -.bb-pick-btn { - display: flex; flex-direction: column; align-items: center; - justify-content: center; gap: 6px; width: 100%; - min-height: 76px; padding: 10px 6px; box-sizing: border-box; - background: #1c1c1c; border: 1px solid #333; border-radius: 12px; color: #FFD700; -} -.bb-pick-btn.active { border-color: #FFD700; background: #2a2412; } -.bb-pick-ico { font-size: 26px; color: #FFD700; line-height: 1; } -.bb-pick-lbl { font-size: 11px; text-align: center; line-height: 1.2; } +.post-meta, +.feed-post-meta, +.feed-row .small, +.feed-row .time, +.feed-row .created-at { + display: block !important; + width: 100% !important; + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: anywhere !important; + line-height: 1.4 !important; +} + +.post-meta, +.feed-post-meta { + flex-direction: column !important; + gap: 4px !important; +} + +.mode-buttons, +.mode-buttons-cols, +.mode-buttons-row, +.filter-group, +.activity-filter-grid, +.activity-sub-filter, +.filters .ui-toolbar { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + justify-content: center !important; + width: 100% !important; + max-width: 100% !important; + gap: 6px !important; + overflow-x: hidden !important; + overflow-y: visible !important; + grid-template-columns: none !important; + box-sizing: border-box !important; +} + +.mode-buttons .column, +.mode-buttons > div, +.activity-filter-col { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + justify-content: center !important; + width: auto !important; + gap: 6px !important; + flex: 1 1 auto !important; +} + +.mode-buttons form, +.filter-group form, +.activity-filter-col form { + width: auto !important; + flex: 0 0 auto !important; + scroll-snap-align: start !important; +} + +.mode-buttons .filter-btn, +.mode-buttons button, +.filter-group .filter-btn, +.filter-group button, +.activity-filter-grid .filter-btn, +.activity-filter-grid button, +.activity-sub-filter .filter-btn, +.filters .filter-btn, +.filters button { + width: auto !important; + flex: 0 0 auto !important; + white-space: nowrap !important; + min-width: 0 !important; +} + +.inhabitant-card { + display: flex !important; + flex-direction: column !important; + width: 100% !important; + max-width: 100% !important; + overflow: hidden !important; +} + +.inhabitant-left { + width: 100% !important; + text-align: center !important; +} + +.inhabitant-details { + width: 100% !important; +} + +.inhabitant-photo, +.inhabitant-photo-details { + max-width: 200px !important; + margin: 0 auto !important; +} + +.inhabitants-list, +.inhabitants-grid { + display: flex !important; + flex-direction: column !important; + gap: 10px !important; +} + +.tribe-card { + display: flex !important; + flex-direction: column !important; + width: 100% !important; + max-width: 100% !important; + overflow: hidden !important; +} + +.tribe-grid { + display: flex !important; + flex-direction: column !important; + gap: 10px !important; + grid-template-columns: 1fr !important; +} + +.tribes-list, +.tribes-grid { + display: flex !important; + flex-direction: column !important; + gap: 10px !important; +} + +.tribe-card-image { + width: 100% !important; + max-width: 100% !important; +} + +.forum-card { + flex-direction: column !important; +} + +.forum-score-col, +.forum-main-col, +.root-vote-col { + width: 100% !important; +} + +.forum-header-row { + flex-direction: column !important; + gap: 4px !important; +} + +.forum-meta { + flex-wrap: wrap !important; + gap: 8px !important; +} + +.forum-thread-header { + flex-direction: column !important; +} + +.forum-comment { + margin-left: 0 !important; + padding-left: 8px !important; +} + +.comment-body-row { + flex-direction: column !important; +} + +.comment-vote-col, +.comment-text-col { + width: 100% !important; +} + +.forum-score-box, +.forum-score-form { + flex-direction: row !important; + justify-content: center !important; +} + +.new-message-form textarea, +.comment-textarea { + width: 100% !important; +} + +[style*="grid-template-columns: repeat(6"] { + grid-template-columns: 1fr !important; +} + +[style*="grid-template-columns: repeat(3"] { + grid-template-columns: 1fr !important; +} + +[style*="grid-template-columns:repeat(6"] { + grid-template-columns: 1fr !important; +} + +[style*="grid-template-columns:repeat(3"] { + grid-template-columns: 1fr !important; +} + +[style*="grid-template-columns: repeat(auto-fit"] { + grid-template-columns: 1fr !important; +} + +[style*="grid-template-columns:repeat(auto-fit"] { + grid-template-columns: 1fr !important; +} + +[style*="width:50%"] { + width: 100% !important; +} + +.update-banner { + background-color: #1a1400; + border-bottom-color: #3a2e00; + color: #FFD700; +} + +.update-banner-link { + color: #FFD700; +} + +.snh-invite-code { + color: #FFA500 !important; +} + +.stats-kpi-label { + color: #ffa300 !important; +} + +.stats-kpi-value { + color: #FFD700 !important; +} + +.carbon-bar-note, +.carbon-bar-formula { + color: #FFD700 !important; +} + +.graphos-node-label { + fill: #ddd !important; +} + +.graphos-node-label-me { + fill: #ffa500 !important; +} + +.graphos-legend { + color: #ddd !important; +} -/* Ocultar los checkboxes nativos del drawer (nav-left/right-toggle) — si no, salen 2 cuadrados blancos arriba */ .oasis-nav-side-toggle { - position: absolute; - width: 1px; - height: 1px; - opacity: 0; - pointer-events: none; - margin: 0; + position: absolute !important; + width: 1px !important; + height: 1px !important; + opacity: 0 !important; + pointer-events: none !important; } -/* ========================================================================== - Usabilidad 0.9.1 — Chats estilo Signal + widgets compactos + foros con hilos - Todo en NUESTRO tema (carga tras mobile.css → gana por cascada, sin !important) - ========================================================================== */ - -/* --- Chats: burbujas tipo Signal (los hooks .chat-message-self/-author ya existen) --- */ -.chat-messages-list { display: flex; flex-direction: column; gap: 4px; width: 100%; } -.chat-message { - max-width: 82%; width: fit-content; min-width: 0; box-sizing: border-box; - padding: 8px 12px; margin: 2px 0; - border: 1px solid #343434; background: #1e1e1e; - border-radius: 16px; border-bottom-left-radius: 4px; - align-self: flex-start; -} -.chat-message.chat-message-self { - align-self: flex-end; margin-left: auto; margin-right: 0; - background: #2a2413; border-color: #5c4c1f; - border-bottom-left-radius: 16px; border-bottom-right-radius: 4px; -} -.chat-message-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 3px; font-size: 10px; color: var(--c-muted, #8c8c8c); } -.chat-message.chat-message-self .chat-message-meta { justify-content: flex-end; } -.chat-message-sender, .chat-message-sender a { font-size: 11px; font-weight: 700; color: #FFD700; } -.chat-message-date { opacity: .8; } -.chat-message-text { - display: block; font-size: 15px; line-height: 1.42; color: #FFD700; margin: 0; - word-break: break-word; overflow-wrap: anywhere; -} -.chat-message-text a { color: #FFD700; } -.chat-message-image-wrap { margin-top: 6px; } -.chat-message-image { max-width: 100%; border-radius: 10px; } - -/* --- Compose de chat: caja redondeada + enviar como pill (no bloque naranja de ancho completo) --- */ -.chat-message-form textarea { - width: 100%; box-sizing: border-box; border-radius: 14px; - padding: 10px 14px; resize: vertical; font-size: 15px; -} -.chat-message-form .filter-btn { - width: 46px !important; height: 46px !important; min-width: 46px !important; min-height: 46px !important; - padding: 0 !important; border-radius: 50% !important; - display: inline-flex !important; align-items: center !important; justify-content: center !important; - font-size: 0 !important; line-height: 0 !important; -} -.chat-message-form .filter-btn::before { content: "\27A4"; font-size: 18px; line-height: 1; color: #121212; } -.chat-message-form input[type="file"] { font-size: 12px; } - -/* --- 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; } - -/* --- Switcher de chats: tira horizontal para saltar entre conversaciones --- */ -.chat-switcher { - display: flex; gap: 6px; overflow-x: auto; padding: 6px 0; margin: 4px 0 8px; - -webkit-overflow-scrolling: touch; scrollbar-width: none; -} -.chat-switcher::-webkit-scrollbar { display: none; } -.chat-switch-chip { - flex: 0 0 auto; max-width: 140px; padding: 6px 12px; border-radius: 999px; - background: #1c1c1c; border: 1px solid #333; color: #FFD700; text-decoration: none; - font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -} -.chat-switch-chip.active { background: #FFD700; color: #121212; border-color: #FFD700; font-weight: 700; } - -/* --- Subtopics de foro: tira de pestañas (topics con nombre) + input de topic en el compose --- */ -.forum-topics { - display: flex; gap: 6px; overflow-x: auto; padding: 6px 0; margin: 4px 0 8px; - scrollbar-width: none; -} -.forum-topics::-webkit-scrollbar { display: none; } -.forum-topic-chip { - flex: 0 0 auto; max-width: 160px; padding: 6px 14px; border-radius: 999px; - background: #1c1c1c; border: 1px solid #333; color: #FFD700; text-decoration: none; - font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -} -.forum-topic-chip.active { background: #FFD700; color: #121212; border-color: #FFD700; font-weight: 700; } -.new-message-form { flex-wrap: wrap; } -.forum-topic-input { - flex: 1 0 100%; box-sizing: border-box; border-radius: 12px; - padding: 8px 12px; font-size: 13px; margin: 0 0 6px; +.header { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + align-items: center !important; + justify-content: center !important; + gap: 8px !important; + padding: 8px !important; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; } -/* --- #3: al desplegar una categoría, sus módulos son BOTONES CUADRADOS redondeados (no hexágonos), - en rejilla 2×2 (2 y 2) y grandes: cada cuadrado llena su columna --- */ -.hive-mods { - display: grid; grid-template-columns: repeat(2, 1fr); - justify-items: stretch; align-content: center; - gap: 12px; padding: 8px 4px; -} -.hex-mod { - clip-path: none; -webkit-clip-path: none; - width: 100%; height: auto; aspect-ratio: 1 / 1; min-height: 120px; - border-radius: 18px; padding: 8px; gap: 7px; -} -.hex-mod::before { clip-path: none; -webkit-clip-path: none; inset: 2px; border-radius: 16px; } -.hex-mod .hx-ico { font-size: 36px; } -.hex-mod .hx-lbl { font-size: 12.5px; max-width: 94%; } -/* si el nº de módulos es IMPAR, el último (solo en su fila) se CENTRA en vez de pegarse a la izquierda; - abarca las 2 columnas pero conserva el ancho de una sola (gap 12 → mitad 6) */ -.hex-mod:last-child:nth-child(odd) { - grid-column: 1 / -1; justify-self: center; width: calc(50% - 6px); +.header .top-bar-left, +.header .top-bar-right, +.header .top-bar-mid, +.header .top-bar-center { + display: contents !important; } -/* --- #2: el hexágono "Personal" muestra tu avatar (diferencia del filtro "Personal" del header) --- */ -.hex-cat .hx-avatar { - width: 58%; height: 58%; border-radius: 50%; object-fit: cover; - position: relative; z-index: 1; border: 1px solid rgba(255,215,0,.6); +.header .top-bar-left a.logo-icon { + order: 1 !important; + flex: 1 1 100% !important; + display: flex !important; + justify-content: center !important; + margin: 0 0 4px 0 !important; } -/* --- #1: chat estilo Signal — compose ABAJO + mensajes arriba (el reply deja de "saltar") --- */ -.tribe-main.chat-full-width { display: flex; flex-direction: column-reverse; } -#chatbox { scroll-margin-top: 8px; scroll-margin-bottom: 8px; } - -/* --- Botón spread → pill compacto (vence button{min-height:48px}/ancho completo) --- */ -.card-spread-centered, .card-spread-left, .spread-row { margin: 6px 0; } -.card-spread-centered .spread-btn, .card-spread-left .spread-btn, -.spread-row .spread-btn, .card-chips-row .spread-btn { - display: inline-flex; align-items: center; justify-content: center; - width: auto; min-width: 0; min-height: 30px; height: 30px; - padding: 2px 14px; font-size: 13px; line-height: 1; - border-radius: 999px; border: 1px solid #555; background: #1a1a1a; color: #FFD700; -} -.card-spread-centered .spread-btn.spread-btn-on, -.card-spread-left .spread-btn.spread-btn-on { background: #FFD700; color: #1a1a1a; border-color: #FFD700; } - -/* --- Caja de votos (▲ n ▼): compacta y HORIZONTAL. - El problema era una cadena de flex-stretch (comment-vote-col flex → estiraba la caja a 135px). - Rompemos el estiramiento con height:auto + inline-flex en toda la cadena, con !important - para vencer el min-height:44px de .score-btn (762) y el !important de epsylon en mobile.css. --- */ -.comment-vote-col { display: block; height: auto; min-height: 0; width: auto; } -.forum-score-box { - display: inline-flex; height: auto; min-height: 0; - min-width: 0; width: auto; - padding: 4px 8px; border-radius: 10px; - flex-direction: row; flex-wrap: nowrap; gap: 12px; align-items: center; -} -.forum-score-box .forum-score-form { - display: inline-flex; height: auto; width: auto; - flex-direction: row; flex-wrap: nowrap; gap: 12px; align-items: center; -} -.score-btn { - flex: 0 0 auto !important; - width: 30px !important; height: 30px !important; min-width: 30px !important; min-height: 30px !important; - font-size: 0.95em !important; border-radius: 8px !important; padding: 0 !important; -} -.score-total { flex: 0 0 auto; font-size: 1.15em; min-height: 0; height: auto; margin: 0; white-space: nowrap; } -.comment-votes .votes-count { font-size: 11px; } - -/* Aplanar el comentario: quitar las cajas/recuadros internos para que sea UN bloque limpio - (cada sección salía como una tarjeta con padding enorme → mucho hueco muerto) */ -.forum-comment .comment-header, -.forum-comment .comment-body-row, -.forum-comment .comment-vote-col, -.forum-comment .comment-text-col, -.forum-comment .new-reply { - background: transparent; border: none; box-shadow: none; - padding: 0; margin: 6px 0 0; border-radius: 0; -} -.forum-comment .comment-header { margin-top: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 12px; } -/* la barra de votos de abajo ya muestra el score → ocultar el ▲:0 ▼:0 redundante de la cabecera */ -.forum-comment .comment-header .comment-votes { display: none; } -.forum-comment .comment-text-col p { margin: 4px 0; } -.forum-comment .comment-text-col > div { background: transparent; border: none; padding: 0; } -.forum-comment { padding: 10px 12px; margin-bottom: 8px; } -/* barra de votos alineada a la izquierda, no centrada, para que ocupe poco. - `display:flex !important` es DE PELEA: vence a mobile.css:164 `.comment-body-row{display:block!important}` */ -.forum-comment .comment-body-row { display: flex !important; flex-direction: column; align-items: flex-start; } - -/* --- Reply/compose del foro estilo Signal: fila [textarea que crece] + [botón REDONDO con icono ↩] --- */ -/* especificidad extra (.main-column …) para ganar a nuestra propia regla `.main-column form{display:block}` sin !important */ -.main-column .new-reply .comment-form, -.main-column .new-message-form { display: flex; flex-direction: row; align-items: flex-end; gap: 8px; margin: 6px 0 0; } -.main-column .new-message-form br { display: none; } -.main-column .comment-textarea, .main-column .new-message-form textarea { - flex: 1 1 auto; box-sizing: border-box; border-radius: 14px; - padding: 9px 14px; min-height: 42px; resize: none; font-size: 14px; margin: 0; -} -.forum-send-btn { - flex: 0 0 auto !important; - width: 44px !important; height: 44px !important; min-width: 44px !important; min-height: 44px !important; - padding: 0 !important; margin: 0 !important; border-radius: 50% !important; - font-size: 0 !important; line-height: 0 !important; - display: inline-flex !important; align-items: center !important; justify-content: center !important; -} -.forum-send-btn::before { content: "\21A9"; font-size: 20px; line-height: 1; color: #121212; } -.new-reply { margin-top: 6px; } - -/* --- Fase 2: hilos anidados. mobile.css fuerza margin-left:0 !important → necesitamos !important para ganar --- */ -.forum-comment.level-1 { margin-left: 10px; border-left: 2px solid #333; } -.forum-comment.level-2 { margin-left: 20px; border-left: 2px solid #3a3a3a; } -.forum-comment.level-3 { margin-left: 28px; border-left: 2px solid #404040; } -.forum-comment.level-4 { margin-left: 34px; border-left: 2px solid #454545; } -/* comentario destacado: acento sutil a la izquierda en vez del marco naranja enorme */ -.forum-comment.highlighted-reply { border: 1px solid #3a3a3a; border-left: 3px solid #FFD700; background: #1b1a16; } - -/* --- Reply del foro SIN tocar la vista de epsylon: la cajita se agranda al enfocarla; - el botón redondo ↩ está SIEMPRE visible (el usuario pidió icono de reply, no gesto/JS). --- */ -.new-reply .comment-textarea { min-height: 42px; } -.new-reply:focus-within .comment-textarea { min-height: 64px; } - -/* ========================================================================== - Mejoras UI 0.9.1: fuente única + barra de búsqueda en topbar + hive global + bottombar fija - ========================================================================== */ - -/* --- fuente única en todo el UI móvil (quitar el lío monospace/sans) --- */ -.oasis-mobile-topbar, .oasis-mobile-menus, .hive-nav, .oasis-bottombar, .main-column, -.oasis-mobile-topbar input, .main-column input, .main-column textarea, .main-column select, button { - font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +.oasis-mobile-menus { + order: 2 !important; + display: flex !important; + flex: 1 1 auto !important; + flex-wrap: wrap !important; + justify-content: center !important; + align-items: center !important; + gap: 8px !important; } -/* (topbar sin barra de búsqueda: vuelve el título "OASIS" — CSS .omt-title más arriba) */ - -/* --- hive ahora global en el header (debajo de los 2 botones) --- */ -.header .hive-nav { margin: 34px auto 10px; } - -/* holgura bajo la topbar fija (el hive va en el header, solo en la home) */ -body { padding-top: 84px; } - -/* ========================================================================== - 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} */ +.oasis-menu-btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + gap: 4px !important; + min-height: 42px !important; + padding: 6px 12px !important; + border: 1px solid #FFD700 !important; + border-radius: 8px !important; + background: #161616 !important; + color: #FFD700 !important; + font-weight: 600 !important; + font-size: 0.85rem !important; + text-decoration: none !important; + white-space: nowrap !important; + cursor: pointer !important; } -.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; } +.oasis-menu-btn .oasis-menu-ico { font-size: 1.05rem !important; } + +.header .top-bar-left nav { + order: 3 !important; + flex: 1 1 100% !important; + width: 100% !important; +} +.header .top-bar-right nav { + order: 4 !important; + flex: 1 1 100% !important; + width: 100% !important; +} +.header .top-bar-left nav ul, +.header .top-bar-right nav ul, +.header .top-bar-mid nav ul { + display: grid !important; + grid-template-columns: repeat(3, 1fr) !important; + gap: 6px !important; + overflow: visible !important; + list-style: none !important; + padding: 0 !important; + margin: 0 !important; + width: 100% !important; + box-sizing: border-box !important; +} +.header .top-bar-left nav ul li, +.header .top-bar-right nav ul li, +.header .top-bar-mid nav ul li { + width: 100% !important; + margin: 0 !important; +} +.header .top-bar-left nav ul li, +.header .top-bar-right nav ul li { margin: 0 !important; } +.header .top-bar-left nav ul li a, +.header .top-bar-right nav ul li a { + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 4px !important; + width: 100% !important; + min-height: 42px !important; + box-sizing: border-box !important; + padding: 6px 8px !important; + font-size: 0.85rem !important; + white-space: nowrap !important; + border: 1px solid #FFD700 !important; + border-radius: 8px !important; + background: #161616 !important; + color: #FFD700 !important; + text-decoration: none !important; + font-weight: 600 !important; +} +.header a, +.header a:link, +.header a:visited { + text-decoration: none !important; +} +.header .top-bar-left nav ul li a .inbox-badge { + color: #fff !important; +} + +.main-content { + display: flex !important; + flex-direction: column !important; + gap: 10px !important; +} +.main-content .main-column { order: 0 !important; } +.main-content .sidebar-left, +.main-content .sidebar-right { + display: none !important; +} +#nav-left-toggle:checked ~ .main-content .sidebar-left, +#nav-right-toggle:checked ~ .main-content .sidebar-right { + display: block !important; + order: -1 !important; + width: 100% !important; + max-width: 100% !important; + background: #0d0d0d !important; + border: 1px solid #FFD700 !important; + border-radius: 10px !important; + padding: 8px !important; + margin: 0 0 8px 0 !important; +} +.sidebar-left nav ul, +.sidebar-right nav ul { + display: flex !important; + flex-direction: column !important; + gap: 4px !important; + list-style: none !important; + padding: 0 !important; + margin: 0 !important; +} + +.activity-filter-grid, +.activity-filters, +.activity-sub-filter, +.filters .ui-toolbar { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + justify-content: center !important; + align-items: flex-start !important; + gap: 6px !important; + overflow-x: hidden !important; + grid-template-columns: none !important; + scroll-snap-type: none !important; + width: 100% !important; + max-width: 100% !important; +} +.activity-filter-col { + display: flex !important; + flex: 1 1 100% !important; + flex-direction: row !important; + flex-wrap: wrap !important; + justify-content: center !important; + gap: 6px !important; + width: 100% !important; + max-width: 100% !important; +} +.activity-filter-grid form, +.activity-filter-col form, +.activity-sub-filter form { + flex: 0 1 auto !important; + scroll-snap-align: none !important; + width: auto !important; +} +.activity-filter-grid .filter-btn, +.activity-filter-col .filter-btn { + width: auto !important; + min-width: 0 !important; + white-space: nowrap !important; +} + +.oasis-footer, +.oasis-footer-center { + overflow-x: hidden !important; + max-width: 100% !important; +} +.oasis-footer-center span, +.oasis-footer-center a { + overflow-wrap: anywhere !important; + word-break: break-all !important; + max-width: 100% !important; +} + +a:not([class]), +a.mention, +a.hashtag { + color: #FFD700 !important; + text-decoration: underline !important; +} + +.module-locked { opacity: 0.45 !important; } +.module-locked .input-checkbox { cursor: not-allowed !important; } + +.game-row { + flex-direction: column !important; + align-items: stretch !important; +} +.game-row-media, +.game-row-media img { + width: 100% !important; + max-width: 100% !important; +} +.game-row-body { width: 100% !important; min-width: 0 !important; } +.game-row-actions { justify-content: center !important; width: 100% !important; } +.games-grid { grid-template-columns: 1fr !important; } + +.profile-sensors-box { display: flex !important; flex-direction: column !important; align-items: center !important; text-align: center !important; gap: 6px !important; max-width: 100% !important; } +.profile-sensors-box span { display: block !important; max-width: 100% !important; overflow-wrap: anywhere !important; } +.larp-sign-block { display: block !important; max-width: 100% !important; overflow: hidden !important; text-align: center !important; } +.larp-sign-large { max-width: 100% !important; height: auto !important; display: block !important; margin: 0 auto !important; } +.profile-side-mention { max-width: 100% !important; overflow-wrap: anywhere !important; word-break: break-all !important; } +.profile-side-mention a, .profile-side-mention strong { max-width: 100% !important; overflow-wrap: anywhere !important; word-break: break-all !important; display: inline-block !important; } +.inhabitant-stats-box { max-width: 100% !important; overflow: hidden !important; } +.inhabitant-stats-grid { display: flex !important; flex-wrap: wrap !important; justify-content: center !important; gap: 6px !important; max-width: 100% !important; } +.inhabitant-stat { max-width: 100% !important; overflow-wrap: anywhere !important; } +.profile-reach { max-width: 100% !important; overflow: hidden !important; text-align: center !important; } +.feed-card .card-footer { display: flex !important; flex-wrap: wrap !important; align-items: center !important; gap: 6px !important; row-gap: 4px !important; } +.feed-card .date-link { max-width: 100% !important; overflow-wrap: anywhere !important; font-size: 0.85em !important; padding: 6px 10px !important; } diff --git a/src/client/assets/themes/OasisMobileUX.css b/src/client/assets/themes/OasisMobileUX.css new file mode 100644 index 0000000..a43279c --- /dev/null +++ b/src/client/assets/themes/OasisMobileUX.css @@ -0,0 +1,777 @@ +html { overflow-x: clip !important; overflow-y: visible !important; height: auto !important; } +body { overflow-x: clip !important; overflow-y: visible !important; } + + +.oasis-bottombar { + display: flex !important; + position: fixed !important; + left: 0 !important; right: 0 !important; bottom: 0 !important; + z-index: 1000 !important; + justify-content: space-around !important; + align-items: center !important; + gap: 4px !important; + background: #1F1F1F !important; + border-top: 1px solid #333 !important; + padding: 8px 6px calc(8px + env(safe-area-inset-bottom, 0px)) !important; +} +.oasis-bottombar .bb-item { + position: relative !important; + display: flex !important; + flex-direction: column !important; + align-items: center !important; + gap: 3px !important; + flex: 1 1 0 !important; + min-width: 0 !important; + padding: 2px 0 !important; + text-decoration: none !important; +} +.oasis-bottombar .bb-ico { font-size: 18px !important; line-height: 1 !important; color: #FFA500 !important; } +.oasis-bottombar .bb-lbl { font-size: 9px !important; color: #ccc !important; white-space: nowrap !important; } +.oasis-bottombar .bb-badge { + position: absolute !important; + top: -4px !important; + left: calc(50% + 6px) !important; + min-width: 15px !important; height: 15px !important; + padding: 0 3px !important; + border-radius: 8px !important; + background: #FFA500 !important; + color: #121212 !important; + font-size: 9px !important; font-weight: bold !important; + display: flex !important; align-items: center !important; justify-content: center !important; +} + +body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)) !important; } + +.oasis-mobile-topbar { + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + width: 100% !important; + padding: 8px 12px !important; + background: transparent !important; + box-shadow: none !important; border: 0 !important; +} + +.oasis-mobile-topbar .omt-logo img, +.oasis-mobile-topbar .omt-avatar img { + width: 32px !important; height: 32px !important; + border-radius: 50% !important; + border: 2px solid #FFA500 !important; + object-fit: cover !important; display: block !important; +} + +.oasis-mobile-topbar .omt-title { + flex: 1 1 auto !important; + text-align: center !important; + font-size: 26px !important; font-weight: bold !important; + letter-spacing: 10px !important; text-indent: 10px !important; + color: #FFD700 !important; text-decoration: none !important; + white-space: nowrap !important; + background: transparent !important; border: 0 !important; box-shadow: none !important; padding: 0 !important; +} + +.header .top-bar-left > a.logo-icon { display: none !important; } + +.oasis-mobile-menus { display: flex !important; gap: 8px !important; padding: 0 10px 6px !important; } +.oasis-mobile-menus .oasis-menu-ico { display: none !important; } +.oasis-mobile-menus .oasis-menu-btn { + flex: 1 1 0 !important; + display: flex !important; align-items: center !important; justify-content: center !important; + padding: 12px !important; + border: 1.5px solid #333 !important; border-radius: 10px !important; + background: #121212 !important; color: #ccc !important; + font-weight: bold !important; letter-spacing: 1px !important; cursor: pointer !important; +} + +#nav-left-toggle:checked ~ .header .oasis-menu-btn[for="nav-left-toggle"] { + background: #FFA500 !important; color: #121212 !important; border-color: #FFA500 !important; +} + +#nav-right-toggle:checked ~ .header .oasis-menu-btn[for="nav-right-toggle"] { + background: #FFD700 !important; color: #121212 !important; border-color: #FFD700 !important; +} + +:root { + --space-1: 4px; --space-2: 8px; --space-3: 16px; --space-4: 24px; --space-5: 32px; + --fs-caption: 12px; --fs-body: 14px; --fs-body-lg: 16px; --fs-title: 20px; + --lh-body: 1.5; --measure: 66ch; + --c-muted: #8c8c8c; --radius-md: 12px; +} + +.oasis-bottombar .bb-fab .bb-ico { + width: 46px !important; height: 46px !important; + margin-top: -22px !important; + border-radius: 50% !important; + background: #FFA500 !important; color: #121212 !important; + display: flex !important; align-items: center !important; justify-content: center !important; + font-size: 22px !important; + box-shadow: 0 2px 8px rgba(0,0,0,.4) !important; +} +.oasis-bottombar .bb-fab .bb-lbl { color: #FFA500 !important; font-weight: bold !important; } + +.main-column, .main-column p, .main-column li, .main-column a, +.main-column span, .main-column td, .main-column h1, .main-column h2, .main-column h3 { + overflow-wrap: anywhere !important; + word-break: break-word !important; +} +.main-column { padding: var(--space-3) !important; } +.main-column p { line-height: var(--lh-body) !important; } + +.main-column > * + * { margin-top: var(--space-3) !important; } + +.main-column img, .main-column video, .main-column table, .main-column pre, .main-column iframe { + max-width: 100% !important; height: auto !important; +} +.main-column pre { white-space: pre-wrap !important; } + +.oasis-nav-group { + list-style: none !important; + background: #1c1c1c !important; + border: 1px solid #333 !important; + border-radius: var(--radius-md) !important; + overflow: hidden !important; + margin-bottom: var(--space-2) !important; +} +.oasis-nav-header { + display: flex !important; align-items: center !important; gap: var(--space-2) !important; + min-height: 48px !important; padding: 0 var(--space-3) !important; + cursor: pointer !important; + color: #FFD700 !important; font-weight: bold !important; + text-transform: uppercase !important; letter-spacing: .04em !important; + font-size: var(--fs-caption) !important; +} +.oasis-nav-header .nav-text { flex: 1 1 auto !important; } +.oasis-nav-header .oasis-nav-arrow { color: var(--c-muted) !important; transition: transform 150ms ease !important; } +.oasis-nav-toggle:checked + .oasis-nav-header .oasis-nav-arrow { transform: rotate(180deg) !important; } +.oasis-nav-header:active { background: #262626 !important; } +.oasis-nav-list { list-style: none !important; padding: 0 !important; margin: 0 !important; } +.oasis-nav-list li a { + display: flex !important; align-items: center !important; gap: var(--space-3) !important; + min-height: 48px !important; padding: 0 var(--space-3) !important; + color: #e8e8e8 !important; text-decoration: none !important; + border-top: 1px solid #262626 !important; +} +.oasis-nav-list li a .emoji { flex: 0 0 auto !important; width: 22px !important; text-align: center !important; color: #FFA500 !important; } +.oasis-nav-list li a .nav-text { font-size: var(--fs-body) !important; } +.oasis-nav-list li a:active { background: #262626 !important; } +.oasis-nav-list li a.current { color: #FFA500 !important; font-weight: bold !important; } + +.main-column h1 { font-size: var(--fs-title) !important; font-weight: 700 !important; line-height: 1.25 !important; margin: 0 0 var(--space-2) !important; } +.main-column h2 { font-size: 18px !important; font-weight: 700 !important; line-height: 1.3 !important; margin: 0 0 var(--space-2) !important; } +.main-column h3 { font-size: var(--fs-body-lg) !important; font-weight: 600 !important; margin: 0 0 var(--space-1) !important; } +.main-column p { font-size: var(--fs-body-lg) !important; } +.main-column .created-at, .main-column .small, .main-column .time, +.main-column .meta, .main-column .author-meta, .main-column .subtitle { + color: var(--c-muted) !important; font-size: var(--fs-caption) !important; +} + +.main-column article, +.main-column [class*="-card"] { + background: #1c1c1c !important; + border: 1px solid #333 !important; + border-radius: var(--radius-md) !important; + padding: var(--space-3) !important; +} +.main-column article:active, +.main-column [class*="-card"]:active { background: #202020 !important; } + +.btn, button, input[type="submit"], input[type="button"], .submit-button, +.filter-btn, .create-button, .edit-btn, .delete-btn, .join-btn, .leave-btn, .buy-btn { + min-height: 48px !important; +} +button:active, input[type="submit"]:active, .submit-button:active, +.filter-btn:active, .create-button:active, .edit-btn:active, +.delete-btn:active, .join-btn:active, .leave-btn:active, .buy-btn:active { + filter: brightness(.85) !important; +} +.main-column a:active { opacity: .6 !important; } + +* { -webkit-tap-highlight-color: transparent !important; } +a, button, label, .tap, .oasis-menu-btn, .bb-item, .oasis-nav-header, .oasis-nav-list li a { + touch-action: manipulation !important; +} +body { overscroll-behavior: none !important; } + +.main-content, .main-column, .header, +.sidebar-left, .sidebar-right { overflow: visible !important; } + +.sidebar-left, .sidebar-right { display: none !important; } +#nav-left-toggle:checked ~ .main-content .sidebar-left { display: block !important; } +#nav-right-toggle:checked ~ .main-content .sidebar-right { display: block !important; } + +.bb-manage { margin: 8px 0 16px !important; } +.bb-manage h3, .bb-manage-add h3 { + font-size: 13px !important; text-transform: uppercase !important; letter-spacing: .05em !important; + color: #FFA500 !important; margin: 14px 0 8px !important; +} +.bb-manage-list { display: flex !important; flex-direction: column !important; gap: 8px !important; } +.bb-manage-form { margin: 0 !important; width: 100% !important; } +.bb-manage-btn { + width: 100% !important; + display: flex !important; align-items: center !important; gap: 12px !important; + min-height: 52px !important; padding: 0 14px !important; + background: #1c1c1c !important; border: 1px solid #333 !important; border-radius: 10px !important; + color: #eee !important; cursor: pointer !important; text-align: left !important; +} +.bb-manage-ico { font-size: 20px !important; color: #FFA500 !important; width: 26px !important; text-align: center !important; } +.bb-manage-lbl { flex: 1 1 auto !important; font-size: 15px !important; } +.bb-manage-x { font-size: 16px !important; color: #ff6b6b !important; font-weight: bold !important; } +.bb-manage-empty, .bb-manage-full { color: #999 !important; font-size: 14px !important; padding: 8px 2px !important; } +.bb-manage-done { + display: block !important; text-align: center !important; + margin-top: 16px !important; min-height: 48px !important; line-height: 48px !important; +} + +.hive-nav { --hx: min(64px, calc((100vw - 40px) / 4)); --hxm: min(58px, calc((100vw - 36px) / 5)); margin: 8px 0 16px !important; overflow: visible !important; background: transparent !important; padding: 0 !important; border: 0 !important; box-shadow: none !important; } +.hive-radio { position: absolute !important; opacity: 0 !important; width: 0 !important; height: 0 !important; pointer-events: none !important; } +.hive-grid { display: flex !important; flex-direction: column !important; align-items: center !important; overflow: visible !important; background: transparent !important; padding: 0 !important; margin: 0 !important; box-shadow: none !important; } +.hive-row { display: flex !important; justify-content: center !important; gap: 3px !important; overflow: visible !important; background: transparent !important; padding: 0 !important; margin: 0 !important; box-shadow: none !important; } +.hive-cats .hive-row + .hive-row { margin-top: calc(var(--hx) * -0.30) !important; } +.hex-cat, .hex-mod { + position: relative !important; + -webkit-clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; + clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; + display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; gap: 2px !important; + text-decoration: none !important; cursor: pointer !important; box-sizing: border-box !important; + background: #D9A400 !important; + filter: drop-shadow(0 1px 2px rgba(0,0,0,.6)) !important; +} +.hex-cat::before, .hex-mod::before { + content: '' !important; position: absolute !important; inset: 1.5px !important; z-index: 0 !important; + -webkit-clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; + clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; + background: #161616 !important; +} +.hex-cat { width: var(--hx) !important; height: calc(var(--hx) * 1.15) !important; } +.hex-mod { width: var(--hxm) !important; height: calc(var(--hxm) * 1.15) !important; } +.hex-cat .hx-ico, .hex-mod .hx-ico { color: #FFA500 !important; line-height: 1 !important; position: relative !important; z-index: 1 !important; } +.hex-cat .hx-ico { font-size: 17px !important; } +.hex-mod .hx-ico { font-size: 16px !important; } +.hex-cat .hx-lbl, .hex-mod .hx-lbl { color: #d8d8d8 !important; text-align: center !important; line-height: 1 !important; font-weight: bold !important; overflow: hidden !important; position: relative !important; z-index: 1 !important; } +.hex-cat .hx-lbl { font-size: 8px !important; max-width: 88% !important; } +.hex-mod .hx-lbl { font-size: 8px !important; max-width: 92% !important; } +.hex-cat:active::before, .hex-mod:active::before { background: #242424 !important; } +#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"], +#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"], +#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"], +#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"], +#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"], +#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"], +#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"], +#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"], +#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"], +#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"] { background: #FFA500 !important; } +#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"]::before, +#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"]::before, +#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"]::before, +#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"]::before, +#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"]::before, +#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"]::before, +#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"]::before, +#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"]::before, +#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"]::before, +#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"]::before { background: #FFA500 !important; } +#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"] :is(.hx-ico,.hx-lbl), +#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"] :is(.hx-ico,.hx-lbl), +#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"] :is(.hx-ico,.hx-lbl), +#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"] :is(.hx-ico,.hx-lbl), +#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"] :is(.hx-ico,.hx-lbl), +#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"] :is(.hx-ico,.hx-lbl), +#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"] :is(.hx-ico,.hx-lbl), +#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"] :is(.hx-ico,.hx-lbl), +#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"] :is(.hx-ico,.hx-lbl), +#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"] :is(.hx-ico,.hx-lbl) { color: #121212 !important; } +.hive-panel { display: none !important; margin: 12px auto 0 !important; max-width: 340px !important; background: #101010 !important; border: 1px solid #242424 !important; border-radius: 16px !important; padding: 12px 8px !important; overflow: visible !important; box-shadow: none !important; } +#hive-personal:checked ~ #hive-panel-personal, +#hive-governance:checked ~ #hive-panel-governance, +#hive-office:checked ~ #hive-panel-office, +#hive-economy:checked ~ #hive-panel-economy, +#hive-tools:checked ~ #hive-panel-tools, +#hive-network:checked ~ #hive-panel-network, +#hive-blogs:checked ~ #hive-panel-blogs, +#hive-creative:checked ~ #hive-panel-creative, +#hive-media:checked ~ #hive-panel-media, +#hive-fediverse:checked ~ #hive-panel-fediverse { display: block !important; } +.hive-mods { display: flex !important; flex-wrap: wrap !important; justify-content: center !important; gap: 4px !important; margin: 0 !important; padding: 0 !important; background: transparent !important; overflow: visible !important; } + +.header .top-bar-left nav, +.header .top-bar-right nav { display: none !important; } + +.oasis-mobile-topbar { padding-top: calc(env(safe-area-inset-top, 0px) + 10px) !important; } + +.oasis-bottombar .bb-pair { + display: flex !important; align-items: center !important; + flex: 0 0 auto !important; + gap: 2px !important; padding: 3px !important; + background: #262626 !important; + border: 1px solid #3a3a3a !important; + border-radius: 999px !important; +} +.oasis-bottombar .bb-pair .bb-item { flex: 0 0 auto !important; padding: 2px 8px !important; } +.oasis-bottombar .bb-pair .bb-lbl { font-size: 8px !important; } + +.oasis-bottombar a.bb-item.current .bb-ico { + background: #FFA500 !important; color: #121212 !important; + border-radius: 999px !important; padding: 4px 12px !important; +} +.oasis-bottombar a.bb-item.current .bb-lbl { color: #FFA500 !important; font-weight: bold !important; } +.oasis-bottombar a.bb-item.current .bb-badge { background: #121212 !important; color: #FFA500 !important; } + +.main-column, .main-column p, .main-column li, .main-column a, +.main-column span, .main-column td, .main-column th, .main-column label, +.main-column h1, .main-column h2, .main-column h3, .main-column h4, +.main-column strong, .main-column em, .main-column small, +.created-at, .post-meta, .feed-post-meta, +.feed-row .small, .feed-row .time, .feed-row .created-at, +.profile-sensors-box > *, +.card-field .card-value, .card-field .card-label { + overflow-wrap: break-word !important; + word-break: normal !important; + -webkit-hyphens: none !important; + hyphens: none !important; +} + +.main-column pre, .main-column code, kbd, samp, +.peer-key, .user-link.peer-key, .oasis-id-copy, .search-oasis-id, +.pm-sent-key-value, .kpi__value, +.block-info-table td .peer-key, .block-info-table td .user-link, +.pm-info-table td .user-link, +.main-column a[href*="ed25519"], .main-column a[href^="ssb:"], +.main-column a[href^="/author/"] { + overflow-wrap: anywhere !important; + word-break: break-all !important; +} + +button, input[type="submit"], input[type="button"], +.filter-btn, a.filter-btn, .create-button, .edit-btn, .delete-btn, +.join-btn, .leave-btn, .buy-btn, .submit-button, .btn, +.conn-actions button, .peers-conn-actions button, +.mode-buttons button, .mode-buttons .filter-btn, +.filter-group button, .filter-group .filter-btn, +.main-column button, .main-column input[type="submit"] { + white-space: normal !important; + word-break: normal !important; + overflow-wrap: break-word !important; + -webkit-hyphens: none !important; + hyphens: none !important; +} + +.conn-actions, .peers-conn-actions { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + justify-content: center !important; + align-items: stretch !important; + gap: 8px !important; + width: 100% !important; + max-width: 100% !important; + overflow-x: hidden !important; +} +.conn-actions > form, .peers-conn-actions > form { + flex: 0 1 auto !important; + width: auto !important; + min-width: 0 !important; + margin: 0 !important; + display: flex !important; +} + +.pm-info-table, .pm-info-table tbody, +.block-info-table, .block-info-table tbody { + display: block !important; + width: 100% !important; + max-width: 100% !important; + border: 0 !important; + background: transparent !important; + box-shadow: none !important; + table-layout: auto !important; +} +.pm-info-table thead, .block-info-table thead { display: none !important; } +.pm-info-table tr, .block-info-table tr { + display: block !important; + width: 100% !important; + box-sizing: border-box !important; + border: 0 !important; + border-bottom: 1px solid rgba(255,165,0,0.18) !important; + border-radius: 0 !important; + margin: 0 0 8px 0 !important; + padding: 6px 0 !important; + background: transparent !important; +} +.pm-info-table td, .block-info-table td { + display: block !important; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + border: 0 !important; + background: transparent !important; + padding: 1px 0 !important; + text-align: left !important; + overflow-wrap: break-word !important; + word-break: normal !important; +} + +.pm-info-table td.card-label, .block-info-table td.card-label { + color: var(--c-muted, #8c8c8c) !important; + font-size: 11px !important; + font-weight: 700 !important; + text-transform: uppercase !important; + letter-spacing: .03em !important; + margin-bottom: 2px !important; + padding-bottom: 0 !important; +} +.pm-info-table td.card-value, .block-info-table td.card-value { + font-size: 15px !important; + line-height: 1.5 !important; + margin-bottom: 6px !important; +} + +.block-info-table tr:first-child:has(> td.card-label) { display: none !important; } +.block-info-table td[data-label]:not([data-label=""])::before { + content: attr(data-label); + display: block !important; + color: var(--c-muted, #8c8c8c) !important; + font-size: 10px !important; + font-weight: 700 !important; + text-transform: uppercase !important; + letter-spacing: .03em !important; + margin-bottom: 1px !important; + white-space: nowrap !important; + word-break: normal !important; +} +.block-info-table td[data-label=""]::before { content: none !important; } + +.peers-list .block-info-table td, +.peers-technical-block .block-info-table td, +.gov-overview tbody td { + word-break: normal !important; + overflow-wrap: break-word !important; +} + +.pm-info-table td .user-link, .pm-info-table td .peer-key, .pm-info-table td a[href], +.block-info-table td .peer-key, .block-info-table td .user-link, .block-info-table td a[href] { + display: inline-block !important; + max-width: 100% !important; + overflow-wrap: anywhere !important; + word-break: break-all !important; +} + +.pm-info-table td form, .block-info-table td form, +.pm-info-table td button, .block-info-table td button { + width: 100% !important; + margin: 0 !important; +} + +.main-content { width: 100% !important; max-width: 100% !important; } +.main-column { + width: 100% !important; max-width: 100% !important; + padding: 16px !important; box-sizing: border-box !important; +} +.main-column .card-field { + flex-direction: column !important; + align-items: stretch !important; + gap: 2px !important; +} +.main-column .card-field .card-value { + width: 100% !important; max-width: 100% !important; font-size: 15px !important; +} +.main-column .card-field .card-label { + color: var(--c-muted, #8c8c8c) !important; font-size: 11px !important; +} +.main-column img, .main-column video, .main-column iframe, .main-column canvas { + max-width: 100% !important; height: auto !important; +} +.main-column hr { + width: 100% !important; border: 0 !important; + border-top: 1px solid #333 !important; margin: 16px 0 !important; +} + +.activity-filter-acc { width: 100% !important; margin-bottom: 8px !important; } + +.activity-quick-modes { + display: flex !important; flex-wrap: nowrap !important; gap: 6px !important; margin-bottom: 8px !important; +} +.activity-quick-modes form { flex: 1 1 0 !important; min-width: 0 !important; margin: 0 !important; display: flex !important; } +.activity-quick-modes .filter-btn { + flex: 1 1 0 !important; width: 100% !important; min-height: 44px !important; + white-space: nowrap !important; word-break: keep-all !important; overflow-wrap: normal !important; + padding-left: 6px !important; padding-right: 6px !important; font-size: 14px !important; +} + +.activity-groups { list-style: none !important; padding: 0 !important; margin: 0 !important; } + +.activity-groups .oasis-nav-header .emoji { display: none !important; } +.activity-groups .oasis-nav-header .nav-text { flex: 1 1 auto !important; } +.activity-groups .act-count { + flex: 0 0 auto !important; min-width: 20px !important; text-align: center !important; + font-size: 11px !important; font-weight: 700 !important; color: #121212 !important; + background: #FFA500 !important; border-radius: 999px !important; padding: 1px 7px !important; + margin: 0 8px 0 0 !important; +} + +.activity-groups .oasis-nav-list li { list-style: none !important; margin: 0 !important; padding: 0 !important; } +.activity-groups .oasis-nav-list form { margin: 0 !important; display: block !important; width: 100% !important; } +.activity-groups .oasis-nav-list .filter-btn { + display: block !important; width: 100% !important; text-align: left !important; + min-height: 44px !important; margin: 2px 0 !important; box-sizing: border-box !important; +} +.activity-groups .oasis-nav-list .filter-btn.active { + outline: 2px solid #FFA500 !important; outline-offset: -2px !important; +} + +.message-list { display: flex !important; flex-direction: column !important; gap: 4px !important; width: 100% !important; } +.message-list .pm-thread { display: flex !important; flex-direction: column !important; width: 100% !important; align-self: stretch !important; } +.pm-thread-replies { display: flex !important; flex-direction: column !important; gap: 4px !important; } + +.pm-card.pm-bubble { + max-width: 82% !important; width: fit-content !important; min-width: 0 !important; + box-sizing: border-box !important; padding: 8px 12px !important; margin: 2px 0 !important; + border-radius: 16px !important; border: 1px solid #333 !important; + background: #1e1e1e !important; box-shadow: none !important; cursor: default !important; +} + +.pm-card.pm-bubble.pm-recv { + align-self: flex-start !important; margin-left: 0 !important; margin-right: auto !important; + border-bottom-left-radius: 4px !important; background: #1e1e1e !important; border-color: #343434 !important; +} + +.pm-card.pm-bubble.pm-sent { + align-self: flex-end !important; margin-left: auto !important; margin-right: 0 !important; + border-bottom-right-radius: 4px !important; background: #2a2413 !important; border-color: #5c4c1f !important; +} + +.pm-bubble .pm-bubble-sender { font-size: 11px !important; font-weight: 700 !important; color: #FFA500 !important; margin-bottom: 3px !important; } +.pm-bubble .pm-bubble-sender a { color: #FFA500 !important; } + +.pm-bubble .message-text { font-size: 15px !important; line-height: 1.42 !important; color: #ececec !important; margin: 0 !important; } +.pm-bubble .message-text a { color: #FFD700 !important; } + +.pm-bubble .pm-bubble-meta { + display: flex !important; flex-wrap: wrap !important; align-items: center !important; + gap: 6px !important; margin-top: 4px !important; font-size: 10px !important; color: var(--c-muted, #8c8c8c) !important; +} +.pm-card.pm-sent .pm-bubble-meta { justify-content: flex-end !important; } +.pm-card.pm-recv .pm-bubble-meta { justify-content: flex-start !important; } +.pm-bubble .pm-bubble-subject { font-style: italic !important; opacity: .75 !important; max-width: 50% !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } +.pm-bubble .pm-bubble-lock { opacity: .85 !important; } +.pm-bubble .pm-bubble-eco { font-size: 9px !important; opacity: .9 !important; } +.pm-bubble .pm-bubble-eco * { font-size: 9px !important; } + +.pm-bubble .pm-actions { margin-top: 6px !important; display: flex !important; gap: 6px !important; } +.pm-card.pm-sent .pm-actions { justify-content: flex-end !important; } +.pm-bubble .pm-actions .pm-btn { font-size: 10px !important; padding: 3px 8px !important; min-height: 28px !important; } +.pm-bubble .pm-actions form { margin: 0 !important; } + +.pm-bubble .pm-info-table { display: none !important; } + +.pm-thread .pm-thread-details { width: 100% !important; } +.pm-thread .pm-thread-toggle { font-size: 12px !important; color: var(--c-muted, #8c8c8c) !important; padding: 4px 0 !important; cursor: pointer !important; } + +.main-column h1, .main-column h2, .main-column h3, .main-column h4, +.tags-header h2, .tags-header h3, .title-with-chip h2, .pm-title, h2.pm-title, +.cn-name, h1.cn-name, .cn-section, h2.cn-section, .profile-side-name, h2.profile-side-name { + text-wrap: balance !important; + overflow-wrap: break-word !important; + word-break: keep-all !important; + -webkit-hyphens: none !important; hyphens: none !important; + text-align: left !important; +} +.main-column h1, .tags-header h2, .cn-name { + font-size: var(--fs-title, 20px) !important; font-weight: 700 !important; + line-height: 1.22 !important; letter-spacing: -0.01em !important; + margin: 0 0 var(--space-2, 8px) !important; +} +.main-column h2, .pm-title, .title-with-chip h2, .cn-section { + font-size: 18px !important; font-weight: 700 !important; + line-height: 1.28 !important; margin: var(--space-1, 4px) 0 var(--space-2, 8px) !important; +} +.main-column h3 { + font-size: var(--fs-body-lg, 16px) !important; font-weight: 600 !important; + line-height: 1.35 !important; margin: var(--space-1, 4px) 0 !important; +} +.main-column h1, .main-column h2, .main-column h3, +.tags-header h2, .pm-title, .cn-name, .cn-section { + max-width: 40ch !important; width: auto !important; min-width: 0 !important; +} + +.main-column h1 code, .main-column h2 code, .main-column h3 code, +.tags-header h2 code, .pm-title code, +.main-column h1 a[href*="ed25519"], .main-column h2 a[href*="ed25519"], +.main-column h1 a[href^="ssb:"], .main-column h2 a[href^="ssb:"] { + word-break: break-all !important; overflow-wrap: anywhere !important; +} + +.main-column .tags-header { + display: flex !important; flex-direction: column !important; align-items: flex-start !important; + gap: var(--space-1, 4px) !important; width: 100% !important; max-width: 100% !important; + margin: 0 0 var(--space-2, 8px) !important; +} +.main-column .tags-header h2 { margin: 0 !important; } + +.main-column .title-with-chip { + display: flex !important; flex-wrap: wrap !important; align-items: center !important; + gap: var(--space-2, 8px) !important; width: 100% !important; max-width: 100% !important; +} +.main-column .title-with-chip h2 { flex: 1 1 auto !important; min-width: 0 !important; margin: 0 !important; max-width: none !important; } +.main-column .title-with-chip > :not(h2) { flex: 0 0 auto !important; } + +.main-column p, .main-column li, +.tags-header p, .tags-header .prefs-help, .indexing-note, .indexing-percent, +.error-page-message, .welcome-text, .message-text, .pm-bubble .message-text { + text-wrap: pretty !important; + line-height: 1.55 !important; + max-width: min(66ch, 100%) !important; + overflow-wrap: anywhere !important; + word-break: normal !important; + min-width: 0 !important; + -webkit-hyphens: none !important; hyphens: none !important; +} +.main-column p + p { margin-top: var(--space-3, 16px) !important; } +.tags-header p, .tags-header .prefs-help, .indexing-note { line-height: 1.5 !important; margin: 4px 0 0 0 !important; } +.pm-bubble .message-text { line-height: 1.45 !important; max-width: 100% !important; } + +[style*="auto-fill"], +[style*="minmax(240px"], [style*="minmax(280px"], [style*="minmax(300px"], +[style*="minmax( 240px"], [style*="minmax( 280px"], [style*="minmax( 300px"], +.trending-container, .cn-hub-grid, .cn-products { + display: grid !important; grid-template-columns: 1fr !important; gap: 12px !important; + width: 100% !important; max-width: 100% !important; +} +.trending-container > *, .games-grid > *, .market-grid > *, .tribe-grid > *, +.feed-container, .feed-card, .trending-card, +.main-column article, .main-column [class*="-card"], .main-column [class*="-item"], +.main-column [class*="-entry"], .main-column .card { + width: 100% !important; max-width: 100% !important; min-width: 0 !important; + box-sizing: border-box !important; margin-left: 0 !important; margin-right: 0 !important; +} +.feed-card .feed-text, .trending-card .feed-text, +.main-column article p, .main-column [class*="-card"] p { max-width: var(--measure, 66ch) !important; } +.main-column form, .comment-form, .comment-form-wrapper, .market-form, +.new-message-form, .project-control-form, .filter-box { + display: block !important; width: 100% !important; max-width: 100% !important; + min-width: 0 !important; box-sizing: border-box !important; +} +.main-column input:not([type="checkbox"]):not([type="radio"]):not(.oasis-nav-toggle):not(.oasis-nav-side-toggle), +.main-column textarea, .main-column select { + width: 100% !important; max-width: 100% !important; min-width: 0 !important; box-sizing: border-box !important; +} +.main-column form > button:only-of-type, .main-column form > .create-button, +.comment-form button[type="submit"], .market-form button[type="submit"], +.new-message-form button[type="submit"] { + display: block !important; width: 100% !important; max-width: 100% !important; + box-sizing: border-box !important; margin-top: 8px !important; +} +[style*="width:33%"], [style*="width: 33%"], [style*="width:25%"], [style*="width: 25%"], +[style*="width:60%"], [style*="width: 60%"], [style*="width:40%"], [style*="width: 40%"] { + width: 100% !important; max-width: 100% !important; +} +.main-column [style*="max-width: 800px"], .main-column [style*="max-width:800px"], +.ai-response, .ai-train-bar { + max-width: 100% !important; width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; box-sizing: border-box !important; +} +.message-list, .pm-thread, .pm-thread-replies { width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; } + +.main-column p .user-link, .main-column p a[href^="/author/"], +.main-column li .user-link, .main-column span .user-link, +.main-column .message-text .user-link, .main-column .message-text a[href^="/author/"], +.pm-bubble .message-text .user-link, .pm-bubble .message-text a[href^="/author/"], +.pm-quote a[href^="/author/"] { + display: inline !important; white-space: normal !important; + overflow-wrap: anywhere !important; word-break: keep-all !important; + -webkit-hyphens: none !important; hyphens: none !important; +} + +.peer-key, .user-link.peer-key, .block-info-table td .peer-key, .pm-info-table td .peer-key, +.pm-info-table td .user-link, .block-info-table td .user-link, +.search-oasis-id, .pm-sent-key-value, .kpi__value { + display: inline-block !important; max-width: 100% !important; + overflow-wrap: anywhere !important; word-break: break-all !important; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; + font-variant-numeric: tabular-nums !important; line-height: 1.45 !important; +} + +.oasis-id-copy { + display: block !important; width: 100% !important; white-space: nowrap !important; + overflow: hidden !important; text-overflow: ellipsis !important; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; +} +.invites-list .user-link.peer-key, .peers-list .block-info-table td .peer-key { + display: block !important; max-width: 100% !important; white-space: nowrap !important; + overflow: hidden !important; text-overflow: ellipsis !important; word-break: normal !important; +} + +.main-column .message-text a[href^="http"], .main-column p a[href^="http"], +.pm-bubble .message-text a[href^="http"], .main-column .message-text a.oasis-path-link, +.main-column .message-text a.job-link, .main-column .message-text a.project-link, +.main-column .message-text a.market-link { + overflow-wrap: anywhere !important; word-break: keep-all !important; + -webkit-hyphens: none !important; hyphens: none !important; +} +.main-column code:not(pre code), .pm-bubble .message-text code { + display: inline !important; max-width: 100% !important; + overflow-wrap: anywhere !important; word-break: break-word !important; white-space: pre-wrap !important; +} + +.main-column .tags-header { margin-bottom: 6px !important; gap: 2px !important; } +.main-column .tags-header h2, .tags-header h2 { + font-size: 17px !important; line-height: 1.2 !important; letter-spacing: 0 !important; +} +.main-column .tags-header p { font-size: 13px !important; line-height: 1.35 !important; margin: 0 !important; } + +.main-column { padding: 10px 12px 16px 12px !important; } +.activity-filter-acc { margin-bottom: 4px !important; } + +.empty-state { + display: flex !important; flex-direction: column !important; align-items: center !important; + justify-content: center !important; text-align: center !important; + padding: 40px 16px !important; gap: 8px !important; width: 100% !important; box-sizing: border-box !important; +} +.empty-state-icon { font-size: 44px !important; line-height: 1 !important; opacity: 0.55 !important; } +.empty-state-title { font-size: 16px !important; font-weight: 700 !important; margin: 4px 0 0 0 !important; color: #eee !important; } +.empty-state-text { color: var(--c-muted, #8c8c8c) !important; font-size: 14px !important; line-height: 1.4 !important; max-width: 40ch !important; margin: 0 !important; } +.empty-state-cta { margin-top: 10px !important; min-height: 48px !important; display: inline-flex !important; align-items: center !important; } + +.activity-filter-collapsed { + display: flex !important; align-items: center !important; gap: 10px !important; + flex-wrap: wrap !important; margin-bottom: 8px !important; width: 100% !important; +} +.activity-back { + display: inline-flex !important; align-items: center !important; gap: 4px !important; + min-height: 44px !important; padding: 6px 14px 6px 10px !important; flex: 0 0 auto !important; +} +.activity-back-arrow { font-size: 22px !important; line-height: 1 !important; font-weight: bold !important; } +.activity-active-filter { + font-weight: 700 !important; color: #FFA500 !important; text-transform: uppercase !important; + font-size: 13px !important; letter-spacing: .03em !important; +} + +.oasis-bottombar .bb-item { min-height: 48px !important; } +.oasis-bottombar .bb-pair .bb-item { min-height: 44px !important; padding: 4px 10px !important; } +.oasis-mobile-menus .oasis-menu-btn { min-height: 48px !important; } +.pm-bubble .pm-actions .pm-btn { min-height: 40px !important; padding: 6px 12px !important; } +.score-btn, .vote-btn, .feed-action-btn { + min-height: 44px !important; min-width: 44px !important; + display: inline-flex !important; align-items: center !important; justify-content: center !important; +} +.score-btn:active, .vote-btn:active, .feed-action-btn:active { filter: brightness(.85) !important; } + +.oasis-bottombar .bb-add .bb-ico { + background: #FFA500 !important; color: #121212 !important; border-radius: 999px !important; + width: 30px !important; height: 30px !important; display: inline-flex !important; + align-items: center !important; justify-content: center !important; + font-weight: bold !important; font-size: 20px !important; line-height: 1 !important; +} +.bb-pick-grid { + display: grid !important; grid-template-columns: repeat(3, 1fr) !important; + gap: 8px !important; width: 100% !important; margin-top: 8px !important; +} +.bb-pick-form { margin: 0 !important; display: block !important; } +.bb-pick-btn { + display: flex !important; flex-direction: column !important; align-items: center !important; + justify-content: center !important; gap: 6px !important; width: 100% !important; + min-height: 76px !important; padding: 10px 6px !important; box-sizing: border-box !important; + background: #1c1c1c !important; border: 1px solid #333 !important; border-radius: 12px !important; color: #eee !important; +} +.bb-pick-btn.active { border-color: #FFA500 !important; background: #2a2412 !important; } +.bb-pick-ico { font-size: 26px !important; color: #FFA500 !important; line-height: 1 !important; } +.bb-pick-lbl { font-size: 11px !important; text-align: center !important; line-height: 1.2 !important; } + diff --git a/src/client/assets/themes/Purple-SNH.css b/src/client/assets/themes/Purple-SNH.css deleted file mode 100644 index 67cd225..0000000 --- a/src/client/assets/themes/Purple-SNH.css +++ /dev/null @@ -1,398 +0,0 @@ -body { - background-color: #4B0A6D; - color: #E5E5E5; - font-family: 'Arial', sans-serif; -} - -footer { - border-top: 2px solid #9B1C96; -} - -.sidebar-left, .sidebar-right { - background-color: #39006D; - border: 1px solid #9B1C96; - color: #E5E5E5; - padding: 15px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); -} - -.main-column { - background-color: #1A1A1A; - border: 1px solid #9B1C96; - padding: 20px; - color: #E5E5E5; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); -} - -button, input[type="submit"], input[type="button"] { - background-color: #9B1C96; - color: #E5E5E5; - border: 2px solid #6A0066; - border-radius: 10px; - padding: 12px 24px; - cursor: pointer; -} - -button:hover, input[type="submit"]:hover, input[type="button"]:hover { - background-color: #6A0066; - border-color: #9B1C96; - box-shadow: 0 0 15px rgba(155, 28, 150, 0.8); -} - -input, textarea, select { - background-color: #333333; - color: #E5E5E5; - border: 1px solid #9B1C96; - border-radius: 5px; - padding: 12px; -} - -input:focus, textarea:focus, select:focus { - background-color: #39006D; - outline: none; -} - -a { - color: #9B1C96; - text-decoration: none; -} - -a:hover { - color: #E5E5E5; -} - -table { - background-color: #1A1A1A; - color: #E5E5E5; - width: 100%; - border-collapse: collapse; -} - -table th { - background-color: #6A0066; -} - -table tr:nth-child(even) { - background-color: #333333; -} - -nav ul { - background-color: #39006D; - padding: 0; - margin: 0; - list-style: none; -} - -nav ul li { - display: inline-block; - margin-right: 10px; -} - -nav ul li a { - color: #E5E5E5; - padding: 15px; - display: inline-block; - text-transform: none; - font-weight: bold; - letter-spacing: 0; -} - -.profile { - background-color: #333333; - color: #E5E5E5; - padding: 20px; - border-radius: 8px; - box-shadow: 0 4px 15px rgba(0,0,0,0.3); -} - -.profile .name { - color: #9B1C96; - font-size: 20px; - font-weight: bold; -} - -.avatar { - border: 4px solid #9B1C96; - border-radius: 50%; - width: 80px; - height: 80px; - margin-bottom: 10px; -} - -article, section { - background-color: #333333; - color: #E5E5E5; - padding: 20px; - border-radius: 10px; - box-shadow: 0 4px 15px rgba(0,0,0,0.3); -} - -.post-preview { - background-color: #39006D; - padding: 15px; - border-radius: 8px; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); -} - -.post-preview img { - border-radius: 8px; - max-width: 100%; -} - -::-webkit-scrollbar-thumb { - background-color: #9B1C96; -} - -::-webkit-scrollbar-track { - background-color: #1A1A1A; -} - -.action-container { - background-color: #333333; - border-color: #9B1C96; - color: #E5E5E5; -} - -footer a { - background-color: #9B1C96; - color: #FFFFFF; - padding: 8px 20px; - border-radius: 5px; - text-decoration: none; -} - -footer a:hover { - background-color: #6A0066; -} - - -.sidebar-left nav ul li a, -.sidebar-right nav ul li a, -.header nav ul li a { - background-color: #682B94 !important; - color: #FFE082 !important; - border: 1px solid #B86ADE !important; - font-weight: 600; - border-radius: 6px; - padding: 12px 16px; - display: flex; - justify-content: flex-start; - box-sizing: border-box; - transition: background-color 0.2s ease, border-color 0.2s ease; -} - -.sidebar-left nav ul li a:hover, -.sidebar-right nav ul li a:hover, -.header nav ul li a:hover { - background-color: #682B94 !important; - border-color: #FFD54F !important; - color: #FFFFFF !important; -} - -body { - background-color: #2D0B47 !important; -} - -.main-column, -article, -section, -.action-container, -.profile, -.post-preview { - background-color: #3C1360 !important; - border-color: #B86ADE !important; - color: #FFEEDB !important; -} - -input, -textarea, -select { - background-color: #4B1A72 !important; - color: #FFFFFF !important; - border-color: #BB5EFF !important; -} - -button, -input[type="submit"], -input[type="button"] { - background-color: #A34AD8 !important; - color: #FFFFFF !important; - border-color: #751E9F !important; -} - -header { - background-color: #5A1A85 !important; - border-bottom: 1px solid #B86ADE !important; - box-shadow: none !important; -} - -.header { - background-color: #5A1A85 !important; -} - -.top-bar-left, -.top-bar-mid, -.top-bar-right { - background-color: #39006D !important; - border: 1px solid #9B1C96 !important; - padding: 15px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); - display: flex; - gap: 12px; - border-radius: 10px; -} - -.sidebar-left, -.sidebar-right { - padding: 16px; - box-sizing: border-box; -} - -.sidebar-left nav ul, -.sidebar-right nav ul { - display: flex; - flex-direction: column; - gap: 10px; - margin: 0; - padding: 0; -} - -.sidebar-left nav ul li, -.sidebar-right nav ul li { - width: 100%; -} - -.card { - color: #E5E5E5; - box-shadow: 0 4px 30px 0 rgba(0, 0, 0, 0.2); - background-color: #3C1360; - border-color: #B86ADE; -} - -.card-label { - color: #9B1C96; -} - -.card-footer { - color: #B86ADE; -} - -.card-tags a.tag-link { - color: #FFFFFF; - background: #D94F4F; -} - -.card-tags a.tag-link:hover { - background: #B86ADE; - color: #E5E5E5; - cursor: pointer; -} - -a.user-link { - background-color: #FFD600; - color: #3C1360; - border-color: #FFD600; - transition: background-color 0.3s, color 0.3s, border-color 0.3s; -} - -a.user-link:hover { - background-color: #FFB600; - border-color: #FFB600; - color: #3C1360; - cursor: pointer; -} - -a.user-link:focus { - background-color: #9A2F2F; - border-color: #9A2F2F; - color: #E5E5E5; -} - -.date-link { - background-color: #2F3C32; - color: #fff; -} - -.date-link:hover { - background-color: #3E4A3D; - color: #fff; -} - -.activitySpreadInhabitant2 { - background-color: #3E4A3D; - color: #fff; -} - -.activityVotePost { - background-color: #3B5C42; - color: #fff; -} - -.update-banner { - background-color: #1a0025; - border-bottom-color: #3d004d; - color: #E5E5E5; -} - -.update-banner-link { - color: #c44bc1; -} - -.snh-invite-code { - color: #9B1C96 !important; -} - -.stats-kpi-label { color: #B86ADE !important; } -.stats-kpi-value { color: #FFEEDB !important; } -.carbon-bar-note, .carbon-bar-formula { color: #FFEEDB !important; } -.graphos-node-label { fill: #FFEEDB !important; } -.graphos-node-label-me { fill: #ffa500 !important; } -.graphos-legend { color: #FFEEDB !important; } -/* Blockexplorer */ -.blockchain-view { background-color: #2D0B47 !important; color: #FFEEDB !important; } -.block { background: #3C1360 !important; border: 1px solid #B86ADE !important; } -.block:hover { box-shadow: 0 0 15px rgba(184,106,222,0.3) !important; } -.blockchain-card-label, .block-info-table .card-label, .pm-info-table .card-label, .block-content-label { color: #B86ADE !important; } -.blockchain-card-value, .block-info-table .card-value, .pm-info-table .card-value, .block-timestamp, .json-content { color: #FFEEDB !important; } -.block-content-preview, .block-content { background: #4B1A72 !important; color: #FFEEDB !important; } -.block-author { color: #FFD600 !important; background: rgba(255,214,0,0.08) !important; } -.block-author:hover { color: #FFB600 !important; } -.block-url { color: #B86ADE !important; } -.block-row--details .block-url { background: #4B1A72 !important; } -.block-row--details .block-url:hover { background: #5A1A85 !important; color: #FFD600 !important; } -.btn-singleview { background: #4B1A72 !important; color: #B86ADE !important; } -.btn-singleview:hover { background: #5A1A85 !important; color: #FFD600 !important; } -.btn-spread-on, .content-actions .btn-pin-on { background: #B86ADE !important; color: #2A0E42 !important; } -.btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #CE8CE9 !important; color: #2A0E42 !important; } -.btn-back { background: #A34AD8 !important; color: #FFFFFF !important; } -.btn-back:hover { background: #751E9F !important; color: #FFFFFF !important; } -.block-info-table td, .pm-info-table td { border-color: #B86ADE !important; } -.block-diagram { border-color: #B86ADE !important; background: #2D0B47 !important; } -.block-diagram-ruler { color: #B86ADE !important; background: #1A0030 !important; border-bottom-color: #B86ADE !important; } -.block-diagram-ruler span { color: #B86ADE !important; } -.block-diagram-cell { border-color: #B86ADE !important; background: #3C1360 !important; } -.bd-label { color: #B86ADE !important; } -.bd-value { color: #FFEEDB !important; } -.deleted-label { color: #ff5555 !important; } - -/* Tribes */ -.tribe-card { background: #3C1360 !important; border-color: #B86ADE !important; } -.tribe-card:hover { box-shadow: 0 0 15px rgba(184,106,222,0.3) !important; } -.tribe-card-title { color: #FFEEDB !important; } -.tribe-card-description { color: #FFEEDB !important; } -.tribe-info-table td { border-color: #B86ADE !important; } -.tribe-info-label { color: #B86ADE !important; background: #3C1360 !important; } -.tribe-info-value { color: #FFEEDB !important; background: #3C1360 !important; } -.tribe-info-empty { color: #8844aa !important; } -.tribe-card-members { border-color: #B86ADE !important; background: #2D0B47 !important; } -.tribe-members-count { color: #FFD600 !important; } -.tribe-card-actions { border-color: #B86ADE !important; background: #2D0B47 !important; } -.tribe-action-btn { border-color: #B86ADE !important; color: #B86ADE !important; } -.tribe-action-btn:hover { background: #B86ADE !important; color: #000 !important; } -.tribe-subtribe-link { background: #4B1A72 !important; border-color: #B86ADE !important; color: #FFD600 !important; } -.tribe-thumb-link { border-color: #B86ADE !important; } -.tribe-thumb-link:hover { border-color: #FFD600 !important; } -.tribe-subtribe-link:hover { background: #5A1A85 !important; } -.tribe-parent-image { border-color: #B86ADE !important; } -.tribe-parent-box { background: #3C1360 !important; } -.trending-card.own-content { box-shadow: inset 3px 0 0 #B86ADE; } diff --git a/src/client/assets/translations/i18n.js b/src/client/assets/translations/i18n.js index 2ed00fb..129e820 100644 --- a/src/client/assets/translations/i18n.js +++ b/src/client/assets/translations/i18n.js @@ -12,9 +12,4 @@ languages.forEach(language => { } }); -try { - const overlay = require('./i18n_fork.js'); - languages.forEach(l => Object.assign(i18n[l] = i18n[l] || {}, overlay[l] || overlay.en || {})); -} catch (e) {} - module.exports = i18n; diff --git a/src/client/assets/translations/i18n_fork.js b/src/client/assets/translations/i18n_fork.js deleted file mode 100644 index 35b2dc4..0000000 --- a/src/client/assets/translations/i18n_fork.js +++ /dev/null @@ -1,718 +0,0 @@ -// Claves de traduccion propias de la rama experimental. -// Se fusionan sobre las de upstream desde i18n.js, para no editar los 11 -// 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, -// 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...). -// 3. Textos de upstream que la rama corrige (el grupo spread en castellano). -// -// Los nombres de las categorias NO se tocan: son los de epsylon y se leen de -// upstream, para que la rama no se separe de el en algo tan visible. -// -// Los 11 idiomas estan completos. Si anades una clave, anadela en los 11. - - -module.exports = { - en: { - // 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", - identitiesCreate: "Create identity", - identitiesDescription: "Each identity lives in its own folder, with its own key, database and blobs. The change applies on restart.", - identitiesInvalidName: "Invalid name. Letters, numbers, hyphen and underscore, up to 32.", - identitiesNewName: "Name for the new identity", - identitiesRestart: "Restart now", - identitiesRestartNeeded: "Selected. Restart Oasis to enter with this identity.", - identitiesRestarting: "Restarting. Open Oasis again in a few seconds.", - identitiesSameFeed: "Shares key with another identity: never publish from both.", - identitiesTitle: "Identities", - identitiesUse: "Use this one", - // Barra inferior - bbAdd: "Add", - bbDone: "Done", - bbFull: "Maximum reached", - bbManage: "Edit", - bbPickDesc: "Choose up to 4 shortcuts for the bottom bar.", - bbPickNone: "None", - bbPickTitle: "Customize the bar", - bbQuickActions: "Quick actions", - bbRemove: "Remove", - bbYourBar: "Your bar", - // Menu y filtros - menuAll: "All", - menuBlogs: "Blogs", - menuSocial: "Social", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "Category", - bookmarkCategoryPlaceholder: "Optional", - chatReply: "Reply", - chatShareUrl: "Share URL", - filter: "Filter", - forumFilterHot: "Hot", - peerLastChange: "Last change", - profileVisibilityDevice: "Device", - publishBlog: "Publish Blog", - spreadHint: "Spread this to your supporters (replicates via your feed).", - spreadMore: "more" - }, - es: { - // 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", - identitiesCreate: "Crear identidad", - identitiesDescription: "Cada identidad vive en su propia carpeta, con su clave, su base de datos y sus blobs. El cambio se aplica al reiniciar.", - identitiesInvalidName: "Nombre no válido. Letras, números, guion y guion bajo, hasta 32.", - identitiesNewName: "Nombre de la nueva identidad", - identitiesRestart: "Reiniciar ahora", - identitiesRestartNeeded: "Seleccionada. Reinicia Oasis para entrar con esta identidad.", - identitiesRestarting: "Reiniciando. Vuelve a abrir Oasis en unos segundos.", - identitiesSameFeed: "Comparte clave con otra identidad: no publiques nunca desde las dos.", - identitiesTitle: "Identidades", - identitiesUse: "Usar esta", - // Barra inferior - bbAdd: "Añadir", - bbDone: "Hecho", - bbFull: "Máximo alcanzado", - bbManage: "Editar", - bbPickDesc: "Elige hasta 4 accesos directos para la barra inferior.", - bbPickNone: "Ninguno", - bbPickTitle: "Personalizar la barra", - bbQuickActions: "Acciones rápidas", - bbRemove: "Quitar", - bbYourBar: "Tu barra", - // Menu y filtros - menuAll: "Todo", - menuBlogs: "Blogs", - menuSocial: "Social", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "Categoría", - bookmarkCategoryPlaceholder: "Opcional", - chatReply: "Responder", - chatShareUrl: "Compartir enlace", - filter: "Filtrar", - forumFilterHot: "Candente", - peerLastChange: "Último cambio", - profileVisibilityDevice: "Dispositivo", - publishBlog: "Publicar blog", - spreadHint: "Difunde esto entre quienes te siguen (se replica por tu feed).", - spreadMore: "más", - // Correcciones al castellano de upstream - spreadChron: "Difusión", - spreadLabel: "Difundir", - spreaded: "Difundido", - spreadedDescription: "Lista de posts replicados por habitante.", - totalspreads: "Réplicas totales" - }, - fr: { - // 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é", - identitiesCreate: "Créer une identité", - identitiesDescription: "Chaque identité vit dans son propre dossier, avec sa clé, sa base de données et ses blobs. Le changement s'applique au redémarrage.", - identitiesInvalidName: "Nom invalide. Lettres, chiffres, tiret et tiret bas, jusqu'à 32.", - identitiesNewName: "Nom de la nouvelle identité", - identitiesRestart: "Redémarrer maintenant", - identitiesRestartNeeded: "Sélectionnée. Redémarrez Oasis pour entrer avec cette identité.", - identitiesRestarting: "Redémarrage. Rouvrez Oasis dans quelques secondes.", - identitiesSameFeed: "Partage sa clé avec une autre identité : ne publiez jamais depuis les deux.", - identitiesTitle: "Identités", - identitiesUse: "Utiliser celle-ci", - // Barra inferior - bbAdd: "Ajouter", - bbDone: "Terminé", - bbFull: "Maximum atteint", - bbManage: "Modifier", - bbPickDesc: "Choisissez jusqu'à 4 raccourcis pour la barre du bas.", - bbPickNone: "Aucun", - bbPickTitle: "Personnaliser la barre", - bbQuickActions: "Actions rapides", - bbRemove: "Retirer", - bbYourBar: "Votre barre", - // Menu y filtros - menuAll: "Tout", - menuBlogs: "Blogs", - menuSocial: "Social", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "Catégorie", - bookmarkCategoryPlaceholder: "Facultatif", - chatReply: "Répondre", - chatShareUrl: "Partager le lien", - filter: "Filtrer", - forumFilterHot: "Populaire", - peerLastChange: "Dernier changement", - profileVisibilityDevice: "Appareil", - publishBlog: "Publier le blog", - spreadHint: "Diffusez ceci auprès de vos soutiens (se réplique via votre flux).", - spreadMore: "plus" - }, - eu: { - // 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", - identitiesCreate: "Sortu nortasuna", - identitiesDescription: "Nortasun bakoitza bere karpetan bizi da, bere gakoarekin, datu-basearekin eta blob-ekin. Aldaketa berrabiaraztean aplikatzen da.", - identitiesInvalidName: "Izen baliogabea. Letrak, zenbakiak, marratxoa eta azpimarra, 32 arte.", - identitiesNewName: "Nortasun berriaren izena", - identitiesRestart: "Berrabiarazi orain", - identitiesRestartNeeded: "Hautatuta. Berrabiarazi Oasis nortasun honekin sartzeko.", - identitiesRestarting: "Berrabiarazten. Ireki Oasis berriro segundo batzuk barru.", - identitiesSameFeed: "Beste nortasun batekin gakoa partekatzen du: ez argitaratu inoiz bietatik.", - identitiesTitle: "Nortasunak", - identitiesUse: "Erabili hau", - // Barra inferior - bbAdd: "Gehitu", - bbDone: "Eginda", - bbFull: "Gehienekora iritsi zara", - bbManage: "Editatu", - bbPickDesc: "Aukeratu 4 lasterbide arte beheko barrarako.", - bbPickNone: "Bat ere ez", - bbPickTitle: "Pertsonalizatu barra", - bbQuickActions: "Ekintza bizkorrak", - bbRemove: "Kendu", - bbYourBar: "Zure barra", - // Menu y filtros - menuAll: "Dena", - menuBlogs: "Blogak", - menuSocial: "Soziala", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "Kategoria", - bookmarkCategoryPlaceholder: "Aukerakoa", - chatReply: "Erantzun", - chatShareUrl: "Partekatu esteka", - filter: "Iragazi", - forumFilterHot: "Bero", - peerLastChange: "Azken aldaketa", - profileVisibilityDevice: "Gailua", - publishBlog: "Argitaratu bloga", - spreadHint: "Zabaldu hau zure jarraitzaileen artean (zure jarioaren bidez errepikatzen da).", - spreadMore: "gehiago" - }, - de: { - // 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", - identitiesCreate: "Identität erstellen", - identitiesDescription: "Jede Identität liegt in einem eigenen Ordner, mit eigenem Schlüssel, eigener Datenbank und eigenen Blobs. Die Änderung greift beim Neustart.", - identitiesInvalidName: "Ungültiger Name. Buchstaben, Ziffern, Bindestrich und Unterstrich, bis zu 32.", - identitiesNewName: "Name der neuen Identität", - identitiesRestart: "Jetzt neu starten", - identitiesRestartNeeded: "Ausgewählt. Starte Oasis neu, um mit dieser Identität einzutreten.", - identitiesRestarting: "Neustart. Öffne Oasis in ein paar Sekunden erneut.", - identitiesSameFeed: "Teilt den Schlüssel mit einer anderen Identität: niemals von beiden veröffentlichen.", - identitiesTitle: "Identitäten", - identitiesUse: "Diese verwenden", - // Barra inferior - bbAdd: "Hinzufügen", - bbDone: "Fertig", - bbFull: "Maximum erreicht", - bbManage: "Bearbeiten", - bbPickDesc: "Wähle bis zu 4 Verknüpfungen für die untere Leiste.", - bbPickNone: "Keine", - bbPickTitle: "Leiste anpassen", - bbQuickActions: "Schnellzugriff", - bbRemove: "Entfernen", - bbYourBar: "Deine Leiste", - // Menu y filtros - menuAll: "Alle", - menuBlogs: "Blogs", - menuSocial: "Sozial", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "Kategorie", - bookmarkCategoryPlaceholder: "Optional", - chatReply: "Antworten", - chatShareUrl: "Link teilen", - filter: "Filtern", - forumFilterHot: "Angesagt", - peerLastChange: "Letzte Änderung", - profileVisibilityDevice: "Gerät", - publishBlog: "Blog veröffentlichen", - spreadHint: "Verbreite dies an deine Unterstützer (repliziert über deinen Feed).", - spreadMore: "mehr" - }, - it: { - // 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à", - identitiesCreate: "Crea identità", - identitiesDescription: "Ogni identità vive nella sua cartella, con la sua chiave, il suo database e i suoi blob. Il cambio si applica al riavvio.", - identitiesInvalidName: "Nome non valido. Lettere, numeri, trattino e trattino basso, fino a 32.", - identitiesNewName: "Nome della nuova identità", - identitiesRestart: "Riavvia ora", - identitiesRestartNeeded: "Selezionata. Riavvia Oasis per entrare con questa identità.", - identitiesRestarting: "Riavvio in corso. Riapri Oasis tra qualche secondo.", - identitiesSameFeed: "Condivide la chiave con un'altra identità: non pubblicare mai da entrambe.", - identitiesTitle: "Identità", - identitiesUse: "Usa questa", - // Barra inferior - bbAdd: "Aggiungi", - bbDone: "Fatto", - bbFull: "Massimo raggiunto", - bbManage: "Modifica", - bbPickDesc: "Scegli fino a 4 scorciatoie per la barra in basso.", - bbPickNone: "Nessuno", - bbPickTitle: "Personalizza la barra", - bbQuickActions: "Azioni rapide", - bbRemove: "Rimuovi", - bbYourBar: "La tua barra", - // Menu y filtros - menuAll: "Tutto", - menuBlogs: "Blog", - menuSocial: "Sociale", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "Categoria", - bookmarkCategoryPlaceholder: "Facoltativo", - chatReply: "Rispondi", - chatShareUrl: "Condividi link", - filter: "Filtra", - forumFilterHot: "Caldo", - peerLastChange: "Ultima modifica", - profileVisibilityDevice: "Dispositivo", - publishBlog: "Pubblica blog", - spreadHint: "Diffondi questo tra chi ti segue (si replica tramite il tuo feed).", - spreadMore: "altro" - }, - pt: { - // 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", - identitiesCreate: "Criar identidade", - identitiesDescription: "Cada identidade vive na sua própria pasta, com a sua chave, base de dados e blobs. A mudança aplica-se ao reiniciar.", - identitiesInvalidName: "Nome inválido. Letras, números, hífen e sublinhado, até 32.", - identitiesNewName: "Nome da nova identidade", - identitiesRestart: "Reiniciar agora", - identitiesRestartNeeded: "Selecionada. Reinicia o Oasis para entrar com esta identidade.", - identitiesRestarting: "A reiniciar. Volta a abrir o Oasis dentro de alguns segundos.", - identitiesSameFeed: "Partilha a chave com outra identidade: nunca publiques a partir das duas.", - identitiesTitle: "Identidades", - identitiesUse: "Usar esta", - // Barra inferior - bbAdd: "Adicionar", - bbDone: "Concluído", - bbFull: "Máximo atingido", - bbManage: "Editar", - bbPickDesc: "Escolhe até 4 atalhos para a barra inferior.", - bbPickNone: "Nenhum", - bbPickTitle: "Personalizar a barra", - bbQuickActions: "Ações rápidas", - bbRemove: "Remover", - bbYourBar: "A tua barra", - // Menu y filtros - menuAll: "Tudo", - menuBlogs: "Blogues", - menuSocial: "Social", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "Categoria", - bookmarkCategoryPlaceholder: "Opcional", - chatReply: "Responder", - chatShareUrl: "Partilhar ligação", - filter: "Filtrar", - forumFilterHot: "Em alta", - peerLastChange: "Última alteração", - profileVisibilityDevice: "Dispositivo", - publishBlog: "Publicar blogue", - spreadHint: "Difunde isto entre quem te segue (replica-se pelo teu feed).", - spreadMore: "mais" - }, - zh: { - // 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: "导出或导入身份", - identitiesCreate: "创建身份", - identitiesDescription: "每个身份都有自己的文件夹,包含各自的密钥、数据库和 blob。更改在重启后生效。", - identitiesInvalidName: "名称无效。字母、数字、连字符和下划线,最多 32 个字符。", - identitiesNewName: "新身份的名称", - identitiesRestart: "立即重启", - identitiesRestartNeeded: "已选择。重启 Oasis 以使用该身份进入。", - identitiesRestarting: "正在重启。请过几秒再打开 Oasis。", - identitiesSameFeed: "与另一个身份共用密钥:切勿从两者同时发布。", - identitiesTitle: "身份", - identitiesUse: "使用这个", - // Barra inferior - bbAdd: "添加", - bbDone: "完成", - bbFull: "已达上限", - bbManage: "编辑", - bbPickDesc: "为底部工具栏选择最多 4 个快捷方式。", - bbPickNone: "无", - bbPickTitle: "自定义工具栏", - bbQuickActions: "快捷操作", - bbRemove: "移除", - bbYourBar: "你的工具栏", - // Menu y filtros - menuAll: "全部", - menuBlogs: "博客", - menuSocial: "社群", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "分类", - bookmarkCategoryPlaceholder: "可选", - chatReply: "回复", - chatShareUrl: "分享链接", - filter: "筛选", - forumFilterHot: "热门", - peerLastChange: "最后变更", - profileVisibilityDevice: "设备", - publishBlog: "发布博客", - spreadHint: "把它扩散给支持你的人(通过你的 feed 复制传播)。", - spreadMore: "更多" - }, - ar: { - // 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: "تصدير هوية أو استيرادها", - identitiesCreate: "إنشاء هوية", - identitiesDescription: "كل هوية تعيش في مجلدها الخاص، بمفتاحها وقاعدة بياناتها وملفاتها. يسري التغيير عند إعادة التشغيل.", - identitiesInvalidName: "اسم غير صالح. حروف وأرقام وشرطة وشرطة سفلية، حتى 32 حرفًا.", - identitiesNewName: "اسم الهوية الجديدة", - identitiesRestart: "أعد التشغيل الآن", - identitiesRestartNeeded: "تم الاختيار. أعد تشغيل Oasis للدخول بهذه الهوية.", - identitiesRestarting: "جارٍ إعادة التشغيل. افتح Oasis مجددًا بعد ثوانٍ.", - identitiesSameFeed: "تشترك في المفتاح مع هوية أخرى: لا تنشر أبدًا من كلتيهما.", - identitiesTitle: "الهويات", - identitiesUse: "استخدم هذه", - // Barra inferior - bbAdd: "إضافة", - bbDone: "تم", - bbFull: "بلغت الحد الأقصى", - bbManage: "تحرير", - bbPickDesc: "اختر حتى 4 اختصارات للشريط السفلي.", - bbPickNone: "لا شيء", - bbPickTitle: "تخصيص الشريط", - bbQuickActions: "إجراءات سريعة", - bbRemove: "إزالة", - bbYourBar: "شريطك", - // Menu y filtros - menuAll: "الكل", - menuBlogs: "المدونات", - menuSocial: "اجتماعي", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "الفئة", - bookmarkCategoryPlaceholder: "اختياري", - chatReply: "رد", - chatShareUrl: "مشاركة الرابط", - filter: "تصفية", - forumFilterHot: "الأكثر تفاعلًا", - peerLastChange: "آخر تغيير", - profileVisibilityDevice: "الجهاز", - publishBlog: "نشر المدونة", - spreadHint: "انشر هذا بين مؤيديك (يُنسخ عبر تدفقك).", - spreadMore: "المزيد" - }, - hi: { - // 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: "पहचान निर्यात या आयात करें", - identitiesCreate: "पहचान बनाएँ", - identitiesDescription: "हर पहचान अपने अलग फ़ोल्डर में रहती है, अपनी कुंजी, डेटाबेस और ब्लॉब्स के साथ। बदलाव पुनः आरंभ करने पर लागू होता है।", - identitiesInvalidName: "अमान्य नाम। अक्षर, अंक, हाइफ़न और अंडरस्कोर, अधिकतम 32।", - identitiesNewName: "नई पहचान का नाम", - identitiesRestart: "अभी पुनः आरंभ करें", - identitiesRestartNeeded: "चयनित। इस पहचान से प्रवेश करने के लिए Oasis पुनः आरंभ करें।", - identitiesRestarting: "पुनः आरंभ हो रहा है। कुछ सेकंड बाद Oasis फिर खोलें।", - identitiesSameFeed: "किसी अन्य पहचान से कुंजी साझा करती है: दोनों से कभी प्रकाशित न करें।", - identitiesTitle: "पहचानें", - identitiesUse: "इसका उपयोग करें", - // Barra inferior - bbAdd: "जोड़ें", - bbDone: "हो गया", - bbFull: "अधिकतम सीमा", - bbManage: "संपादित करें", - bbPickDesc: "नीचे की बार के लिए अधिकतम 4 शॉर्टकट चुनें।", - bbPickNone: "कोई नहीं", - bbPickTitle: "बार अनुकूलित करें", - bbQuickActions: "त्वरित क्रियाएँ", - bbRemove: "हटाएँ", - bbYourBar: "आपकी बार", - // Menu y filtros - menuAll: "सभी", - menuBlogs: "ब्लॉग", - menuSocial: "सामाजिक", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "श्रेणी", - bookmarkCategoryPlaceholder: "वैकल्पिक", - chatReply: "उत्तर दें", - chatShareUrl: "लिंक साझा करें", - filter: "छानें", - forumFilterHot: "चर्चित", - peerLastChange: "अंतिम बदलाव", - profileVisibilityDevice: "डिवाइस", - publishBlog: "ब्लॉग प्रकाशित करें", - spreadHint: "इसे अपने समर्थकों तक फैलाएँ (आपकी फ़ीड से प्रतिकृति होती है)।", - spreadMore: "और" - }, - ru: { - // 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: "Экспортировать или импортировать личность", - identitiesCreate: "Создать личность", - identitiesDescription: "Каждая личность живёт в своей папке — со своим ключом, базой данных и блобами. Изменение вступает в силу после перезапуска.", - identitiesInvalidName: "Недопустимое имя. Буквы, цифры, дефис и подчёркивание, до 32 символов.", - identitiesNewName: "Имя новой личности", - identitiesRestart: "Перезапустить сейчас", - identitiesRestartNeeded: "Выбрано. Перезапустите Oasis, чтобы войти с этой личностью.", - identitiesRestarting: "Перезапуск. Откройте Oasis снова через несколько секунд.", - identitiesSameFeed: "Ключ совпадает с другой личностью: никогда не публикуйте с обеих.", - identitiesTitle: "Личности", - identitiesUse: "Использовать эту", - // Barra inferior - bbAdd: "Добавить", - bbDone: "Готово", - bbFull: "Достигнут максимум", - bbManage: "Изменить", - bbPickDesc: "Выберите до 4 ярлыков для нижней панели.", - bbPickNone: "Нет", - bbPickTitle: "Настроить панель", - bbQuickActions: "Быстрые действия", - bbRemove: "Убрать", - bbYourBar: "Ваша панель", - // Menu y filtros - menuAll: "Все", - menuBlogs: "Блоги", - menuSocial: "Социальное", - // Rescatadas de 0.9.1 y otras - bookmarkCategoryLabel: "Категория", - bookmarkCategoryPlaceholder: "Необязательно", - chatReply: "Ответить", - chatShareUrl: "Поделиться ссылкой", - filter: "Фильтр", - forumFilterHot: "Горячее", - peerLastChange: "Последнее изменение", - profileVisibilityDevice: "Устройство", - publishBlog: "Опубликовать блог", - spreadHint: "Распространите это среди тех, кто вас читает (реплицируется через вашу ленту).", - spreadMore: "ещё" - } -}; diff --git a/src/client/assets/translations/oasis_ar.js b/src/client/assets/translations/oasis_ar.js index 6cb95c1..3319b82 100644 --- a/src/client/assets/translations/oasis_ar.js +++ b/src/client/assets/translations/oasis_ar.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { ar: { + bbAdd: "إضافة", + bbQuickActions: "إجراءات سريعة", + bbPickTitle: "تثبيت وحدة", + bbPickDesc: "اختر وحدة لخانة + في الشريط السفلي.", + bbPickNone: "لا شيء", + bbManage: "تعديل", + bbYourBar: "شريطك", + bbRemove: "إزالة", + bbFull: "تم بلوغ الحد الأقصى", + bbDone: "تم", + activityChangeFilter: "تغيير التصفية", + emptyConnectHint: "اتصل بـ pub للمزامنة مع الشبكة.", + emptyConnectCta: "الاتصال بـ pub", + menuCommunity: "المجتمع", + activityGroupCommunity: "المجتمع والحوكمة", + activityGroupOrg: "التنظيم", + activityGroupComm: "التواصل", + activityGroupEconomy: "الاقتصاد", + activityGroupMedia: "المحتوى والوسائط", + activityGroupOther: "أخرى", + languageName: "العربية", shopOrderStatusPaid: "تم استلام الدفع", shopOrderStatusReceived: "تم الاستلام", shopOrderMarkPaid: "تأكيد استلام الدفع", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "ليس لديك مشتريات بعد.", shopOrderPmSeller: "PM Seller", shopOrderSeller: "البائع", + shopOrderPaymentConcept: "دفعة", shopOrderInvoice: "فاتورة", shopOrderInvoiceConcept: "فاتورة", shopOrderStatusPending: "قيد الانتظار", shopOrderStatusAccepted: "مقبول", shopOrderStatusRejected: "مرفوض", shopOrderStatusShipped: "تم الشحن", + shopOrderStatusDelivered: "تم التسليم", shopOrderAccept: "قبول", shopOrderReject: "رفض", shopOrderMarkShipped: "وضع علامة شُحن", + shopOrderMarkDelivered: "وضع علامة سُلّم", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "عقد ذكي", shopOrderContractConcept: "طلب متجر", shopOrdersPending: "طلبات معلّقة", all: "الكل", @@ -50,13 +75,14 @@ module.exports = { viewJson: "عرض JSON", matchScore: "درجة التطابق", preferencesLabel: "التفضيلات", - inhabitantProfileTitle: "الساكن", + inhabitantProfileTitle: "ملف الساكن", contentWarningLabel: "تحذير المحتوى", invalidPost: "محتوى غير صالح", invalidPostHint: "هذه الرسالة تحتوي على نص فارغ/غير صالح.", spreadBy: "بواسطة", spreadChron: "نشر", spreaded: "تم النشر", + spreadMore: "المزيد", spreadContentUnavailable: "المحتوى غير متاح بعد (في انتظار النسخ)", filteredByTag: "مُصفّى حسب الوسم", filteredByTagTitle: "مُصفّى حسب الوسم", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "بدون خطورة", calendarDeleteDate: "حذف التاريخ", calendarIntervalUntil: "حتى", + bookmarkCategory: "الفئة", bookmarkLastVisit: "آخر زيارة", profileClearnetBookmarksLabel: "الإشارات المرجعية", + forumFilterHot: "رائج", invitesPort: "المنفذ", currentlyUnrecheable: "خطأ!", peerHostValidation: "عنوان IPv4 صالح (مثل 192.168.1.100) أو اسم مضيف (مثل pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "أقصى تقدير سنوي", statsCarbonNetwork: "إجمالي الشبكة", statsCarbonOfEstMax: "من السعة القصوى المقدّرة", - statsCarbonYearProgress: "من السنة المنقضية", - statsNetworkTitle: "الشبكة", - statsStorageTitle: "التخزين", - statsEcoTaxTitle: "ضريبة ECO", - statsAccountTitle: "الحساب", - statsAveragesTitle: "المتوسطات", statsCarbonOfNetwork: "من إجمالي الشبكة", statsCarbonUser: "بصمتك", statsContentCountColumn: "العدد", @@ -154,6 +176,10 @@ module.exports = { statsTopTagsTitle: "أبرز الوسوم", statsTopTypesTitle: "أبرز أنواع المحتوى", statsTotalMsgs: "إجمالي الرسائل", + feedViewDetails: "عرض التفاصيل", + eventFileLabel: "إرفاق صورة", + taskFileLabel: "إرفاق صورة", + courtsRespondentRequired: "معرّف المدعى عليه مطلوب", extended: "الكون المتعدد", extendedDescription: [ "عندما تدعم شخصًا ما، قد تقوم بتنزيل منشورات من السكان الذين يدعمونهم، وتظهر تلك المنشورات هنا، مرتبة حسب الأحدث.", @@ -197,27 +223,36 @@ module.exports = { graphosShowing: "عرض", graphosYou: "أنت", graphosTotalNodes: "إجمالي العقد", + manualMode: "الوضع اليدوي", mentions: "الإشارات", mentionsDescription: [ - strong("كل رسالة تذكرك بـ @"), - ".", + strong("منشورات تُشير إليك @"), + "، مرتبة حسب الأحدث.", ], + nextPage: "التالي", + previousPage: "السابق", noMentions: "لم تتلقَّ إشارات @ بعد.", - mentionsSearchPlaceholder: "ابحث في الإشارات...", privateReminders: "تذكيرات", inboxFiltersLabel: "المرشحات:", inboxToggleToMutuals: "التبديل إلى المتبادلين", inboxToggleToWhole: "التبديل إلى الجميع", + privateFrom: "من", + privateTo: "إلى", privateDate: "التاريخ", pmReply: "رد", pmReplies: "ردود", + pmNew: "جديد", + pmMarkRead: "تعيين كمقروء", inReplyTo: "ردًا على", pmPreview: "معاينة", pmPreviewTitle: "معاينة الرسالة", peers: "الأقران", searchDescription: "الوصف", + imageSearch: "بحث عن الصور", searchFromLabel: "من", searchToLabel: "إلى", + searchMinOpinionsLabel: "أدنى الآراء", + searchInhabitantLabel: "ساكن (Oasis ID)", searchQueryLabel: "استعلام", searchPerPageLabel: "نتائج لكل صفحة", settings: "الإعدادات", @@ -230,39 +265,68 @@ module.exports = { melodyDescription: 'شغّل لحن سلسلة الكتل خاصتك — كل كتلة تصبح نغمة.', melodyEmpty: 'لا توجد نغمات بعد — لم تنتج سلسلة الكتل خاصتك أي كتلة.', melodyCompositionTitle: 'التأليف', + melodyCompositionHelp: 'كل كتلة في سلسلة الكتل خاصتك تصبح نغمة موسيقية حسب نوعها وحجمها.', + melodyStatsTitle: 'التوزيع حسب النوع', + melodyInhabitantLabel: 'الساكن', melodyTotalBlocks: 'نغمات', + melodyType: 'النوع', + melodyCount: 'كتل', melodyFilterMine: 'لي', melodyFilterAll: 'الكل', + melodyFilterShare: 'تحميل لحن', + melodyShareTitle: 'شارك لحنك', + melodyShareHelp: 'انشر لقطة من لحنك الحالي ليتمكن الآخرون من الاستماع وإعادة المزج.', + melodyShareTitleLabel: 'العنوان (اختياري)', + melodyShareTitlePlaceholder: 'شبح بلوكتشين', + melodyShareSubmit: 'نشر اللحن', + melodyNoShared: 'لا توجد ألحان بلوكتشين بعد.', melodyRegenerate: 'إعادة التوليد', melodyDownload: "تنزيل BCS", + profileActivityTitle: 'النشاط', + profileActivityMore: 'عرض كل النشاط', profileContentSectionTitle: 'محتوى الصورة الرمزية', profileContentHelp: 'اختر أي وحدات تُعرض في صورتك الرمزية.', profileSensorsHelp: 'مقاييس اختيارية معروضة في صورتك الرمزية.', profileClearnetHelp: 'الوحدات التي يمكن الوصول إليها من خارج أوازيس.', profileHubAll: 'الكل', + profileHubTitle: 'المحتوى العام', + footerPulseTitle: 'النبض', + footerPulsePeers: 'الأقران', + footerPulseInbox: 'الوارد', + footerPulseSync: 'آخر مزامنة', + footerPulseEcoValue: 'ECO/ساعة', + footerPulseLastActivity: 'آخر نشاط', footerLicenseLabel: 'الترخيص', profileSwitchToOasis: 'العودة إلى Oasis', - profileVisibilityFediverse: "الكون المتعدد", + profileSwitchToClearnet: 'الغوص في Clearnet', + profileVisibilityFediverse: "فيديفيرس", + profileSwitchToFediverse: "انغمس في الفيديفيرس", + coordLabel: 'الإحداثي (مثال: A3)', + coordPlaceholder: 'أدخل الإحداثي', contributorsTitle: "المساهمون", + pixeliaBy: "بواسطة", colorLabel: 'اختر لونًا', paintButton: 'ارسم!', + invalidCoordinate: 'إحداثي غير صحيح', + goToMuralButton: "عرض الجدارية", totalPixels: 'إجمالي البكسلات', modules: "الوحدات", modulesViewTitle: "الوحدات", modulesViewDescription: "اضبط بيئتك بتفعيل أو تعطيل الوحدات.", inbox: "صندوق الوارد", multiverse: "الكون المتعدد", - fediverse: "الكون المتعدد", - fediverseDescription: "أدر حساباتك الأخرى في الكون المتعدد، بما في ذلك إرسال واستقبال المحتوى.", + fediverse: "فيديفيرس", + fediverseDescription: "أدر حساباتك الأخرى في الفيديفيرس، بما في ذلك إرسال واستقبال المحتوى.", fediverseStatus: "الحالة", - fediverseDisconnected: "الكون المتعدد غير متصل. تحقق من %LINK% أو حالة الاتصال.", - fediverseSettingsTitle: "الكون المتعدد", + fediverseDisconnected: "الفيديفيرس غير متصل. تحقق من %LINK% أو حالة الاتصال.", + fediverseSettingsTitle: "فيديفيرس", fediverseInstanceLabel: "العنوان", fediverseTokenLabel: "رمز الوصول", fediverseTokenHelp: "اضبط رمز الوصول الخاص بك. سيُستخدم لإدارة حساب ماستودون من داخل أوازيس.", fediverseConnect: "ربطه", fediverseConnectedAs: "متصل باسم", fediverseDisconnect: "قطع الاتصال", + fediverseEmpty: "عند ربط حسابات أخرى في الفيديفيرس من %LINK%، يمكنك إدارتها من هنا.", fediverseEmptyLink: "الإعدادات", fediverseComposePlaceholder: "بماذا تفكر؟", fediverseReplyPlaceholder: "اكتب ردك…", @@ -271,15 +335,21 @@ module.exports = { fediverseReply: "رد", fediverseBoosted: "قام بالترقية", fediverseNoPosts: "خطك الزمني فارغ.", + fediverseThread: "سلسلة", fediverseTimeline: "الخطوط الزمنية", + fediverseManageTimeline: "إدارة الخط الزمني", fediverseFollowers: "المتابعون", fediverseFollowing: "يُتابِع", fediversePosts: "المنشورات", fediverseJoined: "انضم", + fediverseOverview: "نظرة عامة", fediverseRefresh: "تحديث", fediverseWrite: "اكتب", fediversePreview: "معاينة", + fediverseEdit: "تعديل", + fediverseOpenFeed: "زيارة الخلاصة", fediverseManage: "إدارة", + fediverseStatusNotConnected: "غير متصل", fediverseError: "تعذّر الاتصال بماستودون.", fediverseErrInstance: "رابط خادم غير صالح.", fediverseErrAuth: "رمز غير صالح أو منتهٍ الصلاحية.", @@ -290,6 +360,17 @@ module.exports = { fediverseErrPost: "تعذّر النشر.", fediverseErrMedia: "تعذّر رفع الملف.", fediverseErrEmpty: "اكتب شيئاً أو أرفق ملفاً.", + popularLabel: "أبرز المنشورات", + topicsLabel: "المواضيع", + latestLabel: "الأحدث", + summariesLabel: "الملخصات", + threadsLabel: "المحادثات", + multiverseLabel: "الكون المتعدد", + inboxLabel: "صندوق الوارد", + invitesLabel: "الدعوات", + walletLabel: "المحفظة", + legacyLabel: "المفاتيح", + cipherLabel: "التشفير", bookmarksLabel: "العلامات المرجعية", videosLabel: "الفيديوهات", torrentsLabel: "التورنت", @@ -300,14 +381,18 @@ module.exports = { inhabitantsLabel: "السكان", trendingLabel: "الرائج", eventsLabel: "الأحداث", + tasksLabel: "المهام", apply: "تطبيق", menuPersonal: "شخصي", + menuContent: "المحتوى", + menuBlogs: "مدونات", menuGovernance: "الحوكمة", menuOffice: "المكتب", - menuNetwork: "المجتمع", + menuMultiverse: "الكون المتعدد", + menuNetwork: "الشبكة", menuCreative: "إبداعي", menuEconomy: "الاقتصاد", - menuMedia: "المكتبة", + menuMedia: "الوسائط", menuTools: "الأدوات", comment: "تعليق", subtopic: "موضوع فرعي", @@ -317,6 +402,13 @@ module.exports = { follow: "دعم", block: "حظر", unblock: "إلغاء الحظر", + newerPosts: "منشورات أحدث", + olderPosts: "منشورات أقدم", + feedRangeEmpty: "النطاق المحدد فارغ لهذا التغذية. جرّب عرض ", + seeFullFeed: "التغذية الكاملة", + feedEmpty: "لم تشهد شبكة Oasis أي منشورات من هذا الحساب.", + beginningOfFeed: "هذه بداية التغذية", + noNewerPosts: "لم يتم استقبال منشورات أحدث بعد.", relationshipNotFollowing: "أنت غير مدعوم", relationshipTheyFollow: "يدعمك", relationshipMutuals: "دعم متبادل", @@ -326,20 +418,25 @@ module.exports = { relationshipBlockedBy: "أنت محظور", relationshipMutualBlock: "حظر متبادل", relationshipNone: "أنت لا تدعم", + relationshipConflict: "تعارض", relationshipBlockingPost: "منشور محظور", viewLikes: "عرض الانتشارات", spreadedDescription: "قائمة المنشورات التي نشرها الساكن.", + totalspreads: "إجمالي الانتشارات", + attachFiles: "إرفاق ملفات", preview: "معاينة", publish: "كتابة", contentWarningPlaceholder: "أضف موضوعًا للمنشور (اختياري)", - publishWarningPlaceholder: "النص محدود بـ 7000 حرفًا.", - publishTooLong: "منشورك طويل جدًا. الرجاء تقصيره.", + privateWarningPlaceholder: "أضف سكانًا لإرسال منشور خاص (اختياري)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "تذكّر: بسبب تقنية البلوكتشين، بمجرد نشر المنشور لا يمكن تعديله أو حذفه.", ], commentWarning: [ "تذكّر: بسبب تقنية البلوكتشين، بمجرد نشر المنشور لا يمكن تعديله أو حذفه.", ], + commentPublic: "عام", + commentPrivate: "خاص", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -359,6 +456,7 @@ module.exports = { ".", ], publishCustom: "كتابة منشور متقدم", + subtopicLabel: "إنشاء موضوع فرعي من هذا المنشور", messagePreview: "معاينة المنشور", mentionsMatching: "الإشارات المطابقة", mentionsName: "الاسم", @@ -381,93 +479,21 @@ module.exports = { ssbLogStream: "البلوكتشين", ssbLogStreamDescription: "تكوين حد الرسائل لتدفقات البلوكتشين.", saveSettings: "حفظ الإعدادات", + exportTitle: "تصدير البيانات", exportDescription: "عيّن كلمة مرور (32 حرفًا على الأقل) لتشفير مفتاحك", exportDataTitle: "نسخة احتياطية", exportDataDescription: "تنزيل بياناتك (بدون المفتاح السري!)", exportDataButton: "تنزيل قاعدة البيانات", + pubWallet: "محفظة PUB", + pubWalletDescription: "عيّن عنوان محفظة PUB. سيُستخدم لمعاملات PUB (بما في ذلك الدخل الأساسي الشامل).", + pubWalletConfiguration: "حفظ الإعدادات", + importTitle: "استيراد البيانات", importDescription: "استورد مفتاحك السري المشفر (المفتاح الخاص) لتفعيل صورتك الرمزية", + importAttach: "إرفاق ملف مشفر (.enc)", + passwordLengthInfo: "يجب أن تكون كلمة المرور 32 حرفًا على الأقل.", passwordImport: "اكتب كلمة المرور لفك تشفير البيانات التي سيتم حفظها في مجلد النظام الرئيسي (الاسم: secret)", exportPasswordPlaceholder: "استخدم أحرفًا صغيرة وكبيرة وأرقامًا ورموزًا", - fileInfo: "سيتم تنزيل مفتاحك السري المشفر إلى جهازك (الاسم: oasis.enc)", - developer: "التطوير", - devTitle: "التطوير", - welcomeBannerText: "أهلًا بك في أواسيس. اتبع هذه الخطوات السريعة لإعداد شخصيتك.", - aiSuggestionBanner: "اقتراح:", - aiSuggestionsEnable: "اقتراحات الذكاء الاصطناعي", - welcomeBannerAction: "ابدأ!", - welcomeTitle: "أهلًا بك", - welcomeStepGreetingAction: "إرسال", - welcomeJoinPubButton: "انضم إلى الـ pub", - welcomeStepLarpAction: "انضم إلى «الأكاديمية»", - welcomeStepLarpText: "انضم إلى لعبة تقمص الأدوار الحية الخاصة بنا.", - welcomeStepLarpTitle: "انضم إلى L.A.R.P.", - welcomeStepGreetingTitle: "أرسل «مرحبًا يا عالم!»", - welcomeStepGreetingText: "أرسل رسالة ليتمكن بقية السكان من اكتشافك.", - welcomeJoinPub: "انضم إلى", - welcomeDescription: "بعض الأمور التي يُستحسن فعلها قبل البدء.", - welcomeProgress: "مكتمل", - welcomeStepDone: "تم", - welcomeStepPending: "معلّق", - welcomeSkip: "ليس الآن", - welcomeStepLanguageTitle: "اختر لغتك", - welcomeStepLanguageText: "أواسيس يتحدث عدة لغات. يمكنك تغييرها متى شئت.", - welcomeStepProfileTitle: "حرّر ملفك الشخصي", - welcomeStepProfileText: "اختر اسمًا وأضف وصفًا وضع صورة ليتعرّف عليك بقية السكان.", - welcomeStepProfileAction: "حفظ الملف", - welcomeStepFederationTitle: "انضم إلى الشبكة الرئيسية", - welcomeStepFederationText: "اتصل بالـ pub الخاص بنا للقاء سكان آخرين والبدء في المزامنة.", - welcomeStepFederationAction: "الذهاب إلى الدعوات", - welcomeStepBackupTitle: "انسخ معرّفك احتياطيًا", - welcomeStepBackupText: "هويتك ملف مفاتيح على هذا الجهاز.", - welcomeStepBackupWarning: "إذا ضاع، لا يستطيع أحد استعادته لك.", - welcomeStepBackupAction: "انسخ احتياطيًا!", - welcomeCompleteTitle: "كل شيء جاهز.", - welcomeCompleteText: "عقدتك جاهزة. من هنا تنمو الشبكة مع من تتابعهم.", - welcomeFindPeople: "ابحث عن السكان", - devFilterFiles: "الملفات", - devFilterSearch: "بحث", - devFilterModules: "الوحدات", - devFilterTranslations: "الترجمات", - devFilterStyles: "الأنماط", - devFilterTests: "الاختبارات", - devVersion: "الإصدار", - devNodeVersion: "Node", - devStatsTests: "مجموعات الاختبار", - devParentFolder: "المجلد الأعلى", - devOpenFolder: "فتح", - devDescription: "استكشف الشيفرة المصدرية التي تعمل على هذه العقدة.", - devRoot: "الجذر", - devSearchPlaceholder: "النص المطلوب البحث عنه في الشيفرة", - devAnyExtension: "أي ملف", - devStatsFiles: "الملفات", - devStatsLines: "الأسطر", - devStatsModules: "الوحدات", - devStatsLanguages: "اللغات", - devNotViewable: "غير قابل للعرض", - devEmptyFolder: "هذا المجلد فارغ.", - devSize: "الحجم", - devShowing: "يعرض", - devLine: "سطر", - devReportBug: "الإبلاغ عن خلل", - devCreateTask: "إنشاء مهمة", - devRaw: "RAW", - devReportContext: "وُجد أثناء قراءة الشيفرة المصدرية لأواسيس", - devTaskPrefix: "مراجعة", - devPrevPage: "الأسطر السابقة", - devNextPage: "الأسطر التالية", - devMatches: "تطابقات", - devFilesScanned: "ملفات مفحوصة", - devNoResults: "لا توجد تطابقات بعد.", - devColModule: "الوحدة", - devColState: "الحالة", - devColModel: "النموذج", - devColView: "العرض", - devColRoutes: "المسارات", - devColTests: "الاختبارات", - devOn: "مفعّل", - devOff: "معطّل", - modulesDevLabel: "التطوير", - modulesDevDescription: "وحدة لإدارة شيفرة أواسيس المصدرية.", + fileInfo: "سيتم حفظ مفتاحك السري المشفر في مجلد النظام الرئيسي (الاسم: oasis.enc)", themeIntro: "اختر سمة.", setTheme: "تعيين السمة", @@ -475,13 +501,16 @@ module.exports = { languageDescription: "إذا كنت ترغب في استخدام لغة أخرى، اخترها هنا.", setLanguage: "تعيين اللغة", + peerConnections: "الأقران", peerConnectionsIntro: "إدارة جميع اتصالاتك مع الأقران الآخرين.", peerConnectionsTitle: "الاتصالات", online: "متصل", offline: "غير متصل", discovered: 'مُكتشَف', unknown: 'غير معروف', + lanBroadcastLabel: "بث LAN", lanBroadcastActive: "نشط (بث UDP المتعدد عبر LAN)", + lanBroadcastDisabled: "معطل (وضع pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -494,6 +523,8 @@ module.exports = { noConnections: "لا يوجد أقران متصلون.", noDiscovered: "لم يتم اكتشاف أقران.", noUnknownPeers: "لا توجد أقران غير معروفة في رسم بياني الأصدقاء.", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -503,6 +534,9 @@ module.exports = { peerImport: "استيراد", peerImportTitle: "استيراد قائمة الأقران", peerImportPlaceholder: "ألصق عنوان multiserver واحدًا لكل سطر، أو حمّل ملف .txt…", + noSupportedConnections: "لا يوجد أقران مدعومون.", + noBlockedConnections: "لا يوجد أقران محظورون.", + noRecommendedConnections: "لا يوجد أقران مُوصى بهم.", connectionActionIntro: "", startNetworking: "بدء الشبكة", @@ -516,19 +550,9 @@ module.exports = { homePageDescription: "اختر الوحدة التي تريدها كصفحتك الرئيسية.", saveHomePage: "تعيين الرئيسية", invites: "الدعوات", + invitesTitle: "الدعوات", + invitesInvites: "الدعوات", invitesDescription: "إدارة وتطبيق رموز الدعوة في شبكتك.", - invitesPubsHint: "ألصق رمز دعوة لخادم pub للاتحاد معه والبدء بنسخ سكانه.", - invitesFederationsHint: "الخوادم المتحدة معك: حدّثها، أزل غير المتاحة أو صدّر القائمة.", - invitesHousesHint: "أدخل رمز بيت للانضمام إلى بيت في LARP والمشاركة في اقتصاده.", - invitesTribesHint: "أدخل رمز قبيلة لتصبح عضوًا وتصل إلى محتواها الخاص.", - invitesChatsHint: "أدخل رمز محادثة للانضمام إلى الحوار ومواضيعه.", - invitesPadsHint: "أدخل رمز لوح للكتابة مع آخرين في مستند مشترك.", - invitesCalendarsHint: "أدخل رمز تقويم لمشاركة المواعيد والتذكيرات.", - invitesEventsHint: "أدخل رمز فعالية للانضمام إلى فعالية خاصة.", - invitesForumsHint: "أدخل رمز منتدى للقراءة والرد في منتدى خاص.", - invitesMapsHint: "أدخل رمز خريطة لرؤية الأماكن وإضافتها على خريطة مشتركة.", - invitesShopsHint: "أدخل رمز متجر للانضمام إلى متجر وكتالوجه.", - invitesInhabitantsHint: "أدخل رمز ساكن لمتابعة بعضكما وتبادل المحتوى.", invitesTribesTitle: "القبائل", invitesTribeInviteCodePlaceholder: "أدخل رمز دعوة القبيلة", invitesTribeJoinButton: "انضم إلى القبيلة", @@ -559,6 +583,7 @@ module.exports = { invitesAlreadyFederated: "أنت متحد بالفعل مع هذا الخادم.", invitesFederationsTitle: "الاتحادات", invitesAcceptedInvites: "متحدة", + invitesNoInvites: "لم يتم قبول أي دعوات بعد.", invitesUnfollow: "إلغاء المتابعة", invitesFollow: "متابعة", invitesUnfollowedInvites: "غير متحدة", @@ -578,24 +603,39 @@ module.exports = { panicMode: "وضع الطوارئ!", encryptData: "عيّن كلمة مرور (32 حرفًا على الأقل) لتشفير البلوكتشين الخاص بك", decryptData: "أدخل كلمة المرور لفك تشفير البلوكتشين الخاص بك", + panicModeDescription: "تشفير/فك تشفير أو حذف البلوكتشين الخاص بك", removeDataDescription: "تحذير: لا يمكن التراجع عن هذه العملية.", + encryptPanicButton: "تشفير البلوكتشين", + decryptPanicButton: "فك تشفير البلوكتشين", removePanicButton: "حذف جميع بياناتك!", searchTitle: "بحث", searchDescriptionLabel: "ابحث عن محتوى في شبكتك.", + searchLanguagesLabel: "اللغات", + searchSkillsLabel: "المهارات", searchPlaceholder:"ابحث عن محتوى...", + searchDateLabel:"التاريخ", searchLocationLabel:"الموقع", searchPriceLabel:"السعر", + searchUrlLabel:"الرابط", searchCategoryLabel:"الفئة", + searchStartLabel:"وقت البدء", + searchEndLabel:"وقت الانتهاء", searchPriorityLabel:"الأولوية", searchStatusLabel:"الحالة", statusLabel:"الحالة", + totalVotesLabel:"إجمالي الأصوات", noResultsFound:"لم يتم العثور على نتائج.", votesOption:"خيارات التصويت", voteYesLabel:"نعم", voteNoLabel:"لا", allTypesLabel: "غير محدود", + ABSTENTIONLabel:"امتناع", YESLabel:"نعم", NOLabel:"لا", + FOLLOW_MAJORITYLabel: "اتباع الأغلبية", + CONFUSEDLabel: "مرتبك", + NOT_INTERESTEDLabel: "غير مهتم", + StatusLabel:"الحالة", votesOptionYesLabel:"نعم", votesOptionNoLabel:"لا", votesOptionAbstentionLabel:"امتناع", @@ -603,6 +643,7 @@ module.exports = { votesCreatedByLabel:"أنشأه", voteStatusOpen:"مفتوح", voteStatusClosed:"مغلق", + imageSearchLabel: "أدخل كلمات للبحث عن صور مُعلَّمة بها.", commentDescription: ({ parentUrl }) => [ " علّق على ", a({ href: parentUrl }, " المحادثة"), @@ -613,20 +654,53 @@ module.exports = { a({ href: parentUrl }, " منشور"), ], subtopicTitle: ({ authorName }) => [`موضوع فرعي على منشور @${authorName}`], + mysteryDescription: "نشر منشورًا غامضًا", oasisDescription: "شبكة مشروع OASIS", searchSubmit: "لنبحث...", + postLabel: "المنشورات", + aboutLabel: "السكان", + feedLabel: "التغذيات", votesLabel: "التصويتات", + reportLabel: "التقارير", + imageLabel: "الصور", + videoLabel: "الفيديوهات", + audioLabel: "الملفات الصوتية", + documentLabel: "المستندات", + torrentLabel: "التورنت", pdfFallbackLabel: "مستند PDF", + eventLabel: "الأحداث", + taskLabel: "المهام", + transferLabel: "التحويلات", + curriculumLabel: "السيرة الذاتية", + bookmarkLabel: "العلامات المرجعية", + tribeLabel: "القبائل", + marketLabel: "السوق", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "المحافظ", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "إرسال", + subjectLabel: "الموضوع", editProfile: "تعديل الصورة الرمزية", + profileQrAlt: "رمز QR للملف الشخصي", + profileQrHint: "امسح ضوئيًا لنسخ معرّف Oasis هذا.", oasisIdLabel: "OasisID", - spreadContent: "نسخ", + spreadLabel: "نشر", + spreadHint: "نشر هذا لداعميك (يتم النسخ عبر تغذيتك).", welcomePmSubject: "Hello World!", - welcomePmBody: "مرحبًا بك في أوازيس — شبكة اجتماعية حرة، نِدّ لِنِد، اتحادية ومشفّرة، ذات بنية موزّعة بالدعوة فقط.\n\nبعض الأماكن المثيرة للاهتمام للبدء:\n\n- /profile — عدّل صورتك الرمزية واسمك ووصفك والوحدات التي تظهر في جانبك العام.\n- /invites — أضف pub للاتحاد مع بقية الشبكة.\n- /peers — اطّلع على من هو متصل، أعد مسح شبكتك المحلية، صدّر أو استورد قوائم الأقران.\n- /inhabitants — اكتشف الصور الرمزية للآخرين وزرها.\n- /tribes — أنشئ مجموعات خاصة بتشفير من طرف إلى طرف أو انضم إليها.\n- /inbox — اقرأ وأرسل الرسائل الخاصة.\n- /activity — اطّلع على النشاط الأخير، نشاطك ونشاط شبكتك.\n- /publish — اكتب منشورًا أو شارك صورة أو صوتًا أو فيديو أو مستندًا.\n- /search — ابحث في محتوى الشبكة حسب النوع.\n\nبعض الأماكن المثيرة للاهتمام للاتصال:\n\n- /multiverse — أدر حساباتك الأخرى في الكون المتعدد، بما في ذلك إرسال واستقبال المحتوى.\n\nأُرسلت هذه الرسالة بشكل خاص منك إلى نفسك، لذا لم يلزم أي اتصال لاستلامها.", + welcomePmBody: "مرحبًا بك في أوازيس — شبكة اجتماعية حرة، نِدّ لِنِد، اتحادية ومشفّرة، ذات بنية موزّعة بالدعوة فقط.\n\nبعض الأماكن المثيرة للاهتمام للبدء:\n\n- /profile — عدّل صورتك الرمزية واسمك ووصفك والوحدات التي تظهر في جانبك العام.\n- /invites — أضف pub للاتحاد مع بقية الشبكة.\n- /peers — اطّلع على من هو متصل، أعد مسح شبكتك المحلية، صدّر أو استورد قوائم الأقران.\n- /inhabitants — اكتشف الصور الرمزية للآخرين وزرها.\n- /tribes — أنشئ مجموعات خاصة بتشفير من طرف إلى طرف أو انضم إليها.\n- /inbox — اقرأ وأرسل الرسائل الخاصة.\n- /activity — اطّلع على النشاط الأخير، نشاطك ونشاط شبكتك.\n- /publish — اكتب منشورًا أو شارك صورة أو صوتًا أو فيديو أو مستندًا.\n\nبعض الأماكن المثيرة للاهتمام للاتصال:\n\n- /fediverse — أدر حساباتك الأخرى في الفيديفيرس، بما في ذلك إرسال واستقبال المحتوى.\n\nأُرسلت هذه الرسالة بشكل خاص منك إلى نفسك، لذا لم يلزم أي اتصال لاستلامها.", + profileVisibilityTitle: "ظهور الملف الشخصي العام", + profileVisibilityHint: "اختر الحقول التي يراها السكان الآخرون في ملفك الشخصي. افتراضيًا، يظهر Karma فقط.", profileSensorsSectionTitle: "المستشعرات", profileVisibilityActivity: "مستوى النشاط", + profileVisibilityDevice: "الجهاز", profileDeviceLockedHint: "هذا المستشعر مفعّل دائمًا: يتيح للآخرين معرفة الجهاز الذي تتصل منه ولا يمكن تعطيله.", profileVisibilityKarma: "نقاط KARMA", profileVisibilityUbi: "UBI", @@ -634,6 +708,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "مفتاح GPG", + profileVisibilityClearnet: "نشر ملفي الشخصي", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "المتاجر", profileClearnetJobsLabel: "الوظائف", @@ -687,6 +762,7 @@ module.exports = { " أو حالة الاتصال.", ], walletSentToLine: ({ destination, amount }) => `تم إرسال ECO ${amount} إلى ${destination}`, + walletSettingsTitle: "المحفظة", walletSettingsDescription: "دمج Oasis مع محفظة ECOin الخاصة بك.", walletSettingsDocLink: "دليل تثبيت ECOin", walletStatusMessages: { @@ -708,19 +784,37 @@ module.exports = { cipherTitle: "التشفير", cipherDescription: "تشفير وفك تشفير نصك بشكل متماثل (باستخدام كلمة مرور مشتركة).", randomPassword: "كلمة مرور عشوائية", + cipherEncryptTitle: "تشفير النص", + cipherEncryptDescription: "أدخل النص للتشفير", + cipherTextLabel: "النص للتشفير", cipherTextPlaceholder: "أدخل النص للتشفير...", cipherPasswordLabel: "عيّن كلمة مرور (32 حرفًا على الأقل) لتشفير نصك", cipherPasswordDecryptLabel: "عيّن كلمة مرور (32 حرفًا على الأقل) لفك تشفير نصك", cipherPasswordPlaceholder: "أدخل كلمة مرور...", cipherEncryptButton: "تشفير", + cipherDecryptTitle: "فك تشفير النص", + cipherDecryptDescription: "أدخل النص لفك التشفير", cipherEncryptedMessageLabel: "النص المشفر", cipherDecryptedMessageLabel: "النص المفكوك", + cipherPasswordUsedLabel: "كلمة المرور المستخدمة للتشفير (احتفظ بها!)", cipherEncryptedTextPlaceholder: "أدخل النص المشفر...", + cipherIvLabel: "IV", + cipherIvPlaceholder: "أدخل متجه التهيئة...", cipherDecryptButton: "فك التشفير", password: "كلمة المرور", text: "النص", encryptedText: "النص المشفر", iv: "متجه التهيئة (IV)", + encryptTitle: "تشفير نصك", + encryptDescription: "أدخل النص الذي تريد تشفيره وقدّم كلمة مرور.", + encryptButton: "تشفير", + decryptTitle: "فك تشفير نصك", + decryptDescription: "أدخل النص المشفر وقدّم نفس كلمة المرور المستخدمة للتشفير.", + decryptButton: "فك التشفير", + passwordLengthError: "يجب أن تكون كلمة المرور 32 حرفًا على الأقل.", + missingFieldsError: "لم يتم تقديم النص أو كلمة المرور أو IV.", + encryptionError: "خطأ في تشفير النص.", + decryptionError: "خطأ في فك تشفير النص.", bookmarkTitle: "العلامات المرجعية", bookmarkDescription: "اكتشف وأدِر العلامات المرجعية في شبكتك.", bookmarkAllSectionTitle: "العلامات المرجعية", @@ -746,6 +840,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "اختياري", bookmarkTagsLabel: "الوسوم", bookmarkTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", + bookmarkCategoryLabel: "الفئة", + bookmarkCategoryPlaceholder: "اختياري", bookmarkLastVisitLabel: "آخر زيارة", bookmarkSearchPlaceholder: "ابحث عن رابط، وسوم، فئة، مؤلف...", bookmarkSortRecent: "الأحدث", @@ -760,6 +856,8 @@ module.exports = { noLastVisit: "لا توجد زيارة أخيرة", videoTitle: "الفيديوهات", videoDescription: "استكشف وأدِر محتوى الفيديو في شبكتك.", + videoPluginTitle: "العنوان", + videoPluginDescription: "الوصف", videoMineSectionTitle: "فيديوهاتك", videoCreateSectionTitle: "رفع فيديو", videoUpdateSectionTitle: "تحديث الفيديو", @@ -791,6 +889,7 @@ module.exports = { videoSortOldest: "الأقدم", videoSortTop: "الأكثر تصويتًا", videoSearchButton: "بحث", + videoMessageAuthorButton: "رسالة خاصة", videoUpdatedAt: "تم التحديث", videoNoMatch: "لا توجد فيديوهات تطابق بحثك.", documentTitle: "المستندات", @@ -831,6 +930,8 @@ module.exports = { documentUpdatedAt: "تم التحديث", audioTitle: "الملفات الصوتية", audioDescription: "استكشف وأدِر المحتوى الصوتي في شبكتك.", + audioPluginTitle: "العنوان", + audioPluginDescription: "الوصف", audioMineSectionTitle: "ملفاتك الصوتية", audioCreateSectionTitle: "رفع ملف صوتي", audioUpdateSectionTitle: "تحديث الملف الصوتي", @@ -841,6 +942,7 @@ module.exports = { audioFilterMine: "خاصتي", audioFilterRecent: "الأخيرة", audioFilterTop: "الأفضل", + audioFilterBlockchain: "بلوكتشين", audioCreateButton: "رفع ملف صوتي", audioUpdateButton: "تحديث", audioDeleteButton: "حذف", @@ -867,7 +969,6 @@ module.exports = { audioFilterFavorites: "المفضلة", favoritesTitle: "المفضلة", favoritesDescription: "كل المحتوى المفضل لديك في مكان واحد.", - favoritesSearchPlaceholder: "ابحث في المفضلة...", favoritesFilterAll: "الكل", favoritesFilterRecent: "الأخيرة", favoritesFilterAudios: "الملفات الصوتية", @@ -883,13 +984,18 @@ module.exports = { favoritesNoItems: "لا توجد مفضلات بعد.", yourContacts: "جهات اتصالك", allInhabitants: "السكان", - allCVs: "السير الذاتية", + allCVs: "جميع السير الذاتية", discoverPeople: "اكتشف السكان في شبكتك.", + allInhabitantsButton: "الكل", + contactsButton: "المدعومون", + CVsButton: "السير الذاتية", + matchSkills: "مطابقة المهارات", + matchSkillsButton: "مطابقة المهارات", + suggestedButton: "المقترحون", searchInhabitantsPlaceholder: "تصفية السكان حسب الاسم …", filterLocation: "تصفية السكان حسب الموقع …", filterLanguage: "تصفية السكان حسب اللغة …", filterSkills: "تصفية السكان حسب المهارات …", - cvSkillsCount: "المهارات", applyFilters: "تطبيق الفلاتر", locationLabel: "الموقع", languagesLabel: "اللغات", @@ -898,18 +1004,17 @@ module.exports = { mutualFollowers: "المتابعون المتبادلون", suggestedFollowsYou: "يتابعك", latestInteractions: "آخر التفاعلات", + viewAvatar: "عرض الصورة الرمزية", + viewCV: "عرض السيرة الذاتية", suggestedSectionTitle: "المقترحون", topkarmaSectionTitle: "أفضل كارما", topecoSectionTitle: "أفضل إيكو", topactivitySectionTitle: "الأكثر نشاطًا", - "TOP INACTIVITYButton": "الأقل نشاطاً", - topinactivitySectionTitle: "السكان الأقل نشاطاً", blockedSectionTitle: "المحظورون", gallerySectionTitle: "المعرض", - topSectionTitle: "الأعلى", + blockedButton: "المحظورون", blockedLabel: "مستخدم محظور", inhabitantviewDetails: "عرض التفاصيل", - cvVisitButton: "زيارة السيرة الذاتية", keepReading: "متابعة القراءة...", oasisId: "المعرّف", noInhabitantsFound: "لم يتم العثور على سكان بعد.", @@ -922,8 +1027,7 @@ module.exports = { parliamentFilterCandidatures: "الترشيحات", parliamentFilterProposals: "المقترحات", parliamentFilterLaws: "القوانين", - parliamentFilterRevocations: "الإقالات", - parliamentFilterHistorical: "السجل", + parliamentFilterHistorical: "السجل التاريخي", parliamentFilterLeaders: "القادة", parliamentFilterRules: "القواعد", parliamentGovernmentCard: "الحكومة الحالية", @@ -948,8 +1052,10 @@ module.exports = { parliamentPoliciesDeclined: "القوانين المرفوضة", parliamentPoliciesDiscarded: "القوانين الملغاة", parliamentEfficiency: "% الكفاءة", + parliamentFilterRevocations: 'الإلغاءات', parliamentRevocationFormTitle: 'إلغاء قانون', parliamentRevocationLaw: 'القانون', + parliamentRevocationTitle: 'العنوان', parliamentRevocationReasons: 'الأسباب', parliamentRevocationPublish: 'نشر الإلغاء', parliamentCurrentRevocationsTitle: 'الإلغاءات الحالية', @@ -962,12 +1068,23 @@ module.exports = { parliamentNoGovernments: "لا توجد حكومات بعد.", parliamentCandidatureFormTitle: "اقتراح ترشيح", parliamentCandidatureIdPh: "معرّف Oasis (@...) أو اسم القبيلة", + parliamentCandidatureSlogan: "الشعار (140 حرفًا كحد أقصى)", + parliamentCandidatureSloganPh: "شعار قصير", parliamentCandidatureMethod: "الطريقة", parliamentCandidatureProposeBtn: "نشر الترشيح", + parliamentThDate: "تاريخ الاقتراح", + parliamentThSlogan: "الشعار", + parliamentThSince: "تاريخ الملف الشخصي", + parliamentThVotes: "الأصوات المُستلَمة", + parliamentThVoteAction: "تصويت", + parliamentTypeUser: "ساكن", + parliamentTypeTribe: "قبيلة", parliamentVoteBtn: "تصويت", parliamentProposalFormTitle: "اقتراح قانون", parliamentProposalDescription: "الوصف (≤1000)", parliamentProposalPublish: "نشر الاقتراح", + parliamentFinalize: "إنهاء", + parliamentDeadline: "الموعد النهائي", parliamentNoProposals: "لا توجد مقترحات قوانين بعد.", parliamentNoLaws: "لا توجد قوانين معتمدة بعد.", parliamentMethodDEMOCRACY: "ديمقراطية", @@ -985,21 +1102,29 @@ module.exports = { parliamentVotesSlashTotal: "الأصوات/الإجمالي", parliamentVotesNeeded: "الأصوات المطلوبة", parliamentFutureLawsTitle: "القوانين المستقبلية", + parliamentNoFutureLaws: "لا توجد قوانين مستقبلية بعد.", parliamentVoteAction: "تصويت", + parliamentLeadersTitle: "القادة", parliamentThLeader: "الصورة الرمزية", parliamentPopulation: "عدد السكان", + parliamentThType: "النوع", + parliamentThInPower: "في السلطة", parliamentThPresented: "الترشيحات", parliamentNoLeaders: "لا يوجد قادة بعد.", parliamentCandidaturesListTitle: "قائمة الترشيحات", parliamentTimeRemaining: "الوقت المتبقي", + parliamentNoLeader: "لا يوجد ترشيح فائز بعد.", parliamentElectionsStatusTitle: "الحكومة القادمة", parliamentProposalDeadlineLabel: "الموعد النهائي", parliamentProposalTimeLeft: "الوقت المتبقي", + parliamentProposalOnTrack: "في طريقه للتمرير", + parliamentProposalOffTrack: "لا يوجد دعم كافٍ بعد", parliamentRulesTitle: "كيف يعمل البرلمان", parliamentRulesIntro: "تُحسم الانتخابات كل شهرين؛ الترشيحات مستمرة وتُعاد عند اختيار حكومة.", parliamentRulesCandidates: "يمكن لأي ساكن ترشيح نفسه أو ساكن آخر أو أي قبيلة. يمكن لكل ساكن تقديم 3 ترشيحات كحد أقصى لكل دورة؛ تُرفض التكرارات في نفس الدورة.", parliamentRulesElection: "الفائز هو الترشيح الذي يحصل على أكبر عدد من الأصوات وقت الحسم.", parliamentRulesTies: "ترتيب كسر التعادل: أعلى كارما للساكن؛ إذا تعادلوا، أقدم ملف شخصي؛ إذا لا يزال متعادلاً، أقدم ترشيح؛ ثم ترتيب أبجدي حسب المعرّف.", + parliamentRulesFallback: "إذا لم يصوّت أحد، يفوز آخر ترشيح مقدّم. إذا لم تكن هناك ترشيحات، تُختار قبيلة عشوائية.", parliamentRulesTerm: "يعرض تبويب الحكومة الحكومة الحالية والإحصائيات.", parliamentRulesMethods: "الأشكال: فوضوية (أغلبية بسيطة)، ديمقراطية (50%+1)، أغلبية (80%)، أقلية (20%)، كارماتوقراطية (مقترحات أعلى كارما)، ديكتاتورية (موافقة فورية).", parliamentRulesAnarchy: "الفوضوية هي الوضع الافتراضي: إذا لم يُنتخب أي ترشيح عند الحسم، تُعلن الفوضوية. في ظل الفوضوية، يمكن لأي ساكن اقتراح قوانين.", @@ -1021,7 +1146,11 @@ module.exports = { courtsCaseFormTitle: "فتح قضية", courtsCaseRespondent: "المتهم / المدعى عليه", courtsCaseRespondentPh: "معرّف Oasis (@...) أو اسم القبيلة", + courtsCaseMediatorsAccuser: "الوسطاء (المدعي)", courtsCaseMethod: "طريقة الحل", + courtsCaseDescription: "الوصف (1000 حرف كحد أقصى)", + courtsCaseEvidenceTitle: "أدلة القضية", + courtsCaseEvidenceHelp: "أرفق صورًا أو ملفات صوتية أو مستندات (PDF) أو فيديوهات تدعم قضيتك.", courtsCaseSubmit: "رفع القضية", courtsNominateJudge: "ترشيح قاضٍ", courtsJudgeId: "القاضي", @@ -1066,6 +1195,22 @@ module.exports = { courtsRulesMisconduct: "قد يؤدي التحرش أو التلاعب أو تلفيق الأدلة إلى حكم سلبي فوري.", courtsRulesGlossary: "القضية: سجل نزاع. الدليل: مواد تدعم الادعاءات. الحكم: قرار مع علاج. الاستئناف: طلب مراجعة الحكم.", courtsFilterActions: "الإجراءات", + courtsNoActions: "لا توجد إجراءات معلقة لدورك.", + courtsCaseTitlePlaceholder: "وصف مختصر للنزاع", + courtsCaseSeverity: "الخطورة", + courtsCaseSeverityNone: "بدون تصنيف خطورة", + courtsCaseSeverityLOW: "منخفضة", + courtsCaseSeverityMEDIUM: "متوسطة", + courtsCaseSeverityHIGH: "عالية", + courtsCaseSeverityCRITICAL: "حرجة", + courtsCaseSubject: "الموضوع", + courtsCaseSubjectNone: "بدون تصنيف موضوع", + courtsCaseSubjectBEHAVIOUR: "سلوك", + courtsCaseSubjectCONTENT: "محتوى", + courtsCaseSubjectGOVERNANCE: "حوكمة / قواعد", + courtsCaseSubjectFINANCIAL: "مالي / موارد", + courtsCaseSubjectOTHER: "أخرى", + courtsHiddenRespondent: "مخفي (مرئي فقط للأدوار المعنية).", courtsThRole: "الدور", courtsRoleAccuser: "المدعي", courtsRoleDefence: "الدفاع", @@ -1076,19 +1221,51 @@ module.exports = { courtsAssignJudgeBtn: "اختيار قاضٍ", trendingTitle: "الرائج", exploreTrending: "استكشف المحتوى الأكثر شعبية في شبكتك.", + bookmarkButton: "العلامات المرجعية", + transferButton: "التحويلات", + eventButton: "الأحداث", + taskButton: "المهام", votesButton: "التصويتات", - shopProductButton: "متجر", + reportButton: "التقارير", + feedButton: "التغذية", + marketButton: "السوق", + imageButton: "الصور", + audioButton: "الملفات الصوتية", + videoButton: "الفيديوهات", + documentButton: "المستندات", + torrentButton: "التورنت", + noTrendingFound: "لم يتم العثور على محتوى رائج.", + noContentMessage: "لا يتوفر محتوى رائج بعد.", + trendingDescription: "الوصف", + trendingDate: "التاريخ", + trendingLocation: "الموقع", + trendingPrice: "السعر", + trendingUrl: "الرابط", + trendingCategory: "الفئة", + trendingStart: "البداية", + trendingEnd: "النهاية", + trendingPriority: "الأولوية", + trendingStatus: "الحالة", + trendingFrom: "من", + trendingTo: "إلى", + trendingConcept: "المفهوم", + trendingAmount: "المبلغ", + trendingDeadline: "الموعد النهائي", + trendingItemStatus: "حالة العنصر", + trendingTotalVotes: "إجمالي الأصوات", trendingTotalOpinions: "إجمالي الآراء", - trendingNoContentMessage: "لا توجد اتجاهات بعد.", + trendingNoContentMessage: "لا يتوفر محتوى رائج بعد.", + trendingAuthor: "بواسطة", + trendingCreatedAtLabel: "أُنشئ في", trendingTotalCount: "العدد الإجمالي", tasksTitle: "المهام", tasksDescription: "اكتشف وأدِر المهام في شبكتك.", - taskSearchPlaceholder: "ابحث عن المهام...", taskTitleLabel: "العنوان", taskDescriptionLabel: "الوصف", taskStartTimeLabel: "وقت البدء", taskEndTimeLabel: "وقت الانتهاء", taskPriorityLabel: "الأولوية", + taskPrioritySelect: "اختر الأولوية", taskPriorityUrgent: "عاجل", taskPriorityHigh: "عالية", taskPriorityMedium: "متوسطة", @@ -1098,13 +1275,13 @@ module.exports = { taskVisibilityLabel: "الظهور", taskPublic: "عام", taskPrivate: "خاص", + taskCreatedAt: "أُنشئ في", + taskBy: "بواسطة", taskStatus: "الحالة", taskStatusOpen: "مفتوح", taskStatusInProgress: "قيد التنفيذ", taskStatusClosed: "مغلق", taskAssignedTo: "مُسنَد إلى", - taskAssignedChip: "مُسندة", - taskUnassignedChip: "غير مُسندة", taskAssignees: "المُكلَّفون", taskAssignButton: "إسناد لي", taskUnassignButton: "إلغاء الإسناد", @@ -1117,6 +1294,7 @@ module.exports = { taskFilterInProgress: "قيد التنفيذ", taskFilterClosed: "مغلق", taskFilterAssigned: "مُسنَد", + taskFilterArchived: "مؤرشف", taskFilterUrgent: "عاجل", taskFilterHigh: "عالية", taskFilterMedium: "متوسطة", @@ -1129,21 +1307,23 @@ module.exports = { taskInProgressTitle: "المهام قيد التنفيذ", taskClosedTitle: "المهام المغلقة", taskAssignedTitle: "المهام المُسنَدة", + taskArchivedTitle: "المهام المؤرشفة", + taskPublicTitle: "المهام العامة", + taskPrivateTitle: "المهام الخاصة", notasks: "لا توجد مهام متاحة.", noLocation: "لم يتم تحديد الموقع", taskSetStatus: "تعيين الحالة", + eventTitle: "الأحداث", eventDateLabel: "التاريخ", - eventRecurrenceLabel: "التكرار", - eventRecurrenceUntil: "كرر حتى", - eventRecurrenceHint: "اترك تاريخ الانتهاء فارغًا لفعالية لمرة واحدة.", - eventUpcomingDates: "التواريخ القادمة", eventsTitle: "الأحداث", eventsDescription: "اكتشف وأدِر الأحداث في شبكتك.", - eventSearchPlaceholder: "ابحث عن الأحداث...", + eventDescription: "الوصف", eventPrice: "السعر", eventStatus: "الحالة", + eventOrganizer: "المنظم", eventAllSectionTitle: "الأحداث", eventMineSectionTitle: "أحداثك", + eventArchivedTitle: "الأحداث المؤرشفة", eventCreateSectionTitle: "إنشاء حدث", eventUpdateSectionTitle: "تحديث الحدث", eventDeleteButton: "حذف", @@ -1151,26 +1331,11 @@ module.exports = { eventAddToCalendar: "إضافة إلى التقويم", eventVisitCalendar: "عرض التقويم", viewTask: "عرض المهمة", - viewEvent: "عرض الحدث", - viewPad: "عرض المستند", - viewMap: "عرض الخريطة", - viewCalendar: "عرض التقويم", viewReport: "عرض التقرير", viewTransfer: "عرض التحويل", viewItem: "عرض العنصر", viewShop: "عرض المتجر", viewProject: "عرض المشروع", - viewJob: "عرض الوظيفة", - viewPlace: "عرض المكان", - generatePdf: "إنشاء PDF", - sharePm: "مشاركة برسالة خاصة", - galleryImages: "الصور (الحجم الأقصى: 50 ميجابايت لكل صورة)", - galleryPhotos: "الصور", - galleryAddPhoto: "إضافة صورة", - galleryRemovePhoto: "إزالة", - galleryVideo: "فيديو (الحجم الأقصى: 50 ميجابايت)", - galleryAddVideo: "إضافة فيديو", - galleryRemoveVideo: "إزالة الفيديو", viewVotation: "عرض التصويت", voteCastTitle: "أدلِ بصوتك", voteResults: "النتائج", @@ -1178,15 +1343,29 @@ module.exports = { eventDescriptionLabel: "الوصف", eventDescriptionPlaceholder: "أدخل وصف الحدث...", eventUpdateButton: "تحديث", + eventAttendeesLabel: "الحاضرون", + eventAttendeesPlaceholder: "أدخل الحاضرين، مفصولين بفواصل...", eventTagsLabel: "الوسوم", + eventTagsPlaceholder: "أدخل وسوم الحدث، مفصولة بفواصل...", + eventTags: "الوسوم", eventPriceLabel: "السعر", eventUrlLabel: "الرابط", eventAttendees: "الحاضرون", + noAttendees: "لا يوجد حاضرون بعد", + eventCreatedAt: "أُنشئ في", eventLocation: "الموقع", eventLocationLabel: "الموقع", + eventNoLocation: "لم يتم تحديد الموقع", + eventNoURL: "لم يتم تحديد رابط", + eventBy: "بواسطة", noevents: "لا توجد أحداث متاحة.", eventDate: "التاريخ", + eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "حضور الحدث", + eventUnattendButton: "إلغاء الحضور", + eventCreatedBy: "أُنشئ بواسطة", + eventAttendeesCount: "عدد الحاضرين", + eventCreatedByYou: "أنت أنشأت هذا الحدث", eventFilterAll: "الكل", eventFilterMine: "خاصتي", eventFilterToday: "اليوم", @@ -1194,9 +1373,18 @@ module.exports = { eventFilterMonth: "هذا الشهر", eventFilterYear: "هذه السنة", eventFilterArchived: "مؤرشف", + eventTodayTitle: "أحداث اليوم", + eventThisWeekTitle: "أحداث هذا الأسبوع", + eventThisMonthTitle: "أحداث هذا الشهر", + eventThisYearTitle: "أحداث هذه السنة", eventPrivacyLabel: "الظهور", eventPublic: "عام", eventPrivate: "خاص", + eventPublicTitle: "الأحداث العامة", + eventNoPrice: "حدث مجاني", + eventNoImage: "لم يتم رفع صورة", + eventAttendConfirmation: "أنت الآن تحضر هذا الحدث", + eventUnattendConfirmation: "لم تعد تحضر هذا الحدث", eventAttended: "حضر", eventUnattended: "لم يحضر", eventStatusOpen: "مفتوح", @@ -1207,14 +1395,9 @@ module.exports = { tagsTopSectionTitle: "أفضل الوسوم", tagsCloudSectionTitle: "سحابة الوسوم", tagsFilterAll: "الكل", - tagsFilterMine: "لي", - tagsFilterRecent: "حديثة", - tagsMineSectionTitle: "وسومي", - tagsRecentSectionTitle: "وسوم حديثة", tagsFilterTop: "الأفضل", tagsFilterCloud: "السحابة", tagsNoItems: "لا توجد وسوم متاحة.", - tagsSearchPlaceholder: "ابحث عن الوسوم...", tagsTableHeaderTag: "الوسم/الرابط:", tagsTableHeaderCount: "العداد:", transfersTitle: "التحويلات", @@ -1254,12 +1437,18 @@ module.exports = { transfersDeadline: "الموعد النهائي", transfersTags: "الوسوم", transfersStatus: "الحالة", + transfersStatusUnconfirmed: "غير مؤكد", + transfersStatusClosed: "مغلق", + transfersStatusDiscarded: "ملغى", + transfersCreatedAt: "أُنشئ في", transfersConfirmations: "التأكيدات", - transfersExportContract: "إنشاء فاتورة", + transfersContainingBlock: "الكتلة المحتوية", + transfersExportContract: "إنشاء العقد", transfersConfirmButton: "تأكيد التحويل", transfersNoItems: "لم يتم العثور على تحويلات.", transfersMineSectionTitle: "تحويلاتك", transfersUBISectionTitle: "تحويلات UBI", + transfersMarketSectionTitle: "تحويلات السوق", transfersTopSectionTitle: "أفضل التحويلات", transfersPendingSectionTitle: "التحويلات المعلّقة", transfersUnconfirmedSectionTitle: "التحويلات غير المؤكدة", @@ -1267,13 +1456,21 @@ module.exports = { transfersDiscardedSectionTitle: "التحويلات الملغاة", transfersCreateSectionTitle: "إنشاء عملية نقل", transfersAllSectionTitle: "التحويلات", + transfersFilterFavs: "المفضلة", + transfersFavsSectionTitle: "التحويلات المفضلة", + transfersSearchLabel: "بحث", transfersSearchPlaceholder: "ابحث عن مفهوم، وسوم، مستخدمين...", transfersMinAmountLabel: "الحد الأدنى للمبلغ", transfersMaxAmountLabel: "الحد الأقصى للمبلغ", + transfersSortLabel: "ترتيب حسب", transfersSortRecent: "الأحدث", transfersSortAmount: "الأعلى مبلغًا", transfersSortDeadline: "الأقرب موعدًا", transfersSearchButton: "بحث", + transfersFavoriteButton: "إضافة للمفضلة", + transfersUnfavoriteButton: "إزالة من المفضلة", + transfersMessageUserButton: "رسالة", + transfersExpiringSoonBadge: "على وشك الانتهاء", transfersExpiredBadge: "منتهي الصلاحية", transfersUpdatedAt: "تم التحديث", transfersNoMatch: "لا توجد تحويلات تطابق بحثك.", @@ -1286,9 +1483,6 @@ module.exports = { voteClosedTitle: "التصويتات المغلقة", voteAllSectionTitle: "التصويتات", voteCreateButton: "إنشاء تصويت", - voteErrorTitle: "راجع النموذج", - voteErrorDeadlineMin: "يجب أن يكون الموعد النهائي بعد {days} أيام على الأقل من الآن.", - voteErrorGeneric: "تعذّر حفظ التصويت.", voteUpdateButton: "تحديث", voteDeleteButton: "حذف", voteOptionYes: "نعم", @@ -1318,6 +1512,16 @@ module.exports = { voteNoQuestion: "لم يتم تقديم سؤال", voteUnknownCreator: "مُنشئ غير معروف", voteUnknownDate: "تاريخ غير معروف", + errorVoteNotFound: "لم يتم العثور على التصويت", + errorAlreadyVoted: "لقد أبديت رأيك بالفعل.", + errorVoteClosedCannotEdit: "لا يمكنك تعديل تصويت مغلق", + errorVoteDeadlinePassed: "انتهى الموعد النهائي لهذا التصويت", + errorRetrievingVote: "خطأ في استرجاع التصويت", + errorCreatingVote: "خطأ في إنشاء التصويت", + errorVoteAlreadyVoted: "لا يمكن التعديل بعد الإدلاء بالرأي", + errorDeletingOldVote: "خطأ في حذف الرأي القديم", + errorCreatingUpdatedVote: "خطأ في إنشاء الرأي المحدّث", + errorCreatingTombstone: "خطأ في إنشاء شاهد القبر", voteDetailSectionTitle: 'تفاصيل التصويت', voteCommentsLabel: 'التعليقات', voteCommentsForumButton: 'فتح النقاش', @@ -1327,6 +1531,7 @@ module.exports = { voteNewCommentButton: 'نشر التعليق', voteNewCommentLabel: 'إضافة تعليق', cvTitle: "السيرة الذاتية", + cvLabel: "السيرة الذاتية (CV)", cvEditSectionTitle: "تعديل السيرة الذاتية", cvCreateSectionTitle: "إنشاء سيرة ذاتية", cvDescription: "إدارة ومشاركة مهاراتك ومعلوماتك المهنية.", @@ -1345,121 +1550,29 @@ module.exports = { cvLocationLabel: "الموقع", cvStatusLabel: "الحالة", cvPreferencesLabel: "التفضيلات", + cvOasisContributorLabel: "مساهم في Oasis", cvPersonal: "شخصي", cvOasis: "مساهم في Oasis (اختياري)", - cvOasisContributorView: "المساهمة", + cvOasisContributorView: "مساهمة في Oasis", cvEducational: "تعليمي (اختياري)", cvEducationalView: "تعليمي", cvProfessional: "مهني (اختياري)", cvProfessionalView: "مهني", cvAvailability: "التوفر (اختياري)", + cvAvailabilityView: "التوفر", cvUpdateButton: "تحديث", cvCreateButton: "إنشاء سيرة ذاتية", + cvContactLabel: "التواصل", cvCreatedAt: "أُنشئ في", cvUpdatedAt: "تم التحديث في", cvEditButton: "تحديث", cvDeleteButton: "حذف", cvNoCV: "لم يتم العثور على سيرة ذاتية.", blogSubject: "الموضوع", - blogMessage: "الرسالة", + blogMessage: "النص محدود بـ 8096 حرفًا.", blogImage: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", - blogMedia: "المرفقات (حتى 8 صور ومقطع فيديو)", blogPublish: "معاينة", - pollsTitle: "الاستطلاعات", - dataTitle: "التطابقات", - dataDescription: "استكشف تطابقات شبكتك وشاهدها.", - dataFilterAll: "الكل", - dataFilterMine: "بياناتي", - dataFilterRecent: "الأحدث", - dataFilterTop: "الأعلى", - dataKindInhabitants: "السكان", - dataKindJobs: "الوظائف", - dataKindProjects: "المشاريع", - dataKindEvents: "الفعاليات", - dataKindTribes: "القبائل", - dataKindMarket: "السوق", - dataKindHousing: "السكن", - dataKindIndustry: "الصناعة", - dataKindTasks: "المهام", - dataKindReports: "التقارير", - dataKindVotes: "التصويتات", - dataSearchPlaceholder: "ابحث في البيانات المتقاطعة...", - dataCohesionTitle: "معامل التماسك (CC)", - dataCcShort: "CC", - dataCohesionHint: "متوسط التقارب بين كل ما نشرته الشبكة.", - dataAffinity: "التوافق", - dataCommonTerms: "كلمة مفتاحية", - dataStatPeople: "السير الذاتية", - dataStatConnected: "متصلون", - dataStatSkills: "المهارات", - dataBestMatch: "أفضل تطابق", - dataStatIsolated: "معزولون", - dataStatEntities: "الكيانات المقارَنة", - dataStatTerms: "مصطلحات مختلفة", - dataStatPairs: "الأزواج المرتبطة", - dataMatchesTitle: "التطابقات", - dataMatchesHint: "اقتراحات مخصصة من الخوارزم.", - dataTermsTitle: "الكلمات المفتاحية", - dataTermsHint: "المصطلحات التي تظهر في أكثر المحتويات.", - dataConnections: "تطابقات مرتبطة", - dataTopicTitle: "كل ما يتعلق بـ", - dataTopicHint: "ما يربطه الخوارزم بهذا الموضوع", - dataNoMatches: "لا توجد بيانات متقاطعة لعرضها.", - dataNoProfile: "انشر محتوى أو اكتب سيرتك الذاتية ليتعلم الخوارزم ما يهمك.", - pollsDescription: "وحدة لسؤال الشبكة وعدّ الإجابات.", - pollFilterAll: "الكل", - pollFilterMine: "استطلاعاتي", - pollFilterRecent: "الأحدث", - pollFilterTop: "الأعلى", - pollFilterVoted: "صوّتت", - pollFilterOpen: "مفتوحة", - pollFilterClosed: "مغلقة", - pollCreateButton: "إنشاء استطلاع", - pollCreateTitle: "إنشاء استطلاع", - pollEditTitle: "تعديل الاستطلاع", - pollQuestion: "السؤال", - pollQuestionPlaceholder: "ماذا تريد أن تسأل؟", - pollOptions: "الخيارات (خيار في كل سطر)", - pollOutcome: "النتيجة", - pollNoVotesYet: "لا أصوات بعد", - pollTie: "تعادل", - pollOptionsPlaceholder: "الساحة\nالحديقة\nالمقهى", - pollAnonymous: "مجهول", - pollAnonymousLabel: "استطلاع مجهول", - pollMultiple: "اختيار متعدد", - pollMultipleLabel: "السماح بعدة إجابات", - pollDeadline: "الموعد النهائي", - pollTags: "الوسوم", - pollPublishButton: "نشر الاستطلاع", - pollUpdateButton: "تحديث", - pollCloseButton: "إغلاق", - pollDeleteButton: "حذف", - pollVoteButton: "صوّت", - pollChangeVote: "تغيير الصوت", - pollVoters: "المصوّتون", - pollComments: "التعليقات", - pollStatusLabel: "الحالة", - pollStatusOpen: "مفتوح", - pollStatusClosed: "مغلق", - pollSearchPlaceholder: "ابحث في الاستطلاعات...", - pollsNoItems: "لم يتم العثور على استطلاعات.", - viewPoll: "عرض الاستطلاع", - pollChatCreate: "إنشاء استطلاع", - pollInChat: "استطلاع", - blogTitle: "المدونات", - blogDescription: "وحدة لاكتشاف المدونات وإدارتها.", - blogFilterAll: "الكل", - blogFilterMine: "مدوناتي", - blogFilterRecent: "الأحدث", - blogFilterFavorites: "المفضلة", - blogFilterTop: "الأعلى", - blogCreateButton: "إنشاء مدونة", - blogSearchPlaceholder: "ابحث في المدونات...", - blogComments: "التعليقات", - blogAllowComments: "السماح بالتعليقات", - blogCommentsClosed: "أغلق الكاتب التعليقات على هذه المدونة.", - blogNoItems: "لم يتم العثور على مدونات.", - viewBlog: "عرض المدونة", + noPopularMessages: "لم تُنشر رسائل شائعة بعد", forumTitle: "المنتديات", forumCategoryLabel: "الفئة", forumTitleLabel: "العنوان", @@ -1467,22 +1580,43 @@ module.exports = { forumCreateButton: "إنشاء منتدى", forumCreateSectionTitle: "إنشاء منتدى", forumDescription: "تحدث بصراحة مع السكان الآخرين في شبكتك.", - forumSearchPlaceholder: "ابحث في المنتدى...", forumFilterAll: "الكل", forumFilterMine: "خاصتي", forumFilterRecent: "الأخيرة", forumFilterTop: "الأفضل", + forumMineSectionTitle: "منتدياتك", + forumRecentSectionTitle: "المنتديات الأخيرة", + forumAllSectionTitle: "المنتديات", forumDeleteButton: "حذف", forumParticipants: "المشاركون", forumMessages: "الرسائل", + forumLastMessage: "آخر رسالة", forumMessageLabel: "الرسالة", forumMessagePlaceholder: "اكتب رسالتك...", forumSendButton: "إرسال", + forumVisitForum: "زيارة المنتدى", noForums: "لم يتم العثور على منتديات.", - forumVisitButton: "عرض المنتدى", - forumTopicLabel: "الموضوع", + forumVisitButton: "زيارة المنتدى", + forumCatGENERAL: "عام", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", + forumCatPOLITICS: "سياسة", + forumCatTECH: "تقنية", + forumCatSCIENCE: "علوم", + forumCatMUSIC: "موسيقى", + forumCatART: "فن", + forumCatGAMING: "ألعاب", + forumCatBOOKS: "كتب", + forumCatFILMS: "أفلام", + forumCatPHILOSOPHY: "فلسفة", + forumCatSOCIETY: "مجتمع", + forumCatPRIVACY: "خصوصية", + forumCatCYBERWARFARE: "حرب سيبرانية", + forumCatSURVIVALISM: "البقائية", imageTitle: "الصور", imageDescription: "استكشف وأدِر محتوى الصور في شبكتك.", + imagePluginTitle: "العنوان", + imagePluginDescription: "الوصف", imageMineSectionTitle: "صورك", imageCreateSectionTitle: "رفع صورة", imageUpdateSectionTitle: "تحديث الصورة", @@ -1491,12 +1625,14 @@ module.exports = { imageTopSectionTitle: "أفضل الصور", imageFavoritesSectionTitle: "المفضلة", imageGallerySectionTitle: "المعرض", + imageMemeSectionTitle: "ميمز", imageFilterAll: "الكل", imageFilterMine: "خاصتي", imageFilterRecent: "الأخيرة", imageFilterTop: "الأفضل", imageFilterFavorites: "المفضلة", imageFilterGallery: "المعرض", + imageFilterMeme: "ميمز", imageCreateButton: "رفع صورة", imageUpdateButton: "تحديث", imageDeleteButton: "حذف", @@ -1509,6 +1645,7 @@ module.exports = { imageTitlePlaceholder: "اختياري", imageDescriptionLabel: "الوصف", imageDescriptionPlaceholder: "اختياري", + imageMemeLabel: "تعيين كميم", imageNoFile: "لم يتم تقديم ملف صورة", noImages: "لا توجد صور متاحة.", imageSearchPlaceholder: "ابحث عن عنوان، وسوم، وصف، مؤلف...", @@ -1516,6 +1653,7 @@ module.exports = { imageSortOldest: "الأقدم", imageSortTop: "الأكثر تصويتًا", imageSearchButton: "بحث", + imageMessageAuthorButton: "رسالة", imageUpdatedAt: "تم التحديث", imageNoMatch: "لا توجد صور تطابق بحثك.", feedTitle: "التغذية", @@ -1523,6 +1661,7 @@ module.exports = { createFeedButton: "أرسل التغذية!", feedPlaceholder: "ماذا يحدث؟ (280 حرفًا كحد أقصى)", TODAYButton: "اليوم", + CREATEButton: "إنشاء تغذية", totalOpinions: "إجمالي الآراء", moreVoted: "الأكثر تصويتًا", noFeedsFound: "لم يتم العثور على تغذيات.", @@ -1545,12 +1684,20 @@ module.exports = { concept: "المفهوم", description: "الوصف", meme: "ميم", + activityContact: "جهة اتصال", + activityBy: "الاسم", + activityPixelia: "تمت إضافة بكسل جديد", + viewImage: "عرض الصورة", + playAudio: "تشغيل الصوت", + playVideo: "تشغيل الفيديو", typeRecent: "الأخيرة", - typeTop: "الأعلى", + errorActivity: "خطأ في استرجاع النشاط", typePost: "مدونات", + recentButton: "الأحدث", typeTribe: "القبائل", typeLarp: "L.A.R.P.", typeInhabitants: "السكان", + activityProfileUpdated: "حدّث ملفه الشخصي", typeAbout: "السكان", typeCurriculum: "السير الذاتية", typeImage: "الصور", @@ -1594,6 +1741,8 @@ module.exports = { typeCourtsSettlementAccepted: "المحاكم · تسوية مقبولة", typeCourtsNomination: "المحاكم · ترشيح", typeCourtsNominationVote: "المحاكم · تصويت ترشيح", + activitySupport: "تحالف جديد أُقيم", + activityJoin: "تم الانضمام إلى PUB جديد", question: "السؤال", deadline: "الموعد النهائي", status: "الحالة", @@ -1607,8 +1756,12 @@ module.exports = { date: "التاريخ", category: "الفئة", attendees: "الحاضرون", + activitySpread: "->", + visitLink: "زيارة الرابط", + viewDocument: "عرض المستند", location: "الموقع", contentWarning: "الموضوع", + personName: "اسم الساكن", bankWalletConnected: "محفظة ECOin", bankUbiReceived: "تم استلام الدخل الأساسي", bankTx: "معاملة", @@ -1618,6 +1771,7 @@ module.exports = { link: "رابط", activityProjectFollow: "%OASIS% الآن %ACTION% هذا المشروع %PROJECT%", activityProjectUnfollow: "%OASIS% الآن %ACTION% هذا المشروع %PROJECT%", + activityProjectPledged: "%OASIS% قد %ACTION% %AMOUNT% للمشروع %PROJECT%", following: "يتابع", unfollowing: "ألغى المتابعة", pledged: "تبرع", @@ -1663,17 +1817,26 @@ module.exports = { courtsVotesSlashTotal: "نعم/الإجمالي", courtsOpenVote: "تصويت مفتوح", courtsAnswerTitle: "الرد", + courtsStanceADMIT: "اعتراف", + courtsStanceDENY: "إنكار", + courtsStancePARTIAL: "جزئي", + courtsStanceCOUNTERCLAIM: "دعوى مضادة", + courtsStanceNEUTRAL: "محايد", courtsVerdictResult: "النتيجة", courtsVerdictOrders: "الأوامر", courtsSettlementText: "التسوية", courtsSettlementAccepted: "مقبولة", + courtsSettlementPending: "معلّقة", courtsJudge: "القاضي", courtsThSupports: "الداعمون", courtsFilterOpenCase: 'فتح قضية', + courtsEvidenceFileLabel: 'ملف الدليل (صورة، صوت، فيديو أو PDF)', + courtsCaseMediators: 'الوسطاء', courtsCaseMediatorsPh: 'معرّفات Oasis للوسطاء، مفصولة بفاصلة', courtsMediatorsLabel: 'الوسطاء', courtsThCase: 'القضية', courtsThCreatedAt: 'تاريخ البدء', + courtsThActions: 'الإجراءات', courtsPublicPrefLabel: 'الظهور بعد الحل', courtsPublicPrefYes: 'أوافق على أن تكون هذه القضية علنية بالكامل', courtsPublicPrefNo: 'أفضل إبقاء التفاصيل خاصة', @@ -1681,11 +1844,8 @@ module.exports = { courtsMethodMEDIATION: 'وساطة', courtsNoCases: 'لا توجد قضايا.', reportsTitle: "التقارير", - reportsDescription: "أدر تقارير شبكتك وتابعها.", - reportsSearchPlaceholder: "ابحث عن البلاغات...", + reportsDescription: "إدارة وتتبع التقارير المتعلقة بالمشاكل والأخطاء والإساءات وتحذيرات المحتوى في شبكتك.", reportsFilterAll: "الكل", - reportsFilterRecent: "حديثة", - reportsFilterTop: "الأعلى", reportsFilterMine: "خاصتي", reportsFilterFeatures: "الميزات", reportsFilterBugs: "الأخطاء", @@ -1698,13 +1858,18 @@ module.exports = { reportsFilterInvalid: "غير صالح", reportsCreateButton: "إنشاء تقرير", reportsTitleLabel: "العنوان", + reportsDescriptionLabel: "الوصف", + reportsDescriptionPlaceholder: "يرجى تقديم وصف مفصّل.", reportsCategory: "الفئة", + reportsCategoryLabel: "الفئة", reportsCategoryFeatures: "الميزات", reportsCategoryBugs: "الأخطاء", reportsCategoryAbuse: "الإساءة", - reportsCategoryContent: "المحتوى", + reportsCategoryContent: "مشاكل المحتوى", reportsUpdateButton: "تحديث", reportsDeleteButton: "حذف", + reportsDateLabel: "التاريخ", + reportsUploadFile: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", reportsMineSectionTitle: "تقاريرك", reportsFeaturesSectionTitle: "طلبات الميزات", reportsBugsSectionTitle: "الأخطاء", @@ -1712,6 +1877,11 @@ module.exports = { reportsContentSectionTitle: "مشاكل المحتوى", reportsAllSectionTitle: "التقارير", reportsNoItems: "لا توجد تقارير متاحة.", + reportsValidationTitle: "يرجى إدخال عنوان صالح.", + reportsValidationDescription: "لا يمكن أن يكون الوصف فارغًا.", + reportsValidationCategory: "يرجى اختيار فئة.", + reportsCreatedAt: "أُنشئ في", + reportsCreatedBy: "بواسطة", reportsSeverity: "الخطورة", reportsSeverityLow: "منخفضة", reportsSeverityMedium: "متوسطة", @@ -1722,6 +1892,9 @@ module.exports = { reportsStatusUnderReview: "قيد المراجعة", reportsStatusResolved: "محلول", reportsStatusInvalid: "غير صالح", + reportsUpdateStatusButton: "تحديث الحالة", + reportsAnonymityOption: "إرسال بشكل مجهول", + reportsAnonymousAuthor: "مجهول", reportsConfirmButton: "تأكيد التقرير!", reportsConfirmations: "التأكيدات", reportsConfirmedSectionTitle: "التقارير المؤكدة", @@ -1769,20 +1942,6 @@ module.exports = { reportsRequestedActionLabel: 'الإجراء المطلوب', reportsRequestedActionPlaceholder: 'إزالة، إخفاء، وسم، تحذير، إلخ.', tribesTitle: "القبائل", - tribeFilterAll: "الكل", - tribeFilterRecent: "الأحدث", - tribeFilterMine: "لي", - tribeFilterMembership: "العضوية", - tribeFilterSubtribes: "القبائل الفرعية", - tribeFilterTop: "الأعلى", - tribeFilterGallery: "المعرض", - tribeFeedFilterTOP: "الأعلى", - tribeFeedFilterMINE: "لي", - tribeFeedFilterALL: "الكل", - tribeFeedFilterRECENT: "الأحدث", - courtsStanceDENY: "إنكار", - courtsStanceADMIT: "إقرار", - courtsStancePARTIAL: "إقرار جزئي", tribeAllSectionTitle: "القبائل", tribeMineSectionTitle: "قبائلك", tribeCreateSectionTitle: "إنشاء قبيلة", @@ -1794,6 +1953,13 @@ module.exports = { tribeviewSubTribeButton: "زيارة القبيلة الفرعية", tribeRootLabel: "الجذر", tribeDescription: "استكشف أو أنشئ قبائل في شبكتك.", + tribeFilterAll: "الكل", + tribeFilterMine: "خاصتي", + tribeFilterMembership: "العضوية", + tribeFilterRecent: "الأخيرة", + tribeFilterTop: "الأفضل", + tribeFilterSubtribes: "القبائل الفرعية", + tribeFilterGallery: "المعرض", tribeMainTribeLabel: "القبيلة الرئيسية", tribeCreateButton: "إنشاء قبيلة", tribeUpdateButton: "تحديث", @@ -1812,8 +1978,13 @@ module.exports = { tribeModeLabel: "الوضع", tribeIsAnonymousLabel: "الحالة", tribeMembersCount: "الأعضاء", + tribeInviteCodePlaceholder: "أدخل رمز الدعوة", + tribeJoinByCodeButton: "الانضمام بالرمز", + tribeJoinButton: "انضمام", tribeEnterInvite: "إدخال الرمز", tribeLeaveButton: "مغادرة", + tribeYes: "نعم", + tribeNo: "لا", tribePublic: "عامة", tribePrivate: "خاصة", tribeGenerateInvite: "دعوة فردية", @@ -1822,8 +1993,13 @@ module.exports = { tribeRemoveInvitation: "إزالة الدعوة", tribeCreatedAt: "أُنشئت في", tribeAuthor: "بواسطة", + tribeAuthorLabel: "المؤلف", tribeStrict: "صارم", tribeOpen: "مفتوح", + tribeFeedFilterRECENT: "الأخيرة", + tribeFeedFilterMINE: "خاصتي", + tribeFeedFilterALL: "الكل", + tribeFeedFilterTOP: "الأفضل", tribeFeedRefeeds: "إعادات النشر", tribeFeedRefeed: "إعادة نشر", tribeFeedMessagePlaceholder: "اكتب تغذية…", @@ -1833,12 +2009,17 @@ module.exports = { tribeNotFound: "لم يتم العثور على القبيلة!", createTribeTitle: "إنشاء قبيلة", updateTribeTitle: "تحديث القبيلة", + tribeSectionOverview: "نظرة عامة", tribeSectionInhabitants: "السكان", tribeSectionVotations: "التصويتات", tribeSectionEvents: "الأحداث", + tribeSectionReports: "التقارير", tribeSectionTasks: "المهام", tribeSectionFeed: "التغذية", tribeSectionForum: "المنتدى", + tribeSectionMarket: "السوق", + tribeSectionJobs: "الوظائف", + tribeSectionProjects: "المشاريع", tribeSectionMedia: "الوسائط", tribeSectionImages: "الصور", tribeSectionAudios: "الملفات الصوتية", @@ -1876,6 +2057,11 @@ module.exports = { tribeTaskStatusClosed: "إغلاق", tribeTaskAssign: "إسناد", tribeTaskUnassign: "إلغاء الإسناد", + tribeReportCreate: "إنشاء تقرير", + tribeReportsEmpty: "لا توجد تقارير بعد.", + tribeReportTitle: "العنوان", + tribeReportDescription: "الوصف", + tribeReportCategory: "الفئة", tribeVotationCreate: "إنشاء تصويت", tribeVotationsEmpty: "لا توجد تصويتات بعد.", tribeVotationTitle: "العنوان", @@ -1893,6 +2079,27 @@ module.exports = { tribeForumCategory: "الفئة", tribeForumReply: "رد", tribeForumReplies: "الردود", + tribeMarketCreate: "إنشاء عرض", + tribeMarketEmpty: "لا توجد عروض بعد.", + tribeMarketTitle: "العنوان", + tribeMarketDescription: "الوصف", + tribeMarketPrice: "السعر", + tribeMarketImage: "الصورة", + tribeMarketCategory: "الفئة", + tribeJobCreate: "إنشاء وظيفة", + tribeJobsEmpty: "لا توجد وظائف بعد.", + tribeJobTitle: "العنوان", + tribeJobDescription: "الوصف", + tribeJobLocation: "الموقع", + tribeJobSalary: "الراتب", + tribeJobDeadline: "الموعد النهائي", + tribeProjectCreate: "إنشاء مشروع", + tribeProjectsEmpty: "لا توجد مشاريع بعد.", + tribeProjectTitle: "العنوان", + tribeProjectDescription: "الوصف", + tribeProjectGoal: "الهدف", + tribeProjectFunded: "مُموَّل", + tribeProjectDeadline: "الموعد النهائي", tribeMediaUpload: "رفع وسائط", readDocument: "قراءة المستند", tribeCreateImage: "إنشاء صورة", @@ -1903,6 +2110,7 @@ module.exports = { tribeMediaEmpty: "لا توجد وسائط بعد.", tribeMediaTitle: "العنوان", tribeMediaDescription: "الوصف", + tribeMediaType: "النوع", tribeMediaTypeImage: "صورة", tribeMediaTypeVideo: "فيديو", tribeMediaTypeAudio: "صوت", @@ -1912,6 +2120,11 @@ module.exports = { tribeInviteCodeText: "رمز الدعوة: ", oasisVersionLabel: "إصدار Oasis", tribeInviteCodeHint: "شارك هذا الرمز مع من تريد دعوته. يمكنه الانضمام من /invites → Tribes.", + tribeGroupTribe: "القبيلة", + tribeGroupOffice: "المكتب", + tribeGroupNetwork: "الشبكة", + tribeGroupEconomy: "الاقتصاد", + tribeGroupMedia: "الوسائط", tribeStatusOpen: "مفتوح", tribeStatusClosed: "مغلق", tribeStatusInProgress: "قيد التنفيذ", @@ -1921,17 +2134,33 @@ module.exports = { tribePriorityCritical: "حرجة", tribeTaskFilterAll: "الكل", tribeMediaFilterAll: "الكل", + tribeReportCatBug: "خطأ", + tribeReportCatAbuse: "إساءة", + tribeReportCatContent: "محتوى", + tribeReportCatOther: "أخرى", tribeForumCatGeneral: "عام", tribeForumCatProposal: "اقتراح", tribeForumCatQuestion: "سؤال", tribeForumCatAnnouncement: "إعلان", + tribeMarketCatGoods: "سلع", + tribeMarketCatServices: "خدمات", + tribeMarketCatFood: "طعام", + tribeMarketCatOther: "أخرى", tribeStatusLabel: "الحالة", tribeSubTribes: "القبائل الفرعية", tribeSubTribesCreate: "إنشاء قبيلة فرعية", tribeSubTribesStrictDenied: "الوضع الصارم للقبيلة لا يسمح لك بإنشاء قبائل فرعية جديدة. يرجى الاتصال بالمسؤول.", + tribeSubTribesEmpty: "لم يتم إنشاء قبائل فرعية بعد.", + tribeActivityJoined: "انضم", + tribeActivityLeft: "غادر", + tribeActivityFeed: "تغذية", tribeActivityRefeed: "إعادة نشر", + tribeGroupAnalytics: "التحليلات", + tribeGroupCreative: "إبداعي", tribeSectionActivity: "النشاط", tribeSectionTrending: "الرائج", + tribeSectionOpinions: "الآراء", + tribeSectionPixelia: "بكسيليا", tribeSectionTags: "الوسوم", tribeSectionSearch: "بحث", tribeActivityEmpty: "لا يوجد نشاط بعد.", @@ -1943,15 +2172,25 @@ module.exports = { tribeTrendingPeriodWeek: "هذا الأسبوع", tribeTrendingPeriodAll: "كل الأوقات", tribeTrendingEngagement: "التفاعل", + tribeOpinionsEmpty: "لا توجد آراء بعد.", + tribeOpinionsCast: "تصويت", + tribeOpinionsRankings: "الترتيب", + tribeOpinionsAlreadyVoted: "تم التصويت بالفعل", tribeTopCategory: "الأكثر تصويتًا", + tribePixeliaTitle: "بكسيليا", + tribePixeliaDescription: "لوحة فن بكسل تعاونية.", + tribePixeliaPaint: "رسم", + tribePixeliaContributors: "المساهمون", + tribePixeliaTotalPixels: "البكسلات المرسومة", tribeTagsEmpty: "لم يتم العثور على وسوم.", + tribeTagsCloud: "سحابة الوسوم", + tribeTagsContentWith: "محتوى بالوسم", tribeSearchPlaceholder: "ابحث في محتوى القبيلة...", tribeSearchEmpty: "لم يتم العثور على نتائج.", tribeSearchResults: "النتائج", tribeSearchMinChars: "أدخل حرفين على الأقل للبحث.", agendaTitle: "الأجندة", agendaDescription: "هنا يمكنك العثور على جميع العناصر المسندة إليك.", - agendaSearchPlaceholder: "ابحث في جدول الأعمال...", agendaFilterAll: "الكل", agendaFilterToday: "اليوم", agendaFilterUpcoming: "القادمة", @@ -1965,14 +2204,23 @@ module.exports = { agendaFilterReports: "التقارير", agendaFilterTransfers: "التحويلات", agendaFilterJobs: "الوظائف", - agendaFilterIndustry: "الصناعة", - agendaFilterHousing: "السكن", agendaFilterProjects: "المشاريع", agendaFilterCalendars: "التقويمات", agendaNoItems: "لم يتم العثور على إسنادات.", + agendaAuthor: "بواسطة", agendaDiscardButton: "تجاهل", agendaRestoreButton: "استعادة", + agendaCreatedAt: "أُنشئ في", + agendaTitleLabel: "العنوان", agendaMembersCount: "الأعضاء", + agendaDescriptionLabel: "الوصف", + agendaStatus: "الحالة", + agendaVisibility: "الظهور", + agendaEventDate: "تاريخ الحدث", + agendaEventLocation: "الموقع", + agendaEventPrice: "السعر", + agendaEventUrl: "الرابط", + agendaTaskStart: "وقت البدء", agendaLocationLabel: "الموقع", agendaYes: "نعم", agendaNo: "لا", @@ -1982,6 +2230,11 @@ module.exports = { agendareportCategory: "الفئة", agendareportSeverity: "الخطورة", agendareportStatus: "الحالة", + agendareportDescription: "الوصف", + agendaTaskEnd: "وقت الانتهاء", + agendaTaskPriority: "الأولوية", + agendaTransferFrom: "من", + agendaTransferTo: "إلى", agendaTransferConcept: "المفهوم", agendaTransferAmount: "المبلغ", agendaTransferDeadline: "الموعد النهائي", @@ -1991,7 +2244,47 @@ module.exports = { voteNow: "صوّت الآن", alreadyVoted: "لقد أبديت رأيك بالفعل.", noOpinionsFound: "لم يتم العثور على آراء.", + RECENTButton: "الأخيرة", TOPButton: "الأفضل", + interestingButton: "مثير للاهتمام", + necessaryButton: "ضروري", + funnyButton: "مضحك", + disgustingButton: "مقزز", + sensibleButton: "حساس", + propagandaButton: "دعاية", + adultOnlyButton: "للبالغين فقط", + boringButton: "ممل", + confusingButton: "مربك", + usefulButton: "مفيد", + informativeButton: "إعلامي", + wellResearchedButton: "بحث جيد", + accurateButton: "دقيق", + needsSourcesButton: "يحتاج مصادر", + wrongButton: "خاطئ", + lowQualityButton: "جودة منخفضة", + creativeButton: "إبداعي", + insightfulButton: "ثاقب", + actionableButton: "قابل للتنفيذ", + inspiringButton: "ملهم", + loveButton: "حب", + clearButton: "واضح", + upliftingButton: "مُحفّز", + unnecessaryButton: "غير ضروري", + rejectedButton: "مرفوض", + misleadingButton: "مضلل", + offTopicButton: "خارج الموضوع", + duplicateButton: "مكرر", + clickbaitButton: "طُعم نقرات", + spamButton: "رسائل مزعجة", + trollButton: "تصيّد", + nsfwButton: "NSFW", + violentButton: "عنيف", + toxicButton: "سام", + harassmentButton: "تحرش", + hateButton: "كراهية", + scamButton: "احتيال", + triggeringButton: "مُحفّز سلبيًا", + opinionsCreatedAt: "أُنشئ في", opinionsTotalCount: "إجمالي الآراء", voteInteresting: "مثير للاهتمام", voteNecessary: "ضروري", @@ -2028,39 +2321,27 @@ module.exports = { voteCreative: "إبداعي", voteSpam: "رسائل مزعجة", voteAdultOnly: "للبالغين فقط", + publishBlog: "نشر مدونة", privateMessage: "رسالة خاصة", pmSendTitle: "الرسائل الخاصة", pmSend: "إرسال!", - pmCancel: "إلغاء", - pmReset: "إعادة تعيين", - pmSharedKeyHint: "شارك هذا المفتاح مع المستلم عبر قناة أخرى. فهو ضروري لفك التشفير.", - pmDescription: "استخدم هذا النموذج لإرسال رسالة/ملف مشفّر إلى سكان آخرين.", - pmComposeTitle: "إرسال رسالة", + pmDescription: "استخدم هذا النموذج لإرسال رسالة مشفرة إلى سكان آخرين.", pmRecipients: "المستلمون", pmRecipientsHint: "أدخل معرّفات Oasis مفصولة بفواصل", - pmLimitsHint: "حتى 7 مستلمين لكل رسالة.", - pmTextPlaceholder: "النص محدود بـ 7000 حرفًا.", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "الموضوع", pmSubjectHint: "أدخل موضوع الرسالة", pmText: "الرسالة", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "فك التشفير", - fileShareTitle: "مشاركة ملف", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "ملف", - fileShareDownload: "تنزيل", - fileShareMutualError: "يمكنك مشاركة الملفات فقط مع السكان ذوي الدعم المتبادل.", - fileShareNoFile: "لم يتم اختيار أي ملف.", - fileShareTooLarge: "الملف يتجاوز الحجم المسموح.", - fileShareFailed: "تعذّر تحضير الملف.", - fileShareSendError: "تعذّر إرسال الملف.", - fileShareUnavailable: "هذا الملف غير متاح حاليًا. حاول لاحقًا.", pmCrypterBadKey: "المفتاح المشترك غير صحيح!", pmCrypterTooLong: "الرسالة طويلة جدًا للتشفير باستخدام Crypter. يُرجى اختصارها والمحاولة مرة أخرى.", pmCrypterCipherLabel: "الرسالة المعاد تشفيرها", pmCrypterCharsLabel: "حرفًا", pmInvalidRecipients: "معرّف Oasis غير صالح (يجب أن يكون بالشكل @…=.ed25519).", pmEncryptionLabel: "التشفير", + pmFile: "المرفق", private: "خاص", privateDescription: "رسائلك المشفرة.", privateInbox: "الوارد", @@ -2070,27 +2351,19 @@ module.exports = { noPrivateMessages: "لا توجد رسائل خاصة.", pmFromLabel: "من:", pmToLabel: "إلى:", + pmInvalidMessage: "رسالة غير صالحة", pmNoSubject: "(بدون موضوع)", pmSubjectLabel: "الموضوع:", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "لقد تغيّر البيت الحاكم في L.A.R.P.", - politicalBotParliamentTitle: "بدأت دورة حكم جديدة في البرلمان.", - politicalBotTribeTitle: "بدأت دورة حكم جديدة في إحدى قبائلك.", - politicalBotLarpIntro: "الآن يحكم {house} خلال هذه الدورة: {cycle}", - politicalBotParliamentIntro: "الحكومة الحالية هي {gov}", - politicalBotParliamentIntroLeader: "الحكومة الحالية هي {gov} يمارسها {leader}", - politicalBotTribeIntro: "الحكومة الحالية في قبيلة {tribe} هي {gov}", - politicalBotTribeIntroLeader: "الحكومة الحالية في قبيلة {tribe} هي {gov} يمارسها {leader}", - politicalBotLeaderLabel: "القائد", + pmBodyLabel: "النص", + pmBotJobs: "42-JobsBOT", + pmBotProjects: "42-ProjectsBOT", + pmBotMarket: "42-MarketBOT", inboxJobSubscribedTitle: "اشتراك جديد في عرض عملك", pmInhabitantWithId: "ساكن بمعرّف OASIS:", pmHasSubscribedToYourJobOffer: "اشترك في عرض عملك", + inboxProjectCreatedTitle: "تم إنشاء مشروع جديد", + pmHasCreatedAProject: "أنشأ مشروعًا", inboxMarketItemSoldTitle: "تم بيع عنصر", - inboxShopSoldTitle: "تم بيع المنتج", pmYourItem: "عنصرك", pmHasBeenSoldTo: "تم بيعه إلى", pmFor: "مقابل", @@ -2098,6 +2371,7 @@ module.exports = { pmHasPledged: "تبرع بـ", pmToYourProject: "لمشروعك", blockchain: 'مستكشف الكتل', + blockchainTitle: 'مستكشف الكتل', blockchainDescription: 'استكشف وتصوّر الكتل في البلوكتشين.', blockchainNoBlocks: 'لم يتم العثور على كتل في البلوكتشين.', blockchainStatsTitle: 'الإحصاءات', @@ -2109,36 +2383,42 @@ module.exports = { blockchainBlockCarbon: 'البصمة الكربونية', blockchainBlockType: 'النوع', blockchainBlockTimestamp: 'الطابع الزمني', + blockchainBlockContent: 'الكتلة', + blockchainBlockURL: 'الرابط:', + blockchainContent: 'الكتلة', + blockchainContentPreview: 'معاينة محتوى الكتلة', blockchainLatestDatagram: 'آخر بيانات', - blockchainDatagram: "مخطط البيانات", - blockchainDetails: "عرض تفاصيل الكتلة", - blockchainViewBlockexplorer: "العرض في Blockexplorer", + blockchainDatagram: 'البيانات', + blockchainDetails: 'عرض تفاصيل الكتلة', + blockchainViewBlockexplorer: "عرض في مستكشف الكتل", + blockchainBlockInfo: 'معلومات الكتلة', + blockchainBlockDetails: 'تفاصيل الكتلة المحددة', blockchainBack: 'العودة إلى مستكشف الكتل', blockchainContentDeleted: "تم حذف هذا المحتوى", visitContent: "زيارة المحتوى", - favoriteAdd: "تثبيت في المفضلة", - pmContentTooltip: "إرسال رسالة خاصة", - favoriteRemove: "إزالة من المفضلة", - reportContent: "الإبلاغ عن المحتوى", banking: 'المصرفية', bankingTitle: 'المصرفية', - bankingDescription: "اقتصاد ECOin: الرصيد، الدخل الأساسي، الضرائب وقيمة الصناعة.", + bankingDescription: 'استكشف القيمة الحالية لـ ECOin وتخصيص الدخل الأساسي الشامل المقابل، الموزّع لكل حقبة بناءً على المشاركة والثقة.', bankOverview: 'نظرة عامة', bankEpochs: 'الحقب', bankRules: 'القواعد', pending: 'معلّق', closed: 'مغلق', + bankBack: 'العودة إلى المصرفية', bankViewTx: 'عرض المعاملة', bankClaimNow: 'طالب الآن', bankClaimUBI: 'طالب بـ UBI!', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: 'لا توجد تخصيصات UBI معلقة لهذه الحقبة.', bankEpoch: 'الحقبة', bankPool: 'المجمع (هذه الحقبة)', bankWeightsSum: 'مجموع الأوزان', + bankAllocations: 'التخصيصات', bankNoAllocations: 'لم يتم العثور على تخصيصات.', bankNoEpochs: 'لم يتم العثور على حقب.', bankEpochAllocations: 'تخصيصات الحقبة', @@ -2152,11 +2432,10 @@ module.exports = { bankEpochId: 'معرّف الحقبة', bankRuleHash: 'بصمة لقطة القواعد', bankViewEpoch: 'عرض الحقبة', - bankYourIndustryBalance: "حصتك الصناعية", - bankYourUbiMonth: "دخلك الأساسي (هذا الشهر)", - bankYourFundsMonth: "أموالك (هذا الشهر)", - bankIndustryBalance: "الإنتاج الصناعي (تقديري)", bankUserBalance: 'رصيدك', + ecoWalletNotConfigured: 'محفظة ECOin غير مُعدّة', + editWallet: 'تعديل المحفظة', + addWallet: 'إضافة محفظة', bankAddresses: 'العناوين', bankNoAddresses: 'لم يتم العثور على عناوين.', bankUser: 'معرّف Oasis', @@ -2181,10 +2460,18 @@ module.exports = { search: 'ابحث!', bankLocal: 'محلي', bankFromOasis: 'Oasis', + bankMyAddress: 'عنوانك', + bankRemoveMyAddress: 'إزالة عنواني', + bankNotRemovableOasis: 'لا يمكن إزالة العناوين محليًا', + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "لا توجد أموال!", bankUbiAvailableOk: "متوفر!", - bankUbiAvailability: "الدخل الأساسي (الشبكة)", + bankUbiAvailability: "توفر UBI", bankAlreadyClaimedThisMonth: "تم المطالبة بالفعل هذا الشهر", bankUbiThisMonth: "الدخل الأساسي (هذا الشهر)", bankUbiLastClaimed: "الدخل الأساسي (آخر مطالبة)", @@ -2198,6 +2485,13 @@ module.exports = { shopsTitle: "متاجر", shopDescription: "اكتشف وأدر المتاجر في الشبكة.", shopTitle: "متجر", + shopFilterAll: "الكل", + shopFilterMine: "متاجري", + shopFilterRecent: "الأحدث", + shopFilterTop: "الأفضل", + shopFilterProducts: "المنتجات", + shopFilterPrices: "الأسعار", + shopFilterFavorites: "المفضلة", shopUpload: "إنشاء متجر", shopAllSectionTitle: "جميع المتاجر", shopMineSectionTitle: "متاجري", @@ -2214,10 +2508,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "النشر على Clearnet", + shopClearnetUnpublish: "إلغاء النشر من Clearnet", + shopClearnetUrlLabel: "رابط Clearnet", + shopClearnetWarning: "النشر على Clearnet يجعل هذا المتجر قابلاً للفهرسة بواسطة محركات البحث والزواحف الخارجية.", shopAddFavorite: "إضافة مفضلة", shopRemoveFavorite: "إزالة مفضلة", shopOpen: "مفتوح", shopClosed: "مغلق", + shopOpenShop: "فتح المتجر", + shopCloseShop: "إغلاق المتجر", shopMakePrivate: "جعلها خاصة (E2E)", shopMakePublic: "جعلها عامة", shopProducts: "المنتجات", @@ -2245,6 +2545,8 @@ module.exports = { shopOrderPrice: "السعر", shopOrderBuyer: "المشتري", shopBackToShop: "العودة إلى المتجر", + shopShareUrl: "رابط المشاركة", + shopVisitShop: "زيارة المتجر", marketVisitItem: "زيارة العنصر", shopStatus: "الحالة", shopCreatedAt: "تاريخ الإنشاء", @@ -2253,10 +2555,12 @@ module.exports = { shopLocation: "الموقع", shopTags: "الوسوم", shopVisibility: "الرؤية", + shopImage: "صورة", shopShortDescription: "وصف قصير", shopShortDescriptionPlaceholder: "وصف مختصر لبطاقات المتجر (160 حرف كحد أقصى)", shopTitlePlaceholder: "اسم متجرك", shopDescriptionPlaceholder: "وصف تفصيلي لمتجرك", + shopUrlPlaceholder: "https://رابط-متجرك.com", shopLocationPlaceholder: "المدينة، البلد", shopTagsPlaceholder: "وسم1، وسم2، وسم3", mapTitlePlaceholder: "عنوان الخريطة", @@ -2274,6 +2578,7 @@ module.exports = { bankUnitMinutes: "دقائق", bankUnitDays: "أيام", bankExchangeNoData: 'لا توجد بيانات متاحة', + bankExchangeIndex: 'قيمة ECOin (1 ساعة)', bankInflation: 'تضخم ECOin (anual)', bankInflationMonthly: 'تضخم ECOin (mensual)', bankExchangeChartTitle: 'تطور قيمة ECOin عبر الزمن', @@ -2287,17 +2592,38 @@ module.exports = { bankingSyncStatusOutdated: 'قديم', statsTitle: 'الإحصائيات', statistics: "الإحصائيات", + statsInhabitant: "إحصائيات الساكن", statsDescription: "اكتشف إحصائيات عن شبكتك.", ALLButton: "الكل", MINEButton: "خاصتي", TOMBSTONEButton: "شواهد القبور", + statsYou: "أنت", + statsUserId: "معرّف Oasis", statsCreatedAt: "أُنشئ في", statsYourContent: "المحتوى", statsYourOpinions: "الآراء", + statsYourTombstone: "شواهد القبور", + statsNetwork: "الشبكة", + statsTotalInhabitants: "السكان", + statsDiscoveredTribes: "القبائل (العامة)", + statsPrivateDiscoveredTribes: "القبائل (الخاصة)", statsNetworkContent: "المحتوى", + statsYourMarket: "السوق", + statsYourJob: "الوظائف", + statsYourProject: "المشاريع", + statsYourTransfer: "التحويلات", + statsYourForum: "المنتديات", statsNetworkOpinions: "الآراء", + statsDiscoveredMarket: "السوق", + statsDiscoveredJob: "الوظائف", + statsDiscoveredProject: "المشاريع", + statsBankingTitle: "المصرفية", statsEcoWalletLabel: "محفظة ECOIN", statsEcoWalletNotConfigured: "غير مُعدّة!", + statsTotalEcoAddresses: "إجمالي العناوين", + statsDiscoveredTransfer: "التحويلات", + statsDiscoveredForum: "المنتديات", + statsNetworkTombstone: "شواهد القبور", statsBookmark: "العلامات المرجعية", statsEvent: "الأحداث", statsTask: "المهام", @@ -2319,12 +2645,16 @@ module.exports = { statsAiExchange: "الذكاء الاصطناعي", statsPUBs: 'PUBs', statsPost: "المنشورات", + statsOasisID: "معرّف Oasis", statsSize: "الإجمالي (الحجم)", statsBlockchainSize: "البلوكتشين (الحجم)", statsBlobsSize: "الملفات (الحجم)", statsActivity7d: "النشاط (آخر 7 أيام)", statsActivity7dTotal: "إجمالي 7 أيام", statsActivity30dTotal: "إجمالي 30 يومًا", + statsKarmaScore: "نقاط الكارما", + statsPublic: "عام", + statsPrivate: "خاص", day: "يوم", messages: "الرسائل", statsProject: "المشاريع", @@ -2336,14 +2666,30 @@ module.exports = { statsProjectsCancelled: "ملغى", statsProjectsGoalTotal: "الهدف الإجمالي", statsProjectsPledgedTotal: "إجمالي التبرعات", + statsProjectsSuccessRate: "نسبة النجاح", + statsProjectsAvgProgress: "متوسط التقدم", + statsProjectsMedianProgress: "متوسط التقدم (الوسيط)", + statsProjectsActiveFundingAvg: "متوسط التمويل النشط", + statsJobsTitle: "الوظائف", + statsJobsTotal: "إجمالي الوظائف", + statsJobsOpen: "مفتوح", + statsJobsClosed: "مغلق", + statsJobsOpenVacants: "الشواغر المفتوحة", + statsJobsSubscribersTotal: "إجمالي المشتركين", + statsJobsAvgSalary: "متوسط الراتب", + statsJobsMedianSalary: "متوسط الراتب (الوسيط)", statsMarketTitle: "السوق", statsMarketTotal: "إجمالي العناصر", statsMarketForSale: "للبيع", statsMarketReserved: "محجوز", statsMarketClosed: "مغلق", statsMarketSold: "مُباع", + statsMarketRevenue: "الإيرادات", + statsMarketAvgSoldPrice: "متوسط سعر البيع", statsUsersTitle: "السكان", user: "ساكن", + statsTombstoneTitle: "شواهد القبور", + statsNetworkTombstones: "شواهد قبور الشبكة", statsTombstoneRatio: "نسبة شواهد القبور (%)", statsParliamentCandidature: "ترشيحات البرلمان", statsParliamentTerm: "فترات البرلمان", @@ -2370,21 +2716,30 @@ module.exports = { aiConfiguration: "تعيين التوجيه", aiPromptUsed: "التوجيه", aiClearHistory: "مسح سجل المحادثة", + aiSharePrompt: "إضافة هذه الإجابة للتدريب الجماعي؟", + aiShareYes: "نعم", + aiShareNo: "لا", + aiSharedLabel: "تمت الإضافة للتدريب", + aiRejectedLabel: "لم تتم الإضافة للتدريب", aiServerError: "لم يتمكن الذكاء الاصطناعي من الرد. يرجى المحاولة مرة أخرى.", aiInputPlaceholder: "ما هو Oasis؟", typeAiExchange: "الذكاء الاصطناعي", aiApproveTrain: "إضافة للتدريب الجماعي", aiRejectTrain: "عدم التدريب", + aiTrainPending: "بانتظار الموافقة", aiTrainApproved: "تمت الموافقة للتدريب", aiTrainRejected: "تم الرفض للتدريب", aiSnippetsUsed: "المقتطفات المُستخدَمة", aiSnippetsLearned: "المقتطفات المُتعلَّمة", statsAITraining: "تدريب الذكاء الاصطناعي", + aiApproveCustomTrain: "التدريب باستخدام هذه الإجابة المخصصة", aiCustomAnswerPlaceholder: "اكتب إجابتك المخصصة…", + statsAIExchanges: "تبادلات النموذج", marketMineSectionTitle: "عناصرك", marketCreateSectionTitle: "إنشاء عنصر", marketUpdateSectionTitle: "تحديث", marketAllSectionTitle: "السوق", + marketRecentSectionTitle: "السوق الأخير", marketTitle: "السوق", marketDescription: "سوق لتبادل السلع أو الخدمات في شبكتك.", marketFilterAll: "الكل", @@ -2414,11 +2769,14 @@ module.exports = { marketItemDeadline: "الموعد النهائي", marketItemIncludesShipping: "يشمل الشحن؟", marketItemHighestBid: "أعلى عرض", + marketItemHighestBidder: "صاحب أعلى عرض", marketItemStock: "المخزون", marketOutOfStock: "نفد المخزون", + marketItemBidTime: "وقت العرض", marketActionsUpdate: "تحديث", marketUpdateButton: "تحديث", marketActionsDelete: "حذف", + marketActionsSold: "تعيين كمُباع", marketActionsChangeStatus: "تغيير الحالة", marketActionsBuy: "شراء!", marketAuctionBids: "العروض الحالية", @@ -2427,82 +2785,19 @@ module.exports = { marketNoItems: "لا توجد عناصر متاحة بعد.", marketYourBid: "عرضك", marketCreateFormImageLabel: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", + marketSearchLabel: "بحث", marketSearchPlaceholder: "ابحث عن عنوان أو وسوم", marketMinPriceLabel: "الحد الأدنى للسعر", marketMaxPriceLabel: "الحد الأقصى للسعر", + marketSortLabel: "ترتيب حسب", marketSortRecent: "الأحدث", marketSortPrice: "السعر", marketSortDeadline: "الموعد النهائي", marketSearchButton: "بحث", marketAuctionEndsIn: "ينتهي", marketAuctionEnded: "انتهى", + marketMyBidBadge: "عرضتَ", marketNoItemsMatch: "لا توجد عناصر تطابق بحثك.", - housingTitle: "السكن", - housingDescriptionText: "اكتشف وأدر الأماكن في شبكتك.", - modulesHousingLabel: "السكن", - modulesHousingDescription: "وحدة لاكتشاف الأماكن وإدارتها.", - typeHousing: "السكن", - housingFilterAll: "الكل", - housingFilterMine: "لي", - housingFilterRequested: "المطلوبة", - housingFilterSale: "للبيع", - housingFilterRent: "للإيجار", - housingFilterCouchsurfing: "الاستضافة", - housingFilterOpen: "المتاحة", - housingFilterClosed: "غير المتاحة", - housingFilterRecent: "حديثة", - housingFilterTop: "الأعلى", - housingType: "العرض", - housingTypeSALE: "للبيع", - housingTypeRENT: "للإيجار", - housingTypeCOUCHSURFING: "الاستضافة المجانية", - housingProperty: "العقار", - housingPropertyAPARTMENT: "شقة", - housingPropertyHOUSE: "منزل", - housingPropertyROOM: "غرفة", - housingPropertyLAND: "أرض", - housingPropertyOTHER: "أخرى", - housingTitleLabel: "العنوان", - housingTitlePlaceholder: "اسم المكان", - housingDescription: "الوصف", - housingDescriptionPlaceholder: "صف المكان والحي وما هو مشمول: الحمامات، الحديقة، الأسِرّة…", - housingRules: "القواعد", - housingRulesPlaceholder: "الحيوانات، التدخين، ساعات الهدوء، أقل مدة…", - housingPlace: "المكان", - housingPlacePlaceholder: "البلد، المدينة، العنوان", - housingPrice: "السعر", - housingPriceLabel: "السعر (ECO)", - housingSizeLabel: "المساحة (م²)", - housingFree: "مجاني", - housingPerMonth: "/شهر", - housingRooms: "الغرف", - housingSize: "المساحة", - housingCapacity: "الضيوف", - housingAvailableFrom: "متاح من", - housingAvailableTo: "متاح حتى", - housingTags: "الوسوم", - spreadEditWarning: "سيفقد هذا المحتوى نشراته بعد تعديله", - housingRequests: "الطلبات", - housingRequestButton: "طلب", - housingCancelButton: "إلغاء الطلب", - housingRequestedBadge: "مطلوب", - housingStatus: "الحالة", - housingStatusOPEN: "متاح", - housingStatusCLOSED: "غير متاح", - housingSetOpen: "اجعله متاحاً", - housingSetClosed: "اجعله غير متاح", - housingCreateButton: "إنشاء مكان", - housingUpdateButton: "تحديث", - housingDeleteButton: "حذف", - housingSearchPlaceholder: "ابحث عن أماكن...", - housingSearchButton: "بحث", - housingMinPrice: "أقل سعر", - housingMaxPrice: "أعلى سعر", - housingSortRecent: "الأحدث", - housingSortPrice: "الأقل سعراً", - housingSortRequests: "الأكثر طلباً", - housingSortRating: "الأعلى تقييماً", - housingNoItems: "لم يتم العثور على أماكن.", jobsTitle: "الوظائف", jobsCandidatesTitle: "المرشحون المقترحون", jobsCandidatesDescription: "السكان الذين تتطابق مهاراتهم مع هذه الوظيفة. أرسل لهم رسالة خاصة لدعوتهم.", @@ -2550,6 +2845,7 @@ module.exports = { jobLocationPresencial: "حضوري", jobLocationRemote: "عن بُعد", jobVacantsPlaceholder: "عدد المناصب", + jobSalaryPlaceholder: "الراتب بـ ECO لساعة عمل واحدة", jobImage: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", jobTasks: "المهام", jobType: "نوع الوظيفة", @@ -2559,6 +2855,7 @@ module.exports = { jobsFilterTop: "الأفضل", jobsTopTitle: "الوظائف الأعلى راتبًا", createJobButton: "نشر الوظيفة", + viewDetailsButton: "عرض التفاصيل", noJobsFound: "لم يتم العثور على عروض وظيفية.", jobAuthor: "بواسطة", jobTypeFreelance: "مستقل", @@ -2570,6 +2867,10 @@ module.exports = { jobsHoursOffered: "الساعات المعروضة", jobsHoursRequested: "الساعات المطلوبة في المقابل", jobsExchangeSkill: "المهارة المطلوبة في المقابل", + jobsHoursOfferedPlaceholder: "مثال: 4", + jobsHoursRequestedPlaceholder: "مثال: 4", + jobsExchangeSkillPlaceholder: "مثال: النجارة، التصميم", + jobExchangeHint: "بالنسبة لوظائف تبادل الساعات، املأ الحقول أدناه بدلاً من الراتب.", jobTimePartial: "دوام جزئي", jobTimeComplete: "دوام كامل", jobsDeleteButton: "حذف", @@ -2577,201 +2878,31 @@ module.exports = { jobsFilterApplied: "مُتقدَّم", jobsAppliedTitle: "طلباتي", jobsAppliedBadge: "مُتقدَّم", + jobsFilterFavs: "المفضلة", + jobsFavsTitle: "المفضلة", + jobsFilterNeeds: "يحتاج مساعدة", + jobsNeedsTitle: "يحتاج مساعدة", + jobsSearchLabel: "بحث", jobsSearchPlaceholder: "ابحث عن عنوان، وسوم، وصف...", jobsMinSalaryLabel: "الحد الأدنى للراتب", jobsMaxSalaryLabel: "الحد الأقصى للراتب", + jobsSortLabel: "ترتيب حسب", jobsSortRecent: "الأحدث", jobsSortSalary: "الأعلى راتبًا", jobsSortSubscribers: "الأكثر متقدمين", jobsSearchButton: "بحث", + jobsFavoriteButton: "إضافة للمفضلة", + jobsUnfavoriteButton: "إزالة من المفضلة", + jobsMessageAuthorButton: "رسالة خاصة", jobsApplicants: "المتقدمون", jobsUpdatedAt: "تم التحديث", jobSetOpen: "تعيين كمفتوح", + jobNewBadge: "جديد", jobsTagsLabel: "الوسوم", + jobsTagsPlaceholder: "وسم1، وسم2، وسم3", noJobsMatch: "لا توجد وظائف تطابق بحثك.", - industrySearchPlaceholder: "ابحث عن المصانع…", - industryAllSectors: "جميع القطاعات", - industryVisitButton: "زيارة الصناعة", - industryAdvanceTo: "تعيين إلى", - industryAmount: "المبلغ", - industryApproveBuild: "الموافقة", - industryBlueprint: "مخطط", - industryBlueprints: "مخططات", - industryBuild: "بناء", - industryBuildNotes: "ملاحظات", - industryBuildStart: "تاريخ البدء", - industryVoteUpdateButton: "التصويت للتحديث", - industryVoteDeleteButton: "التصويت للحذف", - industryRevokeVote: "سحب", - industryTimeLeft: "الوقت المتبقي", - industryBuildEnd: "تاريخ الانتهاء", - industryBuilds: "عمليات البناء", - industryBuildTitle: "العنوان", - industryContribute: "المساهمة", - industryContributeButton: "ساهم", - industryContributions: "المساهمات", - industryCreateBlueprintButton: "إنشاء مخطط", - industryDistribute: "توزيع", - industryDistributeButton: "التوزيع حسب الحصص", - industryDistribution: "التوزيع", - industryEcoAmount: "المبلغ (ECO)", - industryEditBlueprint: "تعديل المخطط", - industryFacility: "المصنع", - industryKindLabel: "النوع", - industryKind_labor: "العمل", - industryKind_material: "مادة", - industryKind_eco: "أموال", - industryLaborHours: "العمل (ساعات)", - industryHours: "ساعات", - industryMaterialItem: "مادة", - industryMaterials: "المواد", - industryEstMaterials: "إجمالي المواد", - industryEstLabor: "إجمالي العمل", - industryEstTotal: "السعر المقدر", - industryBuildingPrice: "سعر التصنيع", - industryFinalPrice: "السعر النهائي", - industryBlueprintDescPlaceholder: "المواصفات والأبعاد والوزن والوثائق…", - industryMaterialValue: "القيمة (ECO)", - industryMember: "عضو", - industryNewBlueprint: "مخطط جديد", - industryNewBuild: "اقتراح بناء", - industryEditBuild: "تحرير الإنتاج", - industryNeedBlueprint: "أنشئ مخططًا أولاً: كل إنتاج يصنع واحدًا.", - industryNoBlueprints: "لا توجد مخططات بعد.", - industryNoBuilds: "لا توجد عمليات بناء بعد.", - industryNote: "ملاحظة", - industryOutput: "المخرجات", - industryOutputKind: "نوع المنتج", - industryOutputValue: "قيمة المخرجات (ECO، مثلاً من البيع)", - industryPostJob: "إنشاء وظيفة", - industryPot: "الوعاء", - industryProposeBuildButton: "اقتراح البناء", - industryProposer: "المقترح", - industryAuthor: "المؤلف", - industrySellOnMarket: "إرسال إلى السوق", - industrySendToProjects: "إنشاء مشروع", - industrySkills: "المهارات", - industryTreasury: "الخزينة", - industryKind_physical: "مادي", - industryKind_digital: "رقمي", - industryBuildStatus_PROPOSED: "مقترح", - industryBuildStatus_REJECTED: "مرفوض", - industryBuildStatus_APPROVED: "معتمد", - industryBuildStatus_STOCKING: "تجهيز", - industryBuildStatus_IN_PRODUCTION: "قيد الإنتاج", - industryBuildStatus_COMPLETED: "مكتمل", - industryBuildStatus_FAILED: "فاشل", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "وظيفة تطابق سيرتك الذاتية", - jobsBotMatchIntro: "وظيفة تطابق سيرتك الذاتية.", - jobsBotMatchJob: "الوظيفة", - jobsBotMatchHint: "تصلك هذه الرسالة لأن سيرتك الذاتية مُدارة بالذكاء الاصطناعي. يمكنك تعطيلها من سيرتك.", - cvAiManaged: "مُدار بالذكاء الاصطناعي", - switchOn: "مُفعّل", - switchOff: "معطّل", - cvAiManagedHint: "عند التفعيل، ينبهك JobsBot برسالة خاصة عندما تطابق وظيفة سيرتك الذاتية.", - cvMatchThreshold: "نبّهني بدءًا من هذا المستوى من التطابق", - housingBotRequestedTitle: "طلب أحدهم أحد أماكنك.", - housingBotCancelledTitle: "تم إلغاء طلب على أحد أماكنك.", - housingBotYouRequestedTitle: "لقد طلبت مكاناً.", - housingBotYouCancelledTitle: "لقد ألغيت طلباً.", - housingBotUnavailableTitle: "المكان الذي طلبته لم يعد متاحاً.", - housingBotUnavailableSentTitle: "لقد أبلغت من طلبوا هذا المكان.", - industryBotAdmittedTitle: "تم قبولك في مصنع.", - industryBotApplicationTitle: "طلب عضوية جديد في مصنعك.", - industryBotInvitedTitle: "تمت دعوتك إلى مصنع.", - industryBotDissolvedTitle: "تم حل مصنع تنتمي إليه.", - industryBotBuildApprovedTitle: "تمت الموافقة على بناء.", - industryBotDistributedTitle: "تم توزيع مخرجات بناء.", - industryFilterRules: "القواعد", - industryRulesTitle: "القواعد", - industryRulesIntro: "تتيح Industry للسكان امتلاك وسائل الإنتاج جماعيًا: المصنع ورشة مملوكة للشبكة لا يملكها أحد بشكل خاص.", - industryRulesSteward: "المؤسس هو القيّم — راعٍ لا مالك: لا يمكنه بيع المصنع أو خصخصته أو حله بمفرده.", - industryRulesMembership: "تحدد سياسة العضوية كيفية الانضمام: مفتوح (فورًا)، بالتصويت (بقبول أصوات الأعضاء) أو بالدعوة فقط (يجب أن يدعوك عضو أولاً).", - industryRulesQuorum: "النصاب هو الحد الأدنى من الأعضاء الذين يجب أن يصوّتوا لتُحتسب القرار، حتى لا يقرر شخص واحد في مصنع صغير.", - industryRulesMajority: "الأغلبية هي نسبة الأصوات اللازمة (0.5 = نصف، 1 = إجماع)؛ يمر القرار عندما تبلغ أصوات نعم على الأقل max(النصاب، الأعضاء × الأغلبية).", - industryRulesDecisions: "يصوّت الأعضاء لقبول الجدد والموافقة على عمليات البناء وحل المصنع؛ لا يمكن للقيّم تجاوز هذه القرارات الجماعية.", - industryRulesBlueprints: "المخططات وصفات حرة (COPYLEFT) — المدخلات (مواد، ساعات عمل، مهارات) والمخرجات (سلعة مادية أو رقمية) — ويمكن لأي شخص نسخها.", - industryRulesBuilds: "البناء أمر إنتاج: مقترح ← معتمد (بالتصويت) ← تجهيز ← قيد الإنتاج ← مكتمل، ولا يقبل مساهمات إلا بعد الاعتماد.", - industryRulesContributions: "يساهم الأعضاء بالعمل (ساعات) أو المواد (قيمة ECO معلنة) أو ECO في البناء؛ كل مساهمة تكسب Karma.", - industryRulesLaborRate: "أجر العمل هو قيمة ECO لساعة عمل في هذا المصنع؛ يحوّل الساعات إلى Karma لمقارنة الجهد ورأس المال بعدل: Karma = ساعات × الأجر + قيمة المواد + ECO.", - industryRulesShares: "حصتك في البناء هي Karma الخاص بك ÷ إجمالي Karma، وتحدد مقدار قيمة المخرجات التي تحصل عليها.", - industryRulesDistribution: "عند اكتمال الإنتاج، يوزّع الوصي المبلغ (أموال الإنتاج بالإضافة إلى قيمة البيع) على المساهمين بنسبة حصصهم.", - industryRulesTreasury: "يحتفظ القيّم بمساهمات ECO كأمين لخزينة البناء حتى التوزيع؛ تُسجَّل جميع الحركات على الشبكة ويمكن أن تذهب النزاعات إلى Courts.", - industryJobs: "وظائف", - industryReportButton: "إبلاغ", - industryCreateTask: "إنشاء مهمة", - industryOpenDispute: "فتح نزاع", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "إنشاء دعوة", - industryPauseVotes: "أصوات الحالة", - industryTitle: "الصناعة", - industryDescription: "وحدة لإدارة وسائل الإنتاج بشكل جماعي.", - typeIndustryBuild: "بناء", - typeIndustryBlueprint: "مخطط", - typeIndustryAllocation: "توزيع", - typeIndustry: "الصناعة", - modulesIndustryLabel: "الصناعة", - modulesIndustryDescription: "وحدة لإدارة وسائل الإنتاج بشكل جماعي.", - industryFilterAll: "الكل", - industryFilterMember: "عضو", - industryFilterBlueprints: "مخططات", - industryFilterBuilds: "إنتاجات", - industryFilterMine: "لي", - industryFilterActive: "يعمل", - industryFilterPaused: "موقوفة", - industryFilterDissolved: "منحلة", - industryAllTitle: "الصناعة", - industryMemberTitle: "المصانع التي أنتمي إليها", - industryMineTitle: "مصانعي", - industryActiveTitle: "يعمل", - industryPausedTitle: "المصانع الموقوفة", - industryDissolvedTitle: "المصانع المنحلة", - industryNoFacilitiesFound: "لم يتم العثور على مصانع.", - industryCreateFacility: "إنشاء مصنع", - industryMembers: "الأعضاء", - industryMemberBadge: "عضو", - industryName: "الاسم", - industryNamePlaceholder: "اسم المصنع", - industryDescriptionLabel: "الوصف", - industryDescriptionPlaceholder: "ماذا ينتج هذا المصنع؟", - industrySector: "القطاع", - industryMembershipPolicy: "سياسة العضوية", - industryQuorum: "النصاب", - industryMajority: "الأغلبية", - industryLaborRate: "أجر العمل", - industryCreateButton: "إنشاء مصنع", - industryUpdateButton: "تحديث", - industrySteward: "القيم", - industryStatusActive: "يعمل", - industryStatusPaused: "موقوف", - industryStatusDissolved: "منحل", - industryStatusLabel: "الحالة", - industryJoinButton: "انضمام", - industryApplyButton: "طلب الانضمام", - industryLeaveButton: "مغادرة", - industryInviteButton: "دعوة", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "تحتاج إلى دعوة للانضمام.", - industryPauseButton: "صوّت للإيقاف", - industryResumeButton: "صوّت للاستئناف", - industryDeleteButton: "حذف", - industryPendingApplicants: "طلبات معلقة", - industryVotesYes: "نعم", - industryVotesNo: "لا", - industryAlreadyVoted: "تم التصويت", - industryAdmit: "قبول", - industryReject: "رفض", - industryDissolveTitle: "حل المصنع", - industryDissolveVote: "التصويت للحل", - industryPolicy_open: "مفتوح", - industryPolicy_vote: "بالتصويت", - industryPolicy_invite: "بالدعوة فقط", projectsTitle: "المشاريع", projectsDescription: "أنشئ ومُوّل وتابع مشاريع يقودها المجتمع في شبكتك.", - projectSearchPlaceholder: "ابحث عن المشاريع...", projectCreateProject: "إنشاء مشروع", projectCreateButton: "إنشاء مشروع", projectUpdateButton: "تحديث", @@ -2815,8 +2946,14 @@ module.exports = { projectPledgeTitle: "ادعم هذا المشروع", projectPledgePlaceholder: "المبلغ بـ ECO", projectBounties: "المكافآت", + projectBountiesInputLabel: "المكافآت (واحدة في كل سطر: العنوان|المبلغ [ECO]|الوصف)", + projectBountiesPlaceholder: "إصلاح خطأ واجهة|100|رابط المشكلة\nكتابة التوثيق|250|أمثلة الاستخدام", projectNoBounties: "لم يتم العثور على مكافآت.", projectTitle: "العنوان", + projectAddBountyTitle: "مكافأة جديدة", + projectBountyTitle: "عنوان المكافأة", + projectBountyAmount: "المبلغ (ECO)", + projectBountyDescription: "الوصف", projectMilestoneSelect: "اختيار مرحلة", projectBountyCreateButton: "إنشاء مكافأة", projectBountyStatus: "حالة المكافأة", @@ -2831,15 +2968,19 @@ module.exports = { projectMilestoneTitle: "عنوان المرحلة", projectMilestoneTargetPercent: "النسبة المئوية (%)", projectMilestoneDueDate: "التاريخ", + projectMilestoneCreateButton: "إنشاء مرحلة", projectMilestoneStatus: "حالة المرحلة", projectMilestoneOpen: "مفتوحة", projectMilestoneDone: "مكتملة", projectMilestoneDue: "مستحقة", + projectNoMilestones: "لم يتم العثور على مراحل.", projectMilestoneMarkDone: "تعيين كمنجزة", projectMilestoneTitlePlaceholder: "أدخل عنوان المرحلة", projectMilestoneDescriptionPlaceholder: "أدخل وصف هذه المرحلة", projectMilestoneDescription: "وصف المرحلة", + projectBudgetGoal: "الميزانية (الهدف)", projectBudgetAssigned: "المخصص للمكافآت", + projectBudgetRemaining: "المتبقي", projectBudgetOver: "تجاوز الميزانية: المخصص يتجاوز الهدف", projectFollowers: "المتابعون", projectFollowersTitle: "المتابعون", @@ -2852,6 +2993,7 @@ module.exports = { projectBackersTotalPledged: "إجمالي التبرعات", projectBackersYourPledge: "تبرعك", projectBackersNone: "لا توجد تبرعات بعد.", + projectNoRemainingBudget: "لا توجد ميزانية متبقية.", projectFilterBackers: "الداعمون", projectFilterApplied: "مُتقدَّم", projectAppliedTitle: "مُتقدَّم", @@ -2860,15 +3002,30 @@ module.exports = { projectBackerAmount: "إجمالي المساهمة", projectBackerPledges: "التبرعات", projectBackerProjects: "المشاريع", + projectPledgeAmount: "المبلغ", projectSelectMilestoneOrBounty: "اختر مرحلة أو مكافأة", projectPledgeButton: "تبرّع", footerLicense: "GPLv3", + footerPackage: "الحزمة", + footerVersion: "الإصدار", modulesModuleName: "الاسم", modulesModuleDescription: "الوصف", modulesModuleStatus: "الحالة", modulesTotalModulesLabel: "الوحدات المُحمَّلة", modulesEnabledModulesLabel: "مُفعَّلة", modulesDisabledModulesLabel: "مُعطَّلة", + modulesPopularLabel: "أبرز المنشورات", + modulesPopularDescription: "وحدة لاستقبال المنشورات الرائجة أو الأكثر مشاهدة أو الأكثر تعليقًا.", + modulesTopicsLabel: "المواضيع", + modulesTopicsDescription: "وحدة لاستقبال فئات النقاش القائمة على الاهتمامات المشتركة.", + modulesSummariesLabel: "الملخصات", + modulesSummariesDescription: "وحدة لاستقبال ملخصات النقاشات أو المنشورات الطويلة.", + modulesLatestLabel: "الأحدث", + modulesLatestDescription: "وحدة لاستقبال أحدث المنشورات والنقاشات.", + modulesThreadsLabel: "المحادثات", + modulesThreadsDescription: "وحدة لاستقبال المحادثات المجمعة حسب الموضوع أو السؤال.", + modulesMultiverseLabel: "الكون المتعدد", + modulesMultiverseDescription: "وحدة لاستقبال المحتوى من الأقران المتحدين الآخرين.", modulesInvitesLabel: "الدعوات", modulesInvitesDescription: "وحدة لإدارة وتطبيق رموز الدعوة.", modulesWalletLabel: "المحفظة", @@ -2909,12 +3066,8 @@ module.exports = { modulesOpinionsDescription: "وحدة لاكتشاف والتصويت على الآراء.", modulesTransfersLabel: "التحويلات", modulesTransfersDescription: "وحدة لاكتشاف وإدارة العقود الذكية (التحويلات).", - modulesFediverseLabel: "الكون المتعدد", - modulesBlogsLabel: "المدونات", - modulesBlogsDescription: "وحدة لاكتشاف المدونات وإدارتها.", - modulesPollsLabel: "الاستطلاعات", - modulesPollsDescription: "وحدة لسؤال الشبكة وعدّ الإجابات.", - modulesFediverseDescription: "أدر حساباتك الأخرى في الكون المتعدد، بما في ذلك إرسال واستقبال المحتوى.", + modulesFediverseLabel: "فيديفيرس", + modulesFediverseDescription: "أدر حساباتك الأخرى في الفيديفيرس، بما في ذلك إرسال واستقبال المحتوى.", modulesFeedLabel: "التغذية", modulesFeedDescription: "وحدة لاكتشاف ومشاركة النصوص القصيرة (التغذيات).", modulesParliamentLabel: "البرلمان", @@ -2950,33 +3103,37 @@ module.exports = { visibilityMakeHidden: "إخفاؤه", invitesInhabitantsTitle: "السكان", invitesInhabitantsFollow: "متابعة", - aiNavPlaceholder: "صف ما تحتاجه...", + aiNavPlaceholder: "إلى أين تريد الذهاب؟", aiNavDisabled: "تعطيل التنقل بالذكاء الاصطناعي.", - aiNavResultsTitle: "اقتراحات AINav", + aiNavResultsTitle: "اقتراحات التنقل بالذكاء الاصطناعي", aiNavQueryLabel: "الاستفسار", + aiNavResultsDescription: "اختر المسار الأنسب لما تبحث عنه.", + aiNavResultPath: "المسار", + aiNavResultDescription: "ما يمكنك فعله", aiNavResultMatch: "التطابق", aiNavResultsEmpty: "لا توجد مسارات مطابقة. جرّب /search.", + aiNavSearchInstead: "ابحث بدلًا من ذلك", modulesAINavLabel: "AINav", modulesAINavDescription: "وحدة لإجراء استعلامات بلغة طبيعية حول محتوى الشبكة.", + directConnect: "اتصال مباشر", directConnectDescription: "اتصل مباشرة بقرين عن طريق إدخال عنوان IP والمنفذ والمفتاح العام. سيتم إضافة القرين كاتصال مُتابَع.", peerHost: "IP", peerPort: "المنفذ (افتراضي: 8008)", peerPublicKey: "المفتاح العام (@...ed25519)", connectAndFollow: "اتصال", + deviceSourceLabel: "مصدر الجهاز", modulesPresetTitle: "الإعدادات الشائعة", - workflowsTitle: "مسارات العمل", - workflowsDescription: "يحدد مسار العمل السمة والوحدات المفعّلة.", - workflowsSet: "تطبيق مسار العمل", - workflow_default: "الافتراضي", - workflow_jobs: "العمل", - workflow_ruling: "الحُكم", - workflow_politics: "السياسة", - workflow_social: "اجتماعي", - workflow_business: "الأعمال", - workflow_night: "ليل", - workflow_mobile: "الهاتف", + modulesPreset_minimal: "الحد الأدنى", + modulesPreset_basic: "أساسي", + modulesPreset_social: "اجتماعي", + modulesPreset_economy: "اقتصادي", + modulesPreset_full: "كامل", statsCarbonFootprintTitle: "البصمة الكربونية", + statsCarbonFootprintNetwork: "البصمة الكربونية للشبكة", + statsCarbonFootprintYours: "بصمتك الكربونية", statsCarbonTombstone: "بصمة الحذف", + feedSuccessMsg: "تم نشر التغذية بنجاح!", + dominantOpinionLabel: "الرأي السائد", uploadMedia: "رفع وسائط (الحد الأقصى: 50 ميغابايت)", feedOpenDiscussion: "فتح النقاش", feedDetailTitle: "التغذية", @@ -3009,13 +3166,14 @@ module.exports = { mapDescriptionLabel: "الوصف", mapDescriptionPlaceholder: "صف الخريطة أو الموقع...", mapTypeLabel: "نوع الخريطة", - mapInviteMode: "دعوة", - mapTypeSingle: "فردي", - mapTypeOpen: "مفتوح", - mapTypeClosed: "مغلق", + mapTypeSingle: "فردي (الموقع الأولي فقط)", + mapTypeOpen: "مفتوح (يمكن لأي شخص إضافة علامات)", + mapTypeClosed: "مغلق (المنشئ فقط يضيف علامات)", mapTagsLabel: "الوسوم", mapTagsPlaceholder: "أدخل الوسوم مفصولة بفواصل", mapUrlLabel: "رابط الخريطة", + mapPickCoordLabel: "أو اختر موقعاً على الشبكة:", + mapMarkersLabel: "علامات", mapMarkersTitle: "العلامات", mapMarkerDefault: "علامة", mapMarkerLatLabel: "خط عرض العلامة", @@ -3042,9 +3200,14 @@ module.exports = { padTitle: "وسادة", modulesPadsLabel: "الوسادات", modulesPadsDescription: "وحدة لإدارة محررات النصوص التعاونية.", + padFilterAll: "الكل", + padFilterMine: "خاصتي", + padFilterRecent: "الأخيرة", + padFilterOpen: "مفتوح", + padFilterClosed: "مغلق", padCreate: "إنشاء وسادة", - padUpdate: "تحديث", - padDelete: "حذف", + padUpdate: "تحديث الوسادة", + padDelete: "حذف الوسادة", padTitleLabel: "العنوان", padTitlePlaceholder: "أدخل عنوان الوسادة...", padStatusLabel: "الحالة", @@ -3055,7 +3218,14 @@ module.exports = { padTagsLabel: "الوسوم", padTagsPlaceholder: "وسم1، وسم2، ...", padMembersLabel: "الأعضاء", + padVisitPad: "زيارة الوسادة", + padShareUrl: "مشاركة الرابط", padCreated: "تاريخ الإنشاء", + padAuthor: "المؤلف", + padGenerateCode: "إنشاء رمز", + padInviteCodeLabel: "رمز الدعوة", + padInviteCodePlaceholder: "أدخل رمز الدعوة...", + padValidateInvite: "تحقق", padStartEditing: "ابدأ التحرير!", padEditorPlaceholder: "ابدأ الكتابة...", padSubmitEntry: "إرسال", @@ -3068,11 +3238,12 @@ module.exports = { padClosedSectionTitle: "الوسادات المغلقة", padCreateSectionTitle: "إنشاء وسادة جديدة", padUpdateSectionTitle: "تحديث الوسادة", + padInviteGenerated: "تم إنشاء رمز الدعوة", typePad: "وسادات", padNew: "جديد", padAddFavorite: "إضافة إلى المفضلة", padRemoveFavorite: "إزالة من المفضلة", - padClose: "إغلاق", + padClose: "إغلاق اللوحة", padBackToEditor: "العودة إلى المحرر", padSearchPlaceholder: "البحث عن الوسادات...", @@ -3080,6 +3251,8 @@ module.exports = { modulesChatsDescription: "وحدة لاكتشاف وإدارة المحادثات المشفرة.", typeChat: "محادثات", typeChatMessage: "رسالة محادثة", + chatLabel: "المحادثات", + chatMessageLabel: "رسائل المحادثة", chatsTitle: "المحادثات", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3089,50 +3262,52 @@ module.exports = { chatDescription: "الوصف", chatCategory: "الفئة", chatStatus: "الحالة", + chatFilterAll: "الكل", + chatFilterMine: "لي", + chatFilterRecent: "الأخيرة", + chatFilterFavorites: "المفضلة", + chatFilterOpen: "مفتوح", + chatFilterClosed: "مغلق", chatCreate: "إنشاء محادثة", - chatUpdate: "تحديث", - chatDelete: "حذف", + chatUpdate: "تحديث المحادثة", + chatDelete: "حذف المحادثة", chatClose: "إغلاق المحادثة", chatVisitChat: "زيارة المحادثة", chatUntitled: "محادثة بدون عنوان", chatNoItems: "لا توجد محادثات.", chatParticipants: "المشاركون", - chatMessagesLabel: "الرسائل", - chatThreadsLabel: "المواضيع", + chatStartChatting: "ابدأ المحادثة!", + chatGenerateCode: "إنشاء رمز", + chatShareUrl: "مشاركة الرابط", chatCreatedAt: "تم الإنشاء", chatSearchPlaceholder: "بحث في المحادثات...", chatStatusOpen: "مفتوح", chatStatusInviteOnly: "بالدعوة فقط", chatStatusClosed: "مغلق", chatSendMessage: "إرسال", - chatJumpLatest: "آخر رسالة", - chatPickChat: "اختر محادثة لفتحها", - chatNoneYet: "لا توجد محادثة متاحة بعد.", - activitySearchPlaceholder: "ابحث في النشاط...", - trendingSearchPlaceholder: "ابحث في الرائج...", - opinionsSearchPlaceholder: "ابحث في الآراء...", - votesSearchPlaceholder: "ابحث في التصويتات...", - welcomeStepUxTitle: "اختر واجهتك", - welcomeStepUxText: "قرر شكل Oasis. يمكنك تغييره لاحقاً من الإعدادات.", - welcomeStepUxAction: "استخدم هذا العرض", - chatRateLimitTitle: "رسائل كثيرة جدًا", - chatRateLimitMessage: "لقد بلغت حد الرسائل الذي تقبله هذه الغرفة في الساعة. حاول لاحقًا.", chatMessagePlaceholder: "اكتب رسالتك...", chatNoMessages: "لا توجد رسائل بعد.", + chatLeave: "مغادرة المحادثة", + chatInviteCodeLabel: "أدخل رمز الدعوة", + chatJoinByInvite: "انضم", chatTitlePlaceholder: "عنوان المحادثة", chatDescriptionPlaceholder: "وصف المحادثة", chatTagsPlaceholder: "وسم1، وسم2", chatImageLabel: "اختر ملف صورة (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "رمز الدعوة", chatAuthor: "المؤلف", + chatCreated: "تم الإنشاء", chatAddFavorite: "إضافة للمفضلة", chatRemoveFavorite: "إزالة من المفضلة", chatPM: "رسالة", chatStatusLabel: "الحالة", chatCategoryLabel: "الفئة", + chatParticipantsLabel: "المشاركون", gamesTitle: "الألعاب", gamesDescription: "اكتشف وألعب بعض الألعاب.", gamesFilterAll: "الكل", gamesPlayButton: "العب!", + gamesBackToGames: "العودة إلى الألعاب", modulesGamesLabel: "الألعاب", modulesGamesDescription: "وحدة لاكتشاف وتشغيل بعض الألعاب.", modulesGraphosLabel: "غرافوس", @@ -3149,6 +3324,8 @@ module.exports = { gamesArkanoidDesc: "اكسر جميع الطوب بمضربك وكرتك. تحدي أركيد كلاسيكي.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "بينج بونج كلاسيكي ضد ذكاء اصطناعي. أول من يصل إلى 5 نقاط يفوز.", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "سباق ضد الوقت! تجنب السيارات وصل إلى خط النهاية قبل انتهاء الوقت.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "قد مركبتك عبر حقل الكويكبات. دمرها قبل أن تصطدم بك.", gamesRockPaperScissorsTitle: "حجر ورقة مقص", @@ -3169,6 +3346,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3179,6 +3358,12 @@ module.exports = { modulesCalendarsLabel: "التقاويم", modulesCalendarsDescription: "وحدة لاكتشاف وإدارة التقاويم.", typeCalendar: "تقويمات", + calendarFilterAll: "الكل", + calendarFilterMine: "تقاويمي", + calendarFilterOpen: "مفتوح", + calendarFilterClosed: "مغلق", + calendarFilterRecent: "الأحدث", + calendarFilterFavorites: "المفضلة", calendarCreate: "إنشاء تقويم", calendarUpdate: "تحديث", calendarDelete: "حذف", @@ -3191,17 +3376,24 @@ module.exports = { calendarTagsLabel: "الوسوم", calendarTagsPlaceholder: "وسم1، وسم2...", calendarParticipantsLabel: "المشاركون", + calendarParticipantsCount: "المشاركون", + calendarVisitCalendar: "زيارة التقويم", calendarCreated: "تم الإنشاء", + calendarAuthor: "المؤلف", calendarJoin: "انضمام", calendarGenerateInvite: "إنشاء دعوة", + calendarInviteCodePlaceholder: "أدخل رمز الدعوة...", + calendarValidateInvite: "التحقق من الرمز", + calendarJoined: "منضم", + calendarAddDate: "إضافة تاريخ", + calendarAddNote: "إضافة ملاحظة", calendarDateLabel: "التاريخ", calendarDatePlaceholder: "وصف هذا التاريخ...", - calendarNoteLabel: "ملاحظات", + calendarNoteLabel: "ملاحظة", calendarNotePlaceholder: "إضافة ملاحظة...", calendarFirstDateLabel: "التاريخ", calendarFirstNoteLabel: "ملاحظات", calendarIntervalLabel: "Interval", - calendarIntervalNone: "بدون تكرار", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3212,6 +3404,8 @@ module.exports = { calendarsDescription: "اكتشف وأدر التقويمات في شبكتك.", calendarMonthPrev: "← السابق", calendarMonthNext: "التالي →", + calendarMonthLabel: "التواريخ", + calendarsShareUrl: "رابط المشاركة", calendarAllSectionTitle: "التقاويم", calendarRecentSectionTitle: "التقويمات الأحدث", calendarFavoritesSectionTitle: "المفضلة", @@ -3225,6 +3419,7 @@ module.exports = { calendarRemoveFavorite: "إزالة من المفضلة", calendarSearchPlaceholder: "البحث عن تقاويم...", calendarAddEntry: "إضافة إدخال", + calendarLeave: "مغادرة التقويم", statsCalendar: "التقاويم", statsCalendarDate: "تواريخ التقويم", statsCalendarNote: "ملاحظات التقويم", @@ -3271,6 +3466,7 @@ module.exports = { torrentSortOldest: "الأقدم", torrentSortTop: "الأكثر تصويتاً", torrentNoMatch: "لا توجد تورنتات مطابقة.", + torrentMessageAuthorButton: "مراسلة المؤلف", torrentUpdatedAt: "تم التحديث", statsTorrent: "التورنت", typeTorrent: "التورنت", @@ -3278,18 +3474,10 @@ module.exports = { modulesTorrentsDescription: "وحدة لاكتشاف وإدارة التورنت.", favoritesFilterTorrents: "التورنت", favoritesFilterMarket: "السوق", - favoritesFilterEvents: "الفعاليات", - favoritesFilterForum: "المنتدى", - favoritesFilterHousing: "السكن", - favoritesFilterJobs: "الوظائف", - favoritesFilterProjects: "المشاريع", - favoritesFilterReports: "التقارير", - favoritesFilterTasks: "المهام", - favoritesFilterTransfers: "التحويلات", - favoritesFilterVotes: "التصويتات", favoritesFilterShopProducts: "منتجات المتجر", tribeSectionTorrents: "التورنت", tribeCreateTorrent: "رفع تورنت", + tribeMediaTypeTorrent: "تورنت", settingsWishTitle: "الرغبة", settingsWishDesc: "قم بتكوين رغبة الأفاتار الخاص بك.", settingsWishWhole: "Multiverse", @@ -3300,10 +3488,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "الإرسال حاجز UX. الاستقبال فلتر انتباه.", + pmBlockedNonMutual: "يمكنك إرسال الرسائل الخاصة فقط للسكان ذوي الدعم المتبادل.", inhabitantsPendingFollowsTitle: "طلبات الدعم المعلقة", inhabitantsPendingAccept: "قبول", inhabitantsPendingReject: "رفض", + bxEncrypted: "مشفر", + bxEncryptedHexLabel: "النص المشفر (معاينة)", tribeSectionGovernance: "الحوكمة", + tribeSubStatusPublic: "عامة", + tribeSubStatusPrivate: "خاصة", + tribeSubInheritedPrivate: "خاصة (موروثة من القبيلة الرئيسية)", tribePadInviteRequired: "لا يمكنك الوصول. اطلب دعوة.", tribeChatInviteRequired: "لا يمكنك الوصول للدردشة. اطلب دعوة.", tribeGovernanceDesc: "الحوكمة الداخلية لهذه القبيلة.", @@ -3343,31 +3537,44 @@ module.exports = { tribeGovNoHistorical: "لا يوجد أي سجل بعد", logsTitle: "السجل", logsDescription: "سجل تجربتك في الشبكة.", + logsReadMore: "اقرأ المزيد", logsViewTitle: "السجل", logsColumnType: "النوع", logsView: "عرض", + logsManualPrompt: "اكتب سجلك", logsUpdateButton: "تحديث", - logsEditTitle: "تحديث السجل", + logsEditTitle: "تعديل الإدخال", + logsCreateDescription: "سجل تجربتك...", logsCreateTitle: "إنشاء إدخال", logsWriteButton: "اكتب", logsTextPlaceholder: "صف تجاربك...", + logsTextField: "النص", + logsLabelPlaceholder: "تسمية...", + logsLabelField: "اكتب سجلك (تسمية)", + logsAiDisabledWarn: "فعّل وحدة الذكاء الاصطناعي في /modules لاستخدام السجلات المكتوبة بواسطة الذكاء.", + logsAiContextValue: "يوم blockchain", + logsAiContext: "السياق", + logsAiModStatus: "AImod", + logsModeApply: "تطبيق!", logsModeAIWritten: "AI-Assistant", logsModeManual: "يدوي", logsModeAI: "ذكاء", + logsColumnDelete: "حذف", + logsColumnEdit: "تعديل", logsColumnLog: "السجل", logsColumnDate: "التاريخ", logsDelete: "حذف", - logsEdit: "تحديث", + logsEdit: "تعديل", logsFilterAlways: "دائمًا", logsFilterYear: "السنة الماضية", logsFilterMonth: "الشهر الماضي", logsFilterWeek: "الأسبوع الماضي", logsFilterToday: "اليوم", - logsComments: "التعليقات", - logsAuthorEntries: "المدخلات", + logsFilterRecent: "الأحدث", + logsFilterAll: "الكل", logsCreate: "إنشاء إدخال", logsExport: "تصدير السجل", - logsExportOne: "تصدير السجل", + logsExportOne: "تصدير", logsViewDetails: "عرض التفاصيل", logsGenerateButton: "توليد نص", logsSearchText: "ابحث في السجلات...", @@ -3377,25 +3584,31 @@ module.exports = { modulesLogsLabel: "السجل", modulesLogsDescription: "وحدة لتسجيل تجاربك (عبر مساعد ذكاء اصطناعي).", statsLogsTitle: "السجل", + statsLogsEntries: "إدخالات", typeLog: "سجل", + blockchainCycle: "دورة", larpTitle: "L.A.R.P.", larpDescription: "طبقة لعب أدوار حية للتجربة التعاونية.", - larpNoHousesMatch: "لا توجد بيوت تطابق بحثك.", - larpSearchPlaceholder: "ابحث في البيوت...", larpAllHouses: "البيوت:", larpFilterRuling: "يحكم", larpFilterHouses: "البيوت", larpFilterRules: "FAQ", larpRulesTitle: "أسئلة شائعة عن L.A.R.P.", + larpRulesEntryTitle: "بيت البداية", larpRulesEntryText: "يبدأ كل ساكن في ACADEMIA افتراضياً. من ACADEMIA، اختر بيتاً من لوحة «الانضمام إلى بيت»: ستفتح اختبار الدخول الخاص به. أجب عن الأسئلة العشرة وأرسلها. إذا تجاوزت الحد (7/10) فستُقبل آلياً في ذلك البيت؛ وإلا تُرفض وتبقى في ACADEMIA. في كل الأحوال يسجّل النظام OasisID الخاص بك ودورة المحاولة، ويمنع محاولة جديدة حتى انتهاء فترة الانتظار وهي 30 دورة.", + larpRulesLeaveText: "يستطيع أعضاء أي بيت العودة إلى ACADEMIA في أي وقت من صفحة بيتهم؛ يعيد ذلك ضبط عضويتهم لكنه لا يلغي فترة انتظار الاختبار.", + larpRulesGovernanceTitle: "حائط الحوكمة", larpRulesGovernanceText: "خلال دورته، يحمل البيت الحاكم شارة «يحكم» وهو الوحيد الذي يُعرض حائطه علنياً ضمن تبويب «يحكم».", - larpRulesWallText: "لكل بيت جدار يكتب فيه أعضاؤه. جدار البيت خاص به، عدا جدار البيت الحاكم وجدار ACADEMIA، فيمكن لأي شخص قراءتهما.", + larpRulesSignTitle: "شعار L.A.R.P.", larpRulesSignText: "يستطيع السكان تفعيله (في /profile/edit > المستشعرات) لعرض صورة بيتهم الحالي كختم في الملف الشخصي وبطاقة المؤلف والساكن. يربط الختم بصفحة البيت.", larpPostsTitle: "الحائط", larpPostsEmpty: "لا توجد منشورات بعد.", + larpPostLabel: "منشور جديد", larpPostPlaceholder: "ماذا يريد هذا البيت أن يقول؟", + larpPostPublic: "عام (يراه أي شخص، وإلا فالأعضاء فقط)", larpPostSubmit: "انشر", + larpPostMembersOnly: "للأعضاء فقط", larpCycleLabel: "الدورة:", audioBackToBcs: "Back to BCS", audioFilterBcs: "BCS", @@ -3512,9 +3725,12 @@ module.exports = { bankTaxesLookupNote: "أدخل معرف كتلة للبحث عنه في خلاصتك المحلية.", bankTaxesUserNoteChipsClick: "انقر على أي شريحة لفحص كتلتها في blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntry: "رمز الدعوة", larpRulesInviteEntryText: "يمكن لأعضاء البيوت إنشاء رموز دعوة تمنح الوصول المباشر إلى البيت.", larpRulesOneHouseText: "يمكنك الانتماء إلى بيت واحد فقط في أي وقت. إذا لم تنتم إلى أي بيت، فأنت في ACADEMIA. كل صفحة بيت (غير ACADEMIA) تعرض لأعضائها زر «مغادرة البيت» الذي يعيد العضوية إلى ACADEMIA. المغادرة لا تلغي فترة انتظار الاختبار.", - larpRulesTestEntryText: "في اختبار WILL يمنح كل خيار وزنًا لبيت أو أكثر؛ وعند الإرسال يجمع النظام النقاط ويضمّك تلقائيًا إلى البيت الأعلى نقاطًا.", + larpRulesOneHouseTitle: "بيت واحد في كل مرة", + larpRulesTestEntry: "اختبار WILL", + larpRulesTestEntryText: "كل خيار يرجح بيتاً أو أكثر؛ عند الإرسال، يجمع النظام النقاط ويضمك تلقائياً إلى البيت صاحب أعلى نتيجة.", larpWillTestHint: "بمجرد الانتهاء، ستُسند إليك البيت الذي يتطابق بشكل أفضل مع إجاباتك. تتم معالجة إجاباتك الفردية محلياً ولا يتم تخزينها أبداً — يُنشر في خلاصتك فقط نتيجة تعيين البيت.", larpWillTestTitle: "اختبار WILL", settingsLanDesc: "الإعلان بشكل دوري عن هذا النظير لمثيلات Oasis الأخرى على نفس الشبكة المحلية. تعطيل لإيقاف بث UDP.", @@ -3533,27 +3749,31 @@ module.exports = { aiExchangeMarkHelpful: "+1 مفيد", larpCyclesUntilRuling: "دورة الحكم القادمة", larpVisitTribe: "زيارة القبيلة", - larpStartJourney: "ابدأ الرحلة", larpGoverning: "يحكم:", - larpStartHere: "ابدأ من هنا:", larpMyHouse: "بيتي:", larpMembersCount: "الأعضاء", + larpMembersTitle: "الأعضاء", + larpNoMembers: "لا يوجد أعضاء بعد.", larpRolesLabel: "الأدوار", larpFunctionLabel: "الوظيفة", larpMonthLabel: "دورة الحكم", larpLeaveToAcademia: "العودة إلى ACADEMIA", - larpAcademiaJoinTitle: "بيوت L.A.R.P.", + larpAcademiaJoinTitle: "الانضمام إلى بيت", + larpAcademiaJoinHint: "أجرِ اختبار الشخصية النفسي ليُسنَد إليك البيت الأنسب لك، أو استخدم رمز دعوة تشاركَه أحد أعضاء البيوت.", + larpTakeTestButton: "أجرِ اختبار الشخصية", larpInviteRedeem: "الانضمام إلى البيت", larpInviteCreate: "إنشاء رمز دعوة", larpInvitePlaceholder: "رمز الدعوة", larpInviteBannerTitle: "رمز دعوة جديد", larpInviteBannerHint: "شارك هذا الرمز مع شخص في ACADEMIA. ينتهي خلال 30 دورة أو بعد استخدام واحد.", + larpTestAssignedHouse: "البيت المُسنَد", larpTestRankingTitle: "النقاط لكل بيت", invitesHousesTitle: "L.A.R.P.", invitesHouseInviteCodePlaceholder: "رمز دعوة بيت", invitesHouseJoinButton: "الانضمام إلى البيت", larpLastAttemptTitle: "آخر محاولاتك", larpLastAttemptHouse: "البيت", + larpLastAttemptResult: "النتيجة", larpLastAttemptWhen: "متى", larpLastAttemptCooldown: "المحاولة التالية خلال", larpTestTitle: "اختبار الدخول", @@ -3562,14 +3782,37 @@ module.exports = { larpTestCooldownActive: "الاختبار التالي متاح خلال", larpTestCooldownDays: "دورات", larpTestResultTitle: "نتيجة الاختبار", + larpTestPassed: "اجتزت الاختبار — أهلاً!", + larpTestFailed: "لم تجتز الاختبار", larpTestScore: "الدرجة", larpTestNextAttempt: "يمكنك إجراء اختبار آخر بعد 30 دورة.", larpGoToHouse: "اذهب إلى بيتك", larpBackToAcademia: "العودة إلى ACADEMIA", + larpBackToHouses: "◀ البيوت", larpBadgeYou: "أنت", larpBadgeRuling: "يحكم", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "وحدة طبقة لعب الأدوار الحية في Oasis (البيوت التسعة).", + melodyUploadTitlePlaceholder: "Composition title", + melodyUploadDescPlaceholder: "Optional description", + ecoinTaxLabel: "ECOin Tax", + bankTaxesNetworkTitle: "Network Taxes", + bankTaxesUserTitle: "Your taxes", + bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", + bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", + bankTaxesExample: "Example", + bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", + statsEcoTaxTitle: "ECO Tax", + audioTranscodeTitle: "BCS Transcode", + audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", + audioTranscodeStegoTitle: "Embedded in the waveform", + audioBackToAudio: "Back to audio", + audioAuthorLabel: "Author", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + mapClickToCreate: "Click on the map to select a location", + mapClickToAddMarker: "Click on the map to add a marker", + gameScoreLabel: "GAME SCORES", larpProfileQ1: "عند مواجهة مشكلة معقدة، ما الذي تفعله أولاً؟", larpProfileQ1O1: "التحدث مع الآخرين للوصول إلى توافق", larpProfileQ1O2: "بناء نموذج أولي لاختباره", diff --git a/src/client/assets/translations/oasis_de.js b/src/client/assets/translations/oasis_de.js index dfa3426..0cc9ae8 100644 --- a/src/client/assets/translations/oasis_de.js +++ b/src/client/assets/translations/oasis_de.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { de: { + bbAdd: "Hinzufügen", + bbQuickActions: "Schnellaktionen", + bbPickTitle: "Modul anheften", + bbPickDesc: "Wähle ein Modul für den +-Platz in der unteren Leiste.", + bbPickNone: "Keins", + bbManage: "Bearbeiten", + bbYourBar: "Deine Leiste", + bbRemove: "Entfernen", + bbFull: "Maximum erreicht", + bbDone: "Fertig", + activityChangeFilter: "Filter ändern", + emptyConnectHint: "Verbinde dich mit einem Pub, um dich mit dem Netzwerk zu synchronisieren.", + emptyConnectCta: "Mit einem Pub verbinden", + menuCommunity: "Gemeinschaft", + activityGroupCommunity: "Gemeinschaft & Governance", + activityGroupOrg: "Organisation", + activityGroupComm: "Kommunikation", + activityGroupEconomy: "Wirtschaft", + activityGroupMedia: "Inhalte & Medien", + activityGroupOther: "Sonstiges", + languageName: "Deutsch", shopOrderStatusPaid: "Zahlung erhalten", shopOrderStatusReceived: "Erhalten", shopOrderMarkPaid: "Zahlung erhalten", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "Du hast noch keine Käufe.", shopOrderPmSeller: "PM Seller", shopOrderSeller: "Verkäufer", + shopOrderPaymentConcept: "Zahlung", shopOrderInvoice: "Rechnung", shopOrderInvoiceConcept: "Rechnung", shopOrderStatusPending: "Ausstehend", shopOrderStatusAccepted: "Angenommen", shopOrderStatusRejected: "Abgelehnt", shopOrderStatusShipped: "Versandt", + shopOrderStatusDelivered: "Geliefert", shopOrderAccept: "Annehmen", shopOrderReject: "Ablehnen", shopOrderMarkShipped: "Als versandt markieren", + shopOrderMarkDelivered: "Als geliefert markieren", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "Vertrag", shopOrderContractConcept: "Shop-Bestellung", shopOrdersPending: "Ausstehende Bestellungen", all: "Alle", @@ -50,13 +75,14 @@ module.exports = { viewJson: "JSON anzeigen", matchScore: "Übereinstimmung", preferencesLabel: "Einstellungen", - inhabitantProfileTitle: "Bewohner", + inhabitantProfileTitle: "Bewohnerprofil", contentWarningLabel: "Inhaltswarnung", invalidPost: "Ungültiger Inhalt", invalidPostHint: "Diese Nachricht hat ungültigen/leeren Text.", spreadBy: "Von", spreadChron: "Verbreitung", spreaded: "Verbreitet", + spreadMore: "mehr", spreadContentUnavailable: "Inhalt noch nicht verfügbar (Replikation ausstehend)", filteredByTag: "Nach Tag gefiltert", filteredByTagTitle: "Nach Tag gefiltert", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "Kein Schweregrad", calendarDeleteDate: "Datum löschen", calendarIntervalUntil: "Bis", + bookmarkCategory: "Kategorie", bookmarkLastVisit: "Letzter Besuch", profileClearnetBookmarksLabel: "Lesezeichen", + forumFilterHot: "Beliebt", invitesPort: "Port", currentlyUnrecheable: "FEHLER!", peerHostValidation: "Gültige IPv4 (z. B. 192.168.1.100) oder Hostname (z. B. pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "Jährliche Maximalschätzung", statsCarbonNetwork: "Netzwerk gesamt", statsCarbonOfEstMax: "der geschätzten Maximalkapazität", - statsCarbonYearProgress: "des Jahres vergangen", - statsNetworkTitle: "Netzwerk", - statsStorageTitle: "Speicher", - statsEcoTaxTitle: "ECO-Steuer", - statsAccountTitle: "Konto", - statsAveragesTitle: "Durchschnitte", statsCarbonOfNetwork: "des Netzwerk-Gesamtwerts", statsCarbonUser: "Dein Fußabdruck", statsContentCountColumn: "Anzahl", @@ -154,6 +176,10 @@ module.exports = { statsTopTagsTitle: "Top-Tags", statsTopTypesTitle: "Top-Inhaltstypen", statsTotalMsgs: "Nachrichten gesamt", + feedViewDetails: "Details anzeigen", + eventFileLabel: "Bild anhängen", + taskFileLabel: "Bild anhängen", + courtsRespondentRequired: "Beklagten-ID erforderlich", extended: "Multiversum", extendedDescription: [ "Wenn du jemanden unterstützt, kannst du Beiträge von dessen unterstützten Bewohnern herunterladen. Diese erscheinen hier, nach Aktualität sortiert.", @@ -197,27 +223,36 @@ module.exports = { graphosShowing: "Zeige", graphosYou: "Du", graphosTotalNodes: "Knoten insgesamt", + manualMode: "Manueller Modus", mentions: "Erwähnungen", mentionsDescription: [ - strong("Jede Nachricht, die dich @erwähnt"), - ".", + strong("Beiträge, die dich @erwähnen"), + ", nach Aktualität sortiert.", ], + nextPage: "Weiter", + previousPage: "Zurück", noMentions: "Du hast noch keine @Erwähnungen erhalten.", - mentionsSearchPlaceholder: "Erwähnungen suchen...", privateReminders: "ERINNERUNGEN", inboxFiltersLabel: "Filter:", inboxToggleToMutuals: "Auf Gegenseitige umschalten", inboxToggleToWhole: "Auf Alle umschalten", + privateFrom: "Von", + privateTo: "An", privateDate: "Datum", pmReply: "Antworten", pmReplies: "Antworten", + pmNew: "neu", + pmMarkRead: "Als gelesen markieren", inReplyTo: "ALS ANTWORT AUF", pmPreview: "Vorschau", pmPreviewTitle: "Nachrichtenvorschau", peers: "Verbindungen", searchDescription: "Beschreibung", + imageSearch: "Bildersuche", searchFromLabel: "Von", searchToLabel: "Bis", + searchMinOpinionsLabel: "Min. Meinungen", + searchInhabitantLabel: "Bewohner (Oasis ID)", searchQueryLabel: "Suche", searchPerPageLabel: "Ergebnisse pro Seite", settings: "Einstellungen", @@ -229,38 +264,67 @@ module.exports = { melodyTitle: 'Melody', melodyDescription: 'Spiel die Melodie deiner Blockchain — jeder Block wird zu einer Note.', melodyEmpty: 'Noch keine Noten — deine Blockchain hat noch keinen Block erzeugt.', + melodyCompositionHelp: 'Jeder Block deiner Blockchain wird je nach Typ und Größe zu einer Note.', + melodyStatsTitle: 'Aufschlüsselung nach Typ', + melodyInhabitantLabel: 'Bewohner', melodyTotalBlocks: 'Noten', + melodyType: 'Typ', + melodyCount: 'Blöcke', melodyFilterAll: 'ALLE', + melodyFilterShare: 'Melodie hochladen', + melodyShareTitle: 'Teile deine Melodie', + melodyShareHelp: 'Veröffentliche eine Momentaufnahme deiner aktuellen Melodie, damit andere sie anhören und remixen können.', + melodyShareTitleLabel: 'Titel (optional)', + melodyShareTitlePlaceholder: 'Ein Blockchain-Geist', + melodyShareSubmit: 'Melodie veröffentlichen', + melodyNoShared: 'Noch keine Blockchain-Melodien.', melodyRegenerate: 'Neu erzeugen', melodyDownload: "BCS herunterladen", + profileActivityTitle: 'Aktivität', + profileActivityMore: 'Alle Aktivität anzeigen', profileContentSectionTitle: 'Avatar-Inhalt', profileContentHelp: 'Wähle, welche deiner Module auf deinem Avatar angezeigt werden.', profileSensorsHelp: 'Optionale Metriken, die auf deinem Avatar angezeigt werden.', profileClearnetHelp: 'Module, die von außerhalb von Oasis aufgerufen werden können.', profileHubAll: 'Alle', + profileHubTitle: 'Öffentlicher Inhalt', + footerPulseTitle: 'Puls', + footerPulsePeers: 'Peers', + footerPulseInbox: 'Posteingang', + footerPulseSync: 'Letzte Sync', + footerPulseEcoValue: 'ECO/h', + footerPulseLastActivity: 'Letzte Aktivität', footerLicenseLabel: 'Lizenz', profileSwitchToOasis: 'Zurück zu Oasis', - profileVisibilityFediverse: "Multiversum", + profileSwitchToClearnet: 'Eintauchen ins Clearnet', + profileVisibilityFediverse: "Fediverse", + profileSwitchToFediverse: "Ins Fediverse eintauchen", + coordLabel: 'Koordinate (z.B. A3)', + coordPlaceholder: 'Koordinate eingeben', contributorsTitle: "Mitwirkende", + pixeliaBy: "von", colorLabel: 'Farbe wählen', paintButton: 'Malen!', + invalidCoordinate: 'Ungültige Koordinate', + goToMuralButton: "Wandbild anzeigen", totalPixels: 'Pixel gesamt', modules: "Module", modulesViewTitle: "Module", modulesViewDescription: "Konfiguriere deine Umgebung durch Aktivieren oder Deaktivieren von Modulen.", inbox: "Posteingang", multiverse: "Multiversum", - fediverse: "Multiversum", - fediverseDescription: "Verwalte deine anderen Multiversum-Konten, einschließlich Senden und Empfangen von Inhalten.", + fediverse: "Fediverse", + fediverseDescription: "Verwalte deine anderen Fediverse-Konten, einschließlich Senden und Empfangen von Inhalten.", fediverseStatus: "Status", - fediverseDisconnected: "Multiversum getrennt. Überprüfe %LINK% oder den Verbindungsstatus.", - fediverseSettingsTitle: "Multiversum", + fediverseDisconnected: "Fediverse getrennt. Überprüfe %LINK% oder den Verbindungsstatus.", + fediverseSettingsTitle: "Fediverse", fediverseInstanceLabel: "Adresse", fediverseTokenLabel: "Zugriffstoken", fediverseTokenHelp: "Lege dein Zugriffstoken fest. Es wird verwendet, um dein Mastodon-Konto aus Oasis heraus zu verwalten.", fediverseConnect: "Verbinden", fediverseConnectedAs: "Verbunden als", fediverseDisconnect: "Trennen", + fediverseEmpty: "Wenn du weitere Fediverse-Konten in %LINK% verbindest, kannst du sie von hier aus verwalten.", fediverseEmptyLink: "deinen Einstellungen", fediverseComposePlaceholder: "Was denkst du gerade?", fediverseReplyPlaceholder: "Schreibe deine Antwort…", @@ -269,15 +333,21 @@ module.exports = { fediverseReply: "Antworten", fediverseBoosted: "teilte", fediverseNoPosts: "Deine Timeline ist leer.", + fediverseThread: "Thread", fediverseTimeline: "Zeitleisten", + fediverseManageTimeline: "Zeitleiste verwalten", fediverseFollowers: "Follower", fediverseFollowing: "Folge ich", fediversePosts: "Beiträge", fediverseJoined: "Beigetreten", + fediverseOverview: "Übersicht", fediverseRefresh: "Aktualisieren", fediverseWrite: "Schreiben", fediversePreview: "Vorschau", + fediverseEdit: "Bearbeiten", + fediverseOpenFeed: "Feed ansehen", fediverseManage: "Verwalten", + fediverseStatusNotConnected: "Nicht verbunden", fediverseError: "Mastodon konnte nicht erreicht werden.", fediverseErrInstance: "Ungültige Instanz-URL.", fediverseErrAuth: "Ungültiges oder abgelaufenes Token.", @@ -288,6 +358,17 @@ module.exports = { fediverseErrPost: "Konnte nicht veröffentlichen.", fediverseErrMedia: "Datei konnte nicht hochgeladen werden.", fediverseErrEmpty: "Schreibe etwas oder hänge eine Datei an.", + popularLabel: "Höhepunkte", + topicsLabel: "Themen", + latestLabel: "Neueste", + summariesLabel: "Zusammenfassungen", + threadsLabel: "Diskussionen", + multiverseLabel: "Multiversum", + inboxLabel: "Posteingang", + invitesLabel: "Einladungen", + walletLabel: "Geldbörse", + legacyLabel: "Schlüssel", + cipherLabel: "Verschlüsselung", bookmarksLabel: "Lesezeichen", videosLabel: "Videos", torrentsLabel: "Torrents", @@ -298,14 +379,18 @@ module.exports = { inhabitantsLabel: "Bewohner", trendingLabel: "Trends", eventsLabel: "Termine", + tasksLabel: "Aufgaben", apply: "Anwenden", menuPersonal: "Persönlich", + menuContent: "Inhalt", + menuBlogs: "Blogs", menuGovernance: "Regierung", menuOffice: "Büro", - menuNetwork: "Gemeinschaft", + menuMultiverse: "Multiversum", + menuNetwork: "Netzwerk", menuCreative: "Kreativ", menuEconomy: "Wirtschaft", - menuMedia: "Bibliothek", + menuMedia: "Medien", menuTools: "Werkzeuge", comment: "Kommentar", subtopic: "Unterthema", @@ -315,6 +400,13 @@ module.exports = { follow: "Unterstützen", block: "Blockieren", unblock: "Entsperren", + newerPosts: "Neuere Beiträge", + olderPosts: "Ältere Beiträge", + feedRangeEmpty: "Der angegebene Bereich ist leer für diesen Feed. Versuchen Sie den ", + seeFullFeed: "gesamten Feed", + feedEmpty: "Das Oasis-Netzwerk hat nie Beiträge von diesem Konto gesehen.", + beginningOfFeed: "Dies ist der Anfang des Feeds", + noNewerPosts: "Es wurden noch keine neueren Beiträge empfangen.", relationshipNotFollowing: "Du wirst nicht unterstützt", relationshipTheyFollow: "Unterstützt dich", relationshipMutuals: "Gegenseitige Unterstützung", @@ -324,20 +416,25 @@ module.exports = { relationshipBlockedBy: "Du bist blockiert", relationshipMutualBlock: "Gegenseitige Blockierung", relationshipNone: "Du unterstützt nicht", + relationshipConflict: "Konflikt", relationshipBlockingPost: "Blockierter Beitrag", viewLikes: "Verbreitungen ansehen", spreadedDescription: "Liste der vom Bewohner verbreiteten Beiträge.", + totalspreads: "Verbreitungen gesamt", + attachFiles: "Dateien anhängen", preview: "Vorschau", publish: "Schreiben", contentWarningPlaceholder: "Betreff zum Beitrag hinzufügen (optional)", - publishWarningPlaceholder: "Text auf 7000 Zeichen begrenzt.", - publishTooLong: "Dein Beitrag ist zu lang. Bitte kürze ihn.", + privateWarningPlaceholder: "Bewohner hinzufügen, um einen privaten Beitrag zu senden (optional)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "ACHTUNG: Aufgrund der Blockchain-Technologie kann ein veröffentlichter Beitrag nicht bearbeitet oder gelöscht werden.", ], commentWarning: [ "ACHTUNG: Aufgrund der Blockchain-Technologie kann ein veröffentlichter Beitrag nicht bearbeitet oder gelöscht werden.", ], + commentPublic: "öffentlich", + commentPrivate: "privat", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -357,6 +454,7 @@ module.exports = { ".", ], publishCustom: "Erweiterten Beitrag schreiben", + subtopicLabel: "Ein Unterthema dieses Beitrags erstellen", messagePreview: "Beitragsvorschau", mentionsMatching: "Passende Erwähnungen", mentionsName: "Name", @@ -379,105 +477,36 @@ module.exports = { ssbLogStream: "Blockchain", ssbLogStreamDescription: "Konfiguriere das Nachrichtenlimit für Blockchain-Streams.", saveSettings: "Einstellungen speichern", + exportTitle: "Daten exportieren", exportDescription: "Passwort festlegen (mind. 32 Zeichen), um deinen Schlüssel zu verschlüsseln", exportDataTitle: "Sicherung", exportDataDescription: "Lade deine Daten herunter (geheimer Schlüssel ausgeschlossen!)", exportDataButton: "Datenbank herunterladen", + pubWallet: "PUB-Wallet", + pubWalletDescription: "Setze die PUB-Wallet-URL. Diese wird für PUB-Transaktionen (einschließlich UBI) verwendet.", + pubWalletConfiguration: "Konfiguration speichern", + importTitle: "Daten importieren", importDescription: "Importiere deinen verschlüsselten geheimen Schlüssel, um deinen Avatar zu aktivieren", + importAttach: "Verschlüsselte Datei anhängen (.enc)", + passwordLengthInfo: "Das Passwort muss mindestens 32 Zeichen lang sein.", passwordImport: "Gib dein Passwort ein, um die Daten zu entschlüsseln", exportPasswordPlaceholder: "Klein-, Großbuchstaben, Zahlen und Symbole verwenden", - fileInfo: "Dein verschlüsselter Schlüssel wird auf dein Gerät heruntergeladen (Name: oasis.enc)", - developer: "Entwicklung", - devTitle: "Entwicklung", - welcomeBannerText: "Willkommen bei OASIS. Folge diesen kurzen Schritten, um deinen Avatar einzurichten.", - aiSuggestionBanner: "Vorschlag:", - aiSuggestionsEnable: "KI-Vorschläge", - welcomeBannerAction: "Los geht es!", - welcomeTitle: "Willkommen", - welcomeStepGreetingAction: "Senden", - welcomeJoinPubButton: "Pub beitreten", - welcomeStepLarpAction: "\"Der Akademie\" beitreten", - welcomeStepLarpText: "Mach mit bei unserem Live-Rollenspiel.", - welcomeStepLarpTitle: "L.A.R.P. beitreten", - welcomeStepGreetingTitle: "Sende ein \"Hallo Welt!\"", - welcomeStepGreetingText: "Sende eine Nachricht, damit andere Bewohner dich entdecken können.", - welcomeJoinPub: "Beitreten:", - welcomeDescription: "Ein paar Dinge, die sich vor dem Start lohnen.", - welcomeProgress: "Erledigt", - welcomeStepDone: "fertig", - welcomeStepPending: "offen", - welcomeSkip: "Jetzt nicht", - welcomeStepLanguageTitle: "Wähle deine Sprache", - welcomeStepLanguageText: "Oasis spricht mehrere Sprachen. Du kannst sie jederzeit ändern.", - welcomeStepProfileTitle: "Bearbeite dein Profil", - welcomeStepProfileText: "Wähle einen Namen, füge eine Beschreibung hinzu und setze ein Bild, damit andere Bewohner dich erkennen.", - welcomeStepProfileAction: "Profil speichern", - welcomeStepFederationTitle: "Tritt dem Hauptnetzwerk bei", - welcomeStepFederationText: "Verbinde dich mit unserem Pub, um andere Bewohner zu treffen und zu replizieren.", - welcomeStepFederationAction: "Zu den Einladungen", - welcomeStepBackupTitle: "Sichere deine ID", - welcomeStepBackupText: "Deine Identität ist eine Schlüsseldatei auf diesem Gerät.", - welcomeStepBackupWarning: "WENN SIE VERLOREN GEHT, KANN SIE NIEMAND WIEDERHERSTELLEN.", - welcomeStepBackupAction: "Sicherung!", - welcomeCompleteTitle: "Alles bereit.", - welcomeCompleteText: "Dein Knoten ist bereit. Von hier an wächst das Netzwerk mit den Menschen, denen du folgst.", - welcomeFindPeople: "Bewohner finden", - devFilterFiles: "DATEIEN", - devFilterSearch: "SUCHEN", - devFilterModules: "MODULE", - devFilterTranslations: "ÜBERSETZUNGEN", - devFilterStyles: "STILE", - devFilterTests: "TESTS", - devVersion: "Version", - devNodeVersion: "Node", - devStatsTests: "Test-Suites", - devParentFolder: "Übergeordneter Ordner", - devOpenFolder: "öffnen", - devDescription: "Erkunde den Quellcode, der auf diesem Knoten läuft.", - devRoot: "Wurzel", - devSearchPlaceholder: "Im Code zu suchender Text", - devAnyExtension: "Beliebige Datei", - devStatsFiles: "Dateien", - devStatsLines: "Zeilen", - devStatsModules: "Module", - devStatsLanguages: "Sprachen", - devNotViewable: "nicht anzeigbar", - devEmptyFolder: "Dieser Ordner ist leer.", - devSize: "Größe", - devShowing: "Anzeige", - devLine: "Zeile", - devReportBug: "Fehler Melden", - devCreateTask: "Aufgabe Erstellen", - devRaw: "RAW", - devReportContext: "Beim Lesen des Oasis-Quellcodes gefunden", - devTaskPrefix: "Prüfen", - devPrevPage: "Vorherige Zeilen", - devNextPage: "Nächste Zeilen", - devMatches: "Treffer", - devFilesScanned: "Dateien geprüft", - devNoResults: "Noch keine Treffer.", - devColModule: "Modul", - devColState: "Status", - devColModel: "Modell", - devColView: "Ansicht", - devColRoutes: "Routen", - devColTests: "Tests", - devOn: "an", - devOff: "aus", - modulesDevLabel: "Entwicklung", - modulesDevDescription: "Modul zur Verwaltung des Oasis-Quellcodes.", + fileInfo: "Dein verschlüsselter Schlüssel wird im Systemverzeichnis gespeichert (Name: oasis.enc)", themeIntro: "Wähle ein Design.", setTheme: "Design festlegen", language: "Sprache", languageDescription: "Wenn du eine andere Sprache verwenden möchtest, wähle sie hier aus.", setLanguage: "Sprache festlegen", + peerConnections: "Verbindungen", peerConnectionsIntro: "Verwalte alle Verbindungen mit anderen Peers.", peerConnectionsTitle: "Verbindungen", online: "Online", offline: "Offline", discovered: 'Entdeckt', unknown: 'Unbekannt', + lanBroadcastLabel: "LAN-Broadcast", lanBroadcastActive: "Aktiv (UDP-Multicast im LAN)", + lanBroadcastDisabled: "Deaktiviert (Pub-Modus)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -490,6 +519,8 @@ module.exports = { noConnections: "Keine Peers verbunden.", noDiscovered: "Keine Peers entdeckt.", noUnknownPeers: "Keine unbekannten Peers im Freundes-Graphen.", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -499,6 +530,9 @@ module.exports = { peerImport: "Importieren", peerImportTitle: "Peer-Liste importieren", peerImportPlaceholder: "Eine Multiserver-Adresse pro Zeile einfügen oder eine .txt-Datei hochladen…", + noSupportedConnections: "Keine Peers unterstützt.", + noBlockedConnections: "Keine Peers blockiert.", + noRecommendedConnections: "Keine Peers empfohlen.", connectionActionIntro: "", startNetworking: "Netzwerk starten", @@ -511,19 +545,9 @@ module.exports = { homePageDescription: "Wähle, welches Modul deine Startseite sein soll.", saveHomePage: "Startseite festlegen", invites: "Einladungen", + invitesTitle: "Einladungen", + invitesInvites: "Einladungen", invitesDescription: "Verwalte und verwende Einladungscodes in deinem Netzwerk.", - invitesPubsHint: "Füge einen Pub-Einladungscode ein, um dich mit ihm zu föderieren und seine Bewohner zu replizieren.", - invitesFederationsHint: "Pubs, mit denen du föderiert bist: aktualisieren, unerreichbare entfernen oder die Liste exportieren.", - invitesHousesHint: "Gib einen Haus-Code ein, um einem LARP-Haus beizutreten und an seiner Wirtschaft teilzunehmen.", - invitesTribesHint: "Gib einen Stammes-Code ein, um Mitglied zu werden und die privaten Inhalte zu sehen.", - invitesChatsHint: "Gib einen Chat-Code ein, um dem Gespräch und seinen Threads beizutreten.", - invitesPadsHint: "Gib einen Pad-Code ein, um gemeinsam an einem Dokument zu schreiben.", - invitesCalendarsHint: "Gib einen Kalender-Code ein, um Termine und Erinnerungen zu teilen.", - invitesEventsHint: "Gib einen Event-Code ein, um an einer privaten Veranstaltung teilzunehmen.", - invitesForumsHint: "Gib einen Forum-Code ein, um in einem privaten Forum zu lesen und zu antworten.", - invitesMapsHint: "Gib einen Karten-Code ein, um Orte auf einer gemeinsamen Karte zu sehen und hinzuzufügen.", - invitesShopsHint: "Gib einen Shop-Code ein, um einem Shop und seinem Katalog beizutreten.", - invitesInhabitantsHint: "Gib einen Bewohner-Code ein, um euch gegenseitig zu folgen und Inhalte auszutauschen.", invitesTribesTitle: "Stämme", invitesTribeInviteCodePlaceholder: "Stamm-Einladungscode eingeben", invitesTribeJoinButton: "Stamm beitreten", @@ -554,6 +578,7 @@ module.exports = { invitesAlreadyFederated: "Du bist bereits mit diesem Pub föderiert.", invitesFederationsTitle: "Föderationen", invitesAcceptedInvites: "Föderiert", + invitesNoInvites: "Noch keine Einladungen angenommen.", invitesUnfollow: "Entfolgen", invitesFollow: "Folgen", invitesUnfollowedInvites: "Nicht föderiert", @@ -573,24 +598,39 @@ module.exports = { panicMode: "Panikmodus!", encryptData: "Passwort festlegen (mind. 32 Zeichen), um deine Blockchain zu verschlüsseln", decryptData: "Passwort eingeben, um deine Blockchain zu entschlüsseln", + panicModeDescription: "Verschlüssele/Entschlüssele oder LÖSCHE deine Blockchain", removeDataDescription: "WARNUNG: Dieser Vorgang kann nicht rückgängig gemacht werden.", + encryptPanicButton: "Blockchain verschlüsseln", + decryptPanicButton: "Blockchain entschlüsseln", removePanicButton: "ALLE DEINE DATEN LÖSCHEN!", searchTitle: "Suche", searchDescriptionLabel: "Suche nach Inhalten in deinem Netzwerk.", + searchLanguagesLabel: "Sprachen", + searchSkillsLabel: "Fähigkeiten", searchPlaceholder:"Nach Inhalten suchen...", + searchDateLabel:"Datum", searchLocationLabel:"Ort", searchPriceLabel:"Preis", + searchUrlLabel: "URL", searchCategoryLabel:"Kategorie", + searchStartLabel: "Startzeit", + searchEndLabel: "Endzeit", searchPriorityLabel:"Priorität", searchStatusLabel: "Status", statusLabel: "Status", + totalVotesLabel: "Stimmen gesamt", noResultsFound: "Keine Ergebnisse gefunden.", votesOption: "Abstimmungsoptionen", voteYesLabel:"Ja", voteNoLabel:"Nein", allTypesLabel: "UNBEGRENZT", + ABSTENTIONLabel: "ENTHALTUNG", YESLabel: "JA", NOLabel: "NEIN", + FOLLOW_MAJORITYLabel: "MEHRHEIT FOLGEN", + CONFUSEDLabel: "VERWIRRT", + NOT_INTERESTEDLabel: "NICHT INTERESSIERT", + StatusLabel: "Status", votesOptionYesLabel:"Ja", votesOptionNoLabel:"Nein", votesOptionAbstentionLabel: "Enthaltung", @@ -598,6 +638,7 @@ module.exports = { votesCreatedByLabel: "Erstellt von", voteStatusOpen: "OFFEN", voteStatusClosed: "GESCHLOSSEN", + imageSearchLabel: "Gib Wörter ein, um nach Bildern zu suchen.", commentDescription: ({ parentUrl }) => [ " hat kommentiert in ", a({ href: parentUrl }, " Thread"), @@ -608,20 +649,53 @@ module.exports = { a({ href: parentUrl }, " einem Beitrag"), ], subtopicTitle: ({ authorName }) => [`Unterthema zu @${authorName}s Beitrag`], + mysteryDescription: "hat einen geheimnisvollen Beitrag veröffentlicht", oasisDescription: "OASIS Projekt-Netzwerk", searchSubmit: "Suchen...", + postLabel: "BEITRÄGE", + aboutLabel: "BEWOHNER", + feedLabel: "FEEDS", votesLabel: "ABSTIMMUNGEN", + reportLabel: "BERICHTE", + imageLabel: "BILDER", + videoLabel: "VIDEOS", + audioLabel: "AUDIOS", + documentLabel: "DOKUMENTE", + torrentLabel: "TORRENTS", pdfFallbackLabel: "PDF-Dokument", + eventLabel: "VERANSTALTUNGEN", + taskLabel: "AUFGABEN", + transferLabel: "ÜBERWEISUNGEN", + curriculumLabel: "LEBENSLAUF", + bookmarkLabel: "LESEZEICHEN", + tribeLabel: "STÄMME", + marketLabel: "MARKT", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "GELDBÖRSEN", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "Absenden", + subjectLabel: "Betreff", editProfile: "Avatar bearbeiten", + profileQrAlt: "Profil-QR-Code", + profileQrHint: "Scannen, um diese Oasis-ID zu kopieren.", oasisIdLabel: "OasisID", - spreadContent: "Replizieren", + spreadLabel: "Verbreiten", + spreadHint: "Verbreite dies an deine Unterstützer (über deinen Feed).", welcomePmSubject: "Hello World!", - welcomePmBody: "Willkommen bei Oasis — ein freies, peer-to-peer, föderiertes und verschlüsseltes soziales Netzwerk mit einer verteilten Architektur nur mit Einladung.\n\nEinige interessante Orte zum Anfangen:\n\n- /profile — Bearbeite deinen Avatar, Namen, deine Beschreibung und welche Module auf deiner öffentlichen Seite erscheinen.\n- /invites — Füge einen Pub hinzu, um dich mit dem weiteren Netzwerk zu föderieren.\n- /peers — Sieh, wer verbunden ist, scanne dein LAN erneut, exportiere oder importiere Peer-Listen.\n- /inhabitants — Entdecke und besuche die Avatare anderer Personen.\n- /tribes — Erstelle private Gruppen mit Ende-zu-Ende-Verschlüsselung oder tritt ihnen bei.\n- /inbox — Lies und sende private Nachrichten.\n- /activity — Sieh die jüngste Aktivität, deine und die deines Netzwerks.\n- /publish — Schreibe einen Beitrag oder teile ein Bild, Audio, Video oder Dokument.\n- /search — Durchsuche die Inhalte des Netzwerks nach Typ.\n\nEinige interessante Orte zum Verbinden:\n\n- /Multiversum — Verwalte deine anderen Multiversum-Konten, einschließlich Senden und Empfangen von Inhalten.\n\nDiese Nachricht wurde privat von dir an dich selbst gesendet, daher war keine Verbindung nötig, um sie zu empfangen.", + welcomePmBody: "Willkommen bei Oasis — ein freies, peer-to-peer, föderiertes und verschlüsseltes soziales Netzwerk mit einer verteilten Architektur nur mit Einladung.\n\nEinige interessante Orte zum Anfangen:\n\n- /profile — Bearbeite deinen Avatar, Namen, deine Beschreibung und welche Module auf deiner öffentlichen Seite erscheinen.\n- /invites — Füge einen Pub hinzu, um dich mit dem weiteren Netzwerk zu föderieren.\n- /peers — Sieh, wer verbunden ist, scanne dein LAN erneut, exportiere oder importiere Peer-Listen.\n- /inhabitants — Entdecke und besuche die Avatare anderer Personen.\n- /tribes — Erstelle private Gruppen mit Ende-zu-Ende-Verschlüsselung oder tritt ihnen bei.\n- /inbox — Lies und sende private Nachrichten.\n- /activity — Sieh die jüngste Aktivität, deine und die deines Netzwerks.\n- /publish — Schreibe einen Beitrag oder teile ein Bild, Audio, Video oder Dokument.\n\nEinige interessante Orte zum Verbinden:\n\n- /fediverse — Verwalte deine anderen Fediverse-Konten, einschließlich Senden und Empfangen von Inhalten.\n\nDiese Nachricht wurde privat von dir an dich selbst gesendet, daher war keine Verbindung nötig, um sie zu empfangen.", + profileVisibilityTitle: "Sichtbarkeit des öffentlichen Profils", + profileVisibilityHint: "Wähle, welche Felder andere Bewohner in deinem Profil sehen. Standardmäßig wird nur Karma angezeigt.", profileSensorsSectionTitle: "Sensoren", profileVisibilityActivity: "Aktivitätsstufe", + profileVisibilityDevice: "Gerät", profileDeviceLockedHint: "Dieser Sensor ist immer aktiv: Er zeigt anderen, von welchem Gerät du dich verbindest, und kann nicht deaktiviert werden.", profileVisibilityKarma: "KARMA-Punkte", profileVisibilityUbi: "UBI", @@ -629,6 +703,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "GPG-Schlüssel", + profileVisibilityClearnet: "Mein Profil veröffentlichen", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Shops", profileClearnetJobsLabel: "Stellen", @@ -682,6 +757,7 @@ module.exports = { " oder den Verbindungsstatus.", ], walletSentToLine: ({ destination, amount }) => `ECO ${amount} gesendet an ${destination}`, + walletSettingsTitle: "Geldbörse", walletSettingsDescription: "Integriere Oasis mit deiner ECOin-Wallet.", walletSettingsDocLink: "ECOin-Installationsanleitung", walletStatusMessages: { @@ -703,18 +779,36 @@ module.exports = { cipherTitle: "Verschlüsselung", cipherDescription: "Verschlüssele und entschlüssele Text symmetrisch.", randomPassword: "Zufälliges Passwort", + cipherEncryptTitle: "Text verschlüsseln", + cipherEncryptDescription: "Text zum Verschlüsseln eingeben", + cipherTextLabel: "Zu verschlüsselnder Text", cipherTextPlaceholder: "Text eingeben...", cipherPasswordLabel: "Passwort (mind. 32 Zeichen)", cipherPasswordPlaceholder: "Passwort eingeben...", cipherEncryptButton: "Verschlüsseln", + cipherDecryptTitle: "Text entschlüsseln", + cipherDecryptDescription: "Text zum Entschlüsseln eingeben", cipherEncryptedMessageLabel: "Verschlüsselter Text", cipherDecryptedMessageLabel: "Entschlüsselter Text", + cipherPasswordUsedLabel: "Verwendetes Passwort (aufbewahren!)", cipherEncryptedTextPlaceholder: "Verschlüsselten Text eingeben...", + cipherIvLabel: "IV", + cipherIvPlaceholder: "Initialisierungsvektor eingeben...", cipherDecryptButton: "Entschlüsseln", password: "Passwort", text: "Text", encryptedText: "Verschlüsselter Text", iv: "Initialisierungsvektor (IV)", + encryptTitle: "Text verschlüsseln", + encryptDescription: "Text und Passwort eingeben.", + encryptButton: "Verschlüsseln", + decryptTitle: "Text entschlüsseln", + decryptDescription: "Verschlüsselten Text und Passwort eingeben.", + decryptButton: "Entschlüsseln", + passwordLengthError: "Passwort muss mind. 32 Zeichen lang sein.", + missingFieldsError: "Text, Passwort oder IV fehlt.", + encryptionError: "Fehler beim Verschlüsseln.", + decryptionError: "Fehler beim Entschlüsseln.", bookmarkTitle: "Lesezeichen", bookmarkDescription: "Lesezeichen in deinem Netzwerk entdecken und verwalten.", bookmarkAllSectionTitle: "Lesezeichen", @@ -740,6 +834,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "Optional", bookmarkTagsLabel: "Tags", bookmarkTagsPlaceholder: "Tags durch Kommas getrennt eingeben", + bookmarkCategoryLabel: "Kategorie", + bookmarkCategoryPlaceholder: "Optional", bookmarkLastVisitLabel: "Letzter Besuch", bookmarkSearchPlaceholder: "URL, Tags, Kategorie, Autor suchen...", bookmarkSortRecent: "Neueste zuerst", @@ -754,6 +850,8 @@ module.exports = { noLastVisit: "Kein letzter Besuch", videoTitle: "Videos", videoDescription: "Videoinhalte in deinem Netzwerk entdecken und verwalten.", + videoPluginTitle: "Titel", + videoPluginDescription: "Beschreibung", videoMineSectionTitle: "Deine Videos", videoCreateSectionTitle: "Video hochladen", videoUpdateSectionTitle: "Video aktualisieren", @@ -785,6 +883,7 @@ module.exports = { videoSortOldest: "Älteste zuerst", videoSortTop: "Meistgewählt", videoSearchButton: "Suche", + videoMessageAuthorButton: "PN", videoUpdatedAt: "Aktualisiert", videoNoMatch: "Keine Videos gefunden.", documentTitle: "Dokumente", @@ -825,6 +924,8 @@ module.exports = { documentUpdatedAt: "Aktualisiert", audioTitle: "Audios", audioDescription: "Audioinhalte in deinem Netzwerk entdecken und verwalten.", + audioPluginTitle: "Titel", + audioPluginDescription: "Beschreibung", audioMineSectionTitle: "Deine Audios", audioCreateSectionTitle: "Audio hochladen", audioUpdateSectionTitle: "Audio aktualisieren", @@ -835,6 +936,7 @@ module.exports = { audioFilterMine: "MEINE", audioFilterRecent: "NEUESTE", audioFilterTop: "TOP", + audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Audio hochladen", audioUpdateButton: "Aktualisieren", audioDeleteButton: "Löschen", @@ -861,7 +963,6 @@ module.exports = { audioFilterFavorites: "FAVORITEN", favoritesTitle: "Favoriten", favoritesDescription: "Alle deine favorisierten Inhalte an einem Ort.", - favoritesSearchPlaceholder: "Favoriten durchsuchen...", favoritesFilterAll: "ALLE", favoritesFilterRecent: "NEUESTE", favoritesFilterAudios: "AUDIOS", @@ -877,13 +978,18 @@ module.exports = { favoritesNoItems: "Noch keine Favoriten.", yourContacts: "Deine Kontakte", allInhabitants: "Bewohner", - allCVs: "Lebensläufe", + allCVs: "Alle Lebensläufe", discoverPeople: "Entdecke Bewohner in deinem Netzwerk.", + allInhabitantsButton: "ALLE", + contactsButton: "UNTERSTÜTZT", + CVsButton: "CVs", + matchSkills: "Fähigkeiten abgleichen", + matchSkillsButton: "FÄHIGKEITEN ABGLEICHEN", + suggestedButton: "VORGESCHLAGEN", searchInhabitantsPlaceholder: "BEWOHNER NACH NAME FILTERN …", filterLocation: "BEWOHNER NACH ORT FILTERN …", filterLanguage: "BEWOHNER NACH SPRACHE FILTERN …", filterSkills: "BEWOHNER NACH FÄHIGKEITEN FILTERN …", - cvSkillsCount: "Fähigkeiten", applyFilters: "Filter anwenden", locationLabel: "Ort", languagesLabel: "Sprachen", @@ -892,18 +998,17 @@ module.exports = { mutualFollowers: "Gemeinsame Follower", suggestedFollowsYou: "Folgt dir", latestInteractions: "Letzte Interaktionen", + viewAvatar: "Avatar anzeigen", + viewCV: "Lebenslauf anzeigen", suggestedSectionTitle: "Vorgeschlagen", topkarmaSectionTitle: "Top Karma", topecoSectionTitle: "Top Öko", topactivitySectionTitle: "Top Aktivität", - "TOP INACTIVITYButton": "AM INAKTIVSTEN", - topinactivitySectionTitle: "Am wenigsten aktive Bewohner", blockedSectionTitle: "Blockiert", gallerySectionTitle: "GALERIE", - topSectionTitle: "TOP", + blockedButton: "BLOCKIERT", blockedLabel: "Blockierter Bewohner", inhabitantviewDetails: "Details anzeigen", - cvVisitButton: "Lebenslauf ansehen", keepReading: "Weiterlesen...", oasisId: "ID", noInhabitantsFound: "Noch keine Bewohner gefunden.", @@ -916,9 +1021,8 @@ module.exports = { parliamentFilterCandidatures: "KANDIDATUREN", parliamentFilterProposals: "VORSCHLÄGE", parliamentFilterLaws: "GESETZE", - parliamentFilterRevocations: "ABBERUFUNGEN", - parliamentFilterHistorical: "VERLAUF", - parliamentFilterLeaders: "FÜHRUNG", + parliamentFilterHistorical: "HISTORISCH", + parliamentFilterLeaders: "ANFÜHRER", parliamentFilterRules: "REGELN", parliamentGovernmentCard: "Aktuelle Regierung", parliamentActorInPowerInhabitant: 'REGIERENDER BEWOHNER', @@ -942,8 +1046,10 @@ module.exports = { parliamentPoliciesDeclined: "GESETZE ABGELEHNT", parliamentPoliciesDiscarded: "GESETZE VERWORFEN", parliamentEfficiency: "% EFFIZIENZ", + parliamentFilterRevocations: 'WIDERRUFE', parliamentRevocationFormTitle: 'Gesetz widerrufen', parliamentRevocationLaw: 'Gesetz', + parliamentRevocationTitle: 'Titel', parliamentRevocationReasons: 'Begründung', parliamentRevocationPublish: 'Widerruf veröffentlichen', parliamentCurrentRevocationsTitle: 'Aktuelle Widerrufe', @@ -956,12 +1062,23 @@ module.exports = { parliamentNoGovernments: "Noch keine Regierungen vorhanden.", parliamentCandidatureFormTitle: "Kandidatur vorschlagen", parliamentCandidatureIdPh: "Oasis-ID (@...) oder Stammname", + parliamentCandidatureSlogan: "Slogan (max. 140 Zeichen)", + parliamentCandidatureSloganPh: "Ein kurzes Motto", parliamentCandidatureMethod: "Methode", parliamentCandidatureProposeBtn: "Kandidatur veröffentlichen", + parliamentThDate: "Vorschlagsdatum", + parliamentThSlogan: "Slogan", + parliamentThSince: "Profil seit", + parliamentThVotes: "Erhaltene Stimmen", + parliamentThVoteAction: "Abstimmen", + parliamentTypeUser: "Bewohner", + parliamentTypeTribe: "Stamm", parliamentVoteBtn: "Abstimmen", parliamentProposalFormTitle: "Gesetz vorschlagen", parliamentProposalDescription: "Beschreibung (≤1000)", parliamentProposalPublish: "Vorschlag veröffentlichen", + parliamentFinalize: "Abschließen", + parliamentDeadline: "Frist", parliamentNoProposals: "Noch keine Gesetzesvorschläge vorhanden.", parliamentNoLaws: "Noch keine Gesetze genehmigt.", parliamentMethodDEMOCRACY: "Demokratie", @@ -979,21 +1096,29 @@ module.exports = { parliamentVotesSlashTotal: "Stimmen/Gesamt", parliamentVotesNeeded: "Benötigte Stimmen", parliamentFutureLawsTitle: "Zukünftige Gesetze", + parliamentNoFutureLaws: "Noch keine zukünftigen Gesetze.", parliamentVoteAction: "Abstimmen", + parliamentLeadersTitle: "Anführer", parliamentThLeader: "AVATAR", parliamentPopulation: "Bevölkerung", + parliamentThType: "Typ", + parliamentThInPower: "An der Macht", parliamentThPresented: "KANDIDATUREN", parliamentNoLeaders: "Noch keine Anführer.", parliamentCandidaturesListTitle: "Liste der Kandidaturen", parliamentTimeRemaining: "Verbleibende Zeit", + parliamentNoLeader: "Noch keine führende Kandidatur.", parliamentElectionsStatusTitle: "Nächste Regierung", parliamentProposalDeadlineLabel: "Frist", parliamentProposalTimeLeft: "Verbleibende Zeit", + parliamentProposalOnTrack: "Auf Kurs zur Annahme", + parliamentProposalOffTrack: "Noch nicht genügend Unterstützung", parliamentRulesTitle: "Wie das Parlament funktioniert", parliamentRulesIntro: "Wahlen werden alle 2 Monate aufgelöst; Kandidaturen laufen durchgehend und werden zurückgesetzt, wenn eine Regierung gewählt wird.", parliamentRulesCandidates: "Jeder Bewohner kann sich selbst, einen anderen Bewohner oder einen Stamm vorschlagen. Jeder Bewohner kann bis zu 3 Kandidaturen pro Zyklus einreichen; Duplikate im selben Zyklus werden abgelehnt.", parliamentRulesElection: "Der Gewinner ist die Kandidatur mit den meisten Stimmen zum Zeitpunkt der Auflösung.", parliamentRulesTies: "Stichwahl-Reihenfolge: höchstes Bewohner-Karma; bei Gleichstand ältestes Profil; bei weiterem Gleichstand frühester Vorschlag; dann lexikographisch nach ID.", + parliamentRulesFallback: "Wenn niemand abstimmt, gewinnt die zuletzt vorgeschlagene Kandidatur. Wenn keine Kandidaturen vorliegen, wird ein zufälliger Stamm ausgewählt.", parliamentRulesTerm: "Der Regierungs-Tab zeigt die aktuelle Regierung und Statistiken.", parliamentRulesMethods: "Formen: Anarchie (einfache Mehrheit), Demokratie (50%+1), Mehrheit (80%), Minderheit (20%), Karmatokratie (Vorschläge mit höchstem Karma), Diktatur (sofortige Genehmigung).", parliamentRulesAnarchy: "Anarchie ist der Standardmodus: Wenn bei der Auflösung keine Kandidatur gewählt wird, wird Anarchie ausgerufen. Unter Anarchie kann jeder Bewohner Gesetze vorschlagen.", @@ -1015,7 +1140,11 @@ module.exports = { courtsCaseFormTitle: "Fall eröffnen", courtsCaseRespondent: "Angeklagter / Beklagter", courtsCaseRespondentPh: "Oasis-ID (@...) oder Stammname", + courtsCaseMediatorsAccuser: "Mediatoren (Ankläger)", courtsCaseMethod: "Lösungsmethode", + courtsCaseDescription: "Beschreibung (max. 1000 Zeichen)", + courtsCaseEvidenceTitle: "Fallbeweise", + courtsCaseEvidenceHelp: "Bilder, Audios, Dokumente (PDF) oder Videos anhängen, die deinen Fall unterstützen.", courtsCaseSubmit: "Fall einreichen", courtsNominateJudge: "Richter nominieren", courtsJudgeId: "Richter", @@ -1060,6 +1189,22 @@ module.exports = { courtsRulesMisconduct: "Belästigung, Manipulation oder gefälschte Beweise können zu einer sofortigen negativen Lösung führen.", courtsRulesGlossary: "Fall: Aufzeichnung eines Konflikts. Beweis: Materialien, die Ansprüche unterstützen. Urteil: Entscheidung mit Maßnahme. Berufung: Antrag auf Überprüfung des Urteils.", courtsFilterActions: "AKTIONEN", + courtsNoActions: "Keine ausstehenden Aktionen für deine Rolle.", + courtsCaseTitlePlaceholder: "Kurze Beschreibung des Konflikts", + courtsCaseSeverity: "Schweregrad", + courtsCaseSeverityNone: "Kein Schweregrad", + courtsCaseSeverityLOW: "Niedrig", + courtsCaseSeverityMEDIUM: "Mittel", + courtsCaseSeverityHIGH: "Hoch", + courtsCaseSeverityCRITICAL: "Kritisch", + courtsCaseSubject: "Thema", + courtsCaseSubjectNone: "Kein Thema-Tag", + courtsCaseSubjectBEHAVIOUR: "Verhalten", + courtsCaseSubjectCONTENT: "Inhalt", + courtsCaseSubjectGOVERNANCE: "Regierung / Regeln", + courtsCaseSubjectFINANCIAL: "Finanzen / Ressourcen", + courtsCaseSubjectOTHER: "Sonstiges", + courtsHiddenRespondent: "Verborgen (nur für beteiligte Rollen sichtbar).", courtsThRole: "Rolle", courtsRoleAccuser: "Ankläger", courtsRoleDefence: "Verteidigung", @@ -1070,19 +1215,51 @@ module.exports = { courtsAssignJudgeBtn: "Richter auswählen", trendingTitle: "Trends", exploreTrending: "Entdecke die beliebtesten Inhalte in deinem Netzwerk.", + bookmarkButton: "LESEZEICHEN", + transferButton: "ÜBERWEISUNGEN", + eventButton: "TERMINE", + taskButton: "AUFGABEN", votesButton: "ABSTIMMUNGEN", - shopProductButton: "SHOP", + reportButton: "BERICHTE", + feedButton: "FEED", + marketButton: "MARKT", + imageButton: "BILDER", + audioButton: "AUDIOS", + videoButton: "VIDEOS", + documentButton: "DOKUMENTE", + torrentButton: "TORRENTS", + noTrendingFound: "Keine Trendinhalte gefunden.", + noContentMessage: "Noch keine Trendinhalte vorhanden.", + trendingDescription: "Beschreibung", + trendingDate: "Datum", + trendingLocation: "Ort", + trendingPrice: "Preis", + trendingUrl: "URL", + trendingCategory: "Kategorie", + trendingStart: "Start", + trendingEnd: "Ende", + trendingPriority: "Priorität", + trendingStatus: "Status", + trendingFrom: "Von", + trendingTo: "An", + trendingConcept: "Konzept", + trendingAmount: "Betrag", + trendingDeadline: "Frist", + trendingItemStatus: "Status", + trendingTotalVotes: "Stimmen gesamt", trendingTotalOpinions: "Meinungen gesamt", - trendingNoContentMessage: "Noch keine Trends vorhanden.", + trendingNoContentMessage: "Noch keine Trendinhalte vorhanden.", + trendingAuthor: "Von", + trendingCreatedAtLabel: "Erstellt am", trendingTotalCount: "Gesamtzahl", tasksTitle: "Aufgaben", tasksDescription: "Aufgaben in deinem Netzwerk entdecken und verwalten.", - taskSearchPlaceholder: "Aufgaben suchen...", taskTitleLabel: "Titel", taskDescriptionLabel: "Beschreibung", taskStartTimeLabel: "Startzeit", taskEndTimeLabel: "Endzeit", taskPriorityLabel: "Priorität", + taskPrioritySelect: "Priorität auswählen", taskPriorityUrgent: "Dringend", taskPriorityHigh: "Hoch", taskPriorityMedium: "Mittel", @@ -1092,13 +1269,13 @@ module.exports = { taskVisibilityLabel: "Sichtbarkeit", taskPublic: "öffentlich", taskPrivate: "privat", + taskCreatedAt: "Erstellt am", + taskBy: "Von", taskStatus: "Status", taskStatusOpen: "Öffnen", taskStatusInProgress: "In Bearbeitung", taskStatusClosed: "Geschlossen", taskAssignedTo: "Zugewiesen an", - taskAssignedChip: "Zugewiesen", - taskUnassignedChip: "Nicht zugewiesen", taskAssignees: "Zugewiesene", taskAssignButton: "Mir zuweisen", taskUnassignButton: "Zuweisung aufheben", @@ -1111,6 +1288,7 @@ module.exports = { taskFilterInProgress: "IN BEARBEITUNG", taskFilterClosed: "GESCHLOSSEN", taskFilterAssigned: "ZUGEWIESEN", + taskFilterArchived: "ARCHIVIERT", taskFilterUrgent: "DRINGEND", taskFilterHigh: "HOCH", taskFilterMedium: "MITTEL", @@ -1123,21 +1301,23 @@ module.exports = { taskInProgressTitle: "Aufgaben in Bearbeitung", taskClosedTitle: "Geschlossene Aufgaben", taskAssignedTitle: "Zugewiesene Aufgaben", + taskArchivedTitle: "Archivierte Aufgaben", + taskPublicTitle: "Öffentliche Aufgaben", + taskPrivateTitle: "Private Aufgaben", notasks: "Keine Aufgaben vorhanden.", noLocation: "Kein Ort angegeben", taskSetStatus: "Status setzen", + eventTitle: "Termine", eventDateLabel: "Datum", - eventRecurrenceLabel: "Wiederholung", - eventRecurrenceUntil: "Wiederholen bis", - eventRecurrenceHint: "Lass das Enddatum leer für ein einmaliges Ereignis.", - eventUpcomingDates: "Nächste Termine", eventsTitle: "Termine", eventsDescription: "Termine in deinem Netzwerk entdecken und verwalten.", - eventSearchPlaceholder: "Veranstaltungen suchen...", + eventDescription: "Beschreibung", eventPrice: "Preis", eventStatus: "Status", + eventOrganizer: "Veranstalter", eventAllSectionTitle: "Termine", eventMineSectionTitle: "Deine Termine", + eventArchivedTitle: "Archivierte Termine", eventCreateSectionTitle: "Termin erstellen", eventUpdateSectionTitle: "Termin aktualisieren", eventDeleteButton: "Löschen", @@ -1145,26 +1325,11 @@ module.exports = { eventAddToCalendar: "Zum Kalender hinzufügen", eventVisitCalendar: "Kalender ansehen", viewTask: "Aufgabe ansehen", - viewEvent: "Event ansehen", - viewPad: "Pad ansehen", - viewMap: "Karte ansehen", - viewCalendar: "Kalender ansehen", viewReport: "Bericht ansehen", viewTransfer: "Transfer ansehen", viewItem: "Artikel ansehen", viewShop: "Shop ansehen", viewProject: "Projekt ansehen", - viewJob: "Job Ansehen", - viewPlace: "Ort Ansehen", - generatePdf: "PDF Erzeugen", - sharePm: "Per PN Teilen", - galleryImages: "Bilder (max. 50 MB pro Datei)", - galleryPhotos: "Bilder", - galleryAddPhoto: "Bild Hinzufügen", - galleryRemovePhoto: "Entfernen", - galleryVideo: "Video (max. 50 MB)", - galleryAddVideo: "Video Hinzufügen", - galleryRemoveVideo: "Video entfernen", viewVotation: "Abstimmung ansehen", voteCastTitle: "Stimme abgeben", voteResults: "Ergebnisse", @@ -1172,15 +1337,29 @@ module.exports = { eventDescriptionLabel: "Beschreibung", eventDescriptionPlaceholder: "Terminbeschreibung eingeben...", eventUpdateButton: "Aktualisieren", + eventAttendeesLabel: "Teilnehmer", + eventAttendeesPlaceholder: "Teilnehmer eingeben, durch Kommas getrennt...", eventTagsLabel: "Tags", + eventTagsPlaceholder: "Termin-Tags eingeben, durch Kommas getrennt...", + eventTags: "Tags", eventPriceLabel: "Preis", eventUrlLabel: "URL", eventAttendees: "Teilnehmer", + noAttendees: "Noch keine Teilnehmer", + eventCreatedAt: "Erstellt am", eventLocation: "Ort", eventLocationLabel: "Ort", + eventNoLocation: "Kein Ort angegeben", + eventNoURL: "Keine URL angegeben", + eventBy: "Von", noevents: "Keine Termine vorhanden.", eventDate: "Datum", + eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "Teilnehmen", + eventUnattendButton: "Nicht mehr teilnehmen", + eventCreatedBy: "Erstellt von", + eventAttendeesCount: "Teilnehmerzahl", + eventCreatedByYou: "Du hast diesen Termin erstellt", eventFilterAll: "ALLE", eventFilterMine: "MEINE", eventFilterToday: "HEUTE", @@ -1188,9 +1367,18 @@ module.exports = { eventFilterMonth: "DIESER MONAT", eventFilterYear: "DIESES JAHR", eventFilterArchived: "ARCHIVIERT", + eventTodayTitle: "Heutige Termine", + eventThisWeekTitle: "Termine dieser Woche", + eventThisMonthTitle: "Termine dieses Monats", + eventThisYearTitle: "Termine dieses Jahres", eventPrivacyLabel: "Sichtbarkeit", eventPublic: "Öffentlich", eventPrivate: "Privat", + eventPublicTitle: "Öffentliche Termine", + eventNoPrice: "Kostenloser Termin", + eventNoImage: "Kein Bild hochgeladen", + eventAttendConfirmation: "Du nimmst jetzt an diesem Termin teil", + eventUnattendConfirmation: "Du nimmst nicht mehr an diesem Termin teil", eventAttended: "Teilgenommen", eventUnattended: "Nicht teilgenommen", eventStatusOpen: "Öffnen", @@ -1201,14 +1389,9 @@ module.exports = { tagsTopSectionTitle: "Top-Tags", tagsCloudSectionTitle: "Tag-Wolke", tagsFilterAll: "ALLE", - tagsFilterMine: "MEINE", - tagsFilterRecent: "NEU", - tagsMineSectionTitle: "Meine Tags", - tagsRecentSectionTitle: "Neue Tags", tagsFilterTop: "TOP", tagsFilterCloud: "WOLKE", tagsNoItems: "Keine Tags vorhanden.", - tagsSearchPlaceholder: "Tags suchen...", tagsTableHeaderTag: "TAG/LINK:", tagsTableHeaderCount: "ANZAHL:", transfersTitle: "Überweisungen", @@ -1248,12 +1431,18 @@ module.exports = { transfersDeadline: "Frist", transfersTags: "Tags", transfersStatus: "Status", + transfersStatusUnconfirmed: "UNBESTÄTIGT", + transfersStatusClosed: "GESCHLOSSEN", + transfersStatusDiscarded: "VERWORFEN", + transfersCreatedAt: "Erstellt am", transfersConfirmations: "BESTÄTIGUNGEN", - transfersExportContract: "Rechnung Erzeugen", + transfersContainingBlock: "Enthaltender Block", + transfersExportContract: "Vertrag erstellen", transfersConfirmButton: "Überweisung bestätigen", transfersNoItems: "Keine Überweisungen gefunden.", transfersMineSectionTitle: "Deine Überweisungen", transfersUBISectionTitle: "UBI Überweisungen", + transfersMarketSectionTitle: "Markt-Überweisungen", transfersTopSectionTitle: "Top-Überweisungen", transfersPendingSectionTitle: "Ausstehende Überweisungen", transfersUnconfirmedSectionTitle: "Unbestätigte Überweisungen", @@ -1261,13 +1450,21 @@ module.exports = { transfersDiscardedSectionTitle: "Verworfene Überweisungen", transfersCreateSectionTitle: "Transfer erstellen", transfersAllSectionTitle: "Überweisungen", + transfersFilterFavs: "Favoriten", + transfersFavsSectionTitle: "Favorisierte Überweisungen", + transfersSearchLabel: "Suche", transfersSearchPlaceholder: "Konzept, Tags, Bewohner suchen...", transfersMinAmountLabel: "Mindestbetrag", transfersMaxAmountLabel: "Höchstbetrag", + transfersSortLabel: "Sortieren nach", transfersSortRecent: "Neueste zuerst", transfersSortAmount: "Höchster Betrag", transfersSortDeadline: "Nächste Frist", transfersSearchButton: "Suche", + transfersFavoriteButton: "Favorit", + transfersUnfavoriteButton: "Kein Favorit", + transfersMessageUserButton: "Nachricht", + transfersExpiringSoonBadge: "LÄUFT AB", transfersExpiredBadge: "ABGELAUFEN", transfersUpdatedAt: "Aktualisiert", transfersNoMatch: "Keine Überweisungen gefunden.", @@ -1280,9 +1477,6 @@ module.exports = { voteClosedTitle: "Geschlossene Abstimmungen", voteAllSectionTitle: "Abstimmungen", voteCreateButton: "Abstimmung erstellen", - voteErrorTitle: "Formular prüfen", - voteErrorDeadlineMin: "Die Frist muss mindestens {days} Tage in der Zukunft liegen.", - voteErrorGeneric: "Die Abstimmung konnte nicht gespeichert werden.", voteUpdateButton: "Aktualisieren", voteDeleteButton: "Löschen", voteOptionYes: "JA", @@ -1312,6 +1506,16 @@ module.exports = { voteNoQuestion: "Keine Frage angegeben", voteUnknownCreator: "Unbekannter Ersteller", voteUnknownDate: "Unbekanntes Datum", + errorVoteNotFound: "Abstimmung nicht gefunden", + errorAlreadyVoted: "Du hast bereits abgestimmt.", + errorVoteClosedCannotEdit: "Eine geschlossene Abstimmung kann nicht bearbeitet werden", + errorVoteDeadlinePassed: "Die Frist für diese Abstimmung ist abgelaufen", + errorRetrievingVote: "Fehler beim Abrufen der Abstimmung", + errorCreatingVote: "Fehler beim Erstellen der Abstimmung", + errorVoteAlreadyVoted: "Nach Abgabe einer Meinung nicht mehr bearbeitbar", + errorDeletingOldVote: "Fehler beim Löschen der alten Meinung", + errorCreatingUpdatedVote: "Fehler beim Erstellen der aktualisierten Meinung", + errorCreatingTombstone: "Fehler beim Erstellen des Tombstone", voteDetailSectionTitle: 'Abstimmungsdetails', voteCommentsLabel: 'Kommentare', voteCommentsForumButton: 'Diskussion eröffnen', @@ -1321,6 +1525,7 @@ module.exports = { voteNewCommentButton: 'Kommentar veröffentlichen', voteNewCommentLabel: 'Einen Kommentar hinzufügen', cvTitle: "Lebenslauf", + cvLabel: "LEBENSLÄUFE", cvEditSectionTitle: "Lebenslauf bearbeiten", cvCreateSectionTitle: "Lebenslauf erstellen", cvDescription: "Verwalte und teile deine beruflichen Fähigkeiten und Informationen.", @@ -1339,121 +1544,29 @@ module.exports = { cvLocationLabel: "Ort", cvStatusLabel: "Status", cvPreferencesLabel: "Präferenzen", + cvOasisContributorLabel: "Oasis-Mitwirkender", cvPersonal: "Persönlich", cvOasis: "Oasis-Mitwirkender (optional)", - cvOasisContributorView: "Beitrag", + cvOasisContributorView: "Oasis-Beitrag", cvEducational: "Bildung (optional)", cvEducationalView: "Bildung", cvProfessional: "Beruf (optional)", cvProfessionalView: "Beruf", cvAvailability: "Verfügbarkeit (optional)", + cvAvailabilityView: "Verfügbarkeit", cvUpdateButton: "Aktualisieren", cvCreateButton: "Lebenslauf erstellen", + cvContactLabel: "Kontakt", cvCreatedAt: "Erstellt am", cvUpdatedAt: "Aktualisiert am", cvEditButton: "Aktualisieren", cvDeleteButton: "Löschen", cvNoCV: "Kein Lebenslauf gefunden.", blogSubject: "Betreff", - blogMessage: "Nachricht", + blogMessage: "Text auf 8096 Zeichen begrenzt.", blogImage: "Medien hochladen (max: 50MB)", - blogMedia: "Anhänge (bis zu 8 Bilder und ein Video)", blogPublish: "Vorschau", - pollsTitle: "Umfragen", - dataTitle: "Übereinstimmungen", - dataDescription: "Erkunde und visualisiere die Übereinstimmungen in deinem Netzwerk.", - dataFilterAll: "ALLE", - dataFilterMine: "MEINE", - dataFilterRecent: "NEUESTE", - dataFilterTop: "TOP", - dataKindInhabitants: "BEWOHNER", - dataKindJobs: "JOBS", - dataKindProjects: "PROJEKTE", - dataKindEvents: "EREIGNISSE", - dataKindTribes: "STÄMME", - dataKindMarket: "MARKT", - dataKindHousing: "WOHNEN", - dataKindIndustry: "INDUSTRIE", - dataKindTasks: "AUFGABEN", - dataKindReports: "BERICHTE", - dataKindVotes: "ABSTIMMUNGEN", - dataSearchPlaceholder: "Querdaten durchsuchen...", - dataCohesionTitle: "Kohäsionskoeffizient (CC)", - dataCcShort: "CC", - dataCohesionHint: "Durchschnittliche Affinität zwischen allem, was das Netzwerk veröffentlicht hat.", - dataAffinity: "Affinität", - dataCommonTerms: "Schlüsselwort", - dataStatPeople: "Lebensläufe", - dataStatConnected: "Verbunden", - dataStatSkills: "Fähigkeiten", - dataBestMatch: "Bestes Match", - dataStatIsolated: "Isoliert", - dataStatEntities: "Verglichene Einheiten", - dataStatTerms: "Verschiedene Begriffe", - dataStatPairs: "Verbundene Paare", - dataMatchesTitle: "Übereinstimmungen", - dataMatchesHint: "Personalisierte Vorschläge des Algorithmus.", - dataTermsTitle: "Schlagwörter", - dataTermsHint: "Die Begriffe, die in den meisten Inhalten vorkommen.", - dataConnections: "Verknüpfte Treffer", - dataTopicTitle: "Alles zu", - dataTopicHint: "Was der Algorithmus mit diesem Thema verbindet", - dataNoMatches: "Keine Querdaten vorhanden.", - dataNoProfile: "Veröffentliche Inhalte oder schreib deinen Lebenslauf, damit der Algorithmus lernt, was dich interessiert.", - pollsDescription: "Modul, um das Netzwerk zu fragen und Antworten zu zählen.", - pollFilterAll: "ALLE", - pollFilterMine: "MEINE", - pollFilterRecent: "NEUESTE", - pollFilterTop: "TOP", - pollFilterVoted: "ABGESTIMMT", - pollFilterOpen: "OFFEN", - pollFilterClosed: "GESCHLOSSEN", - pollCreateButton: "Umfrage Erstellen", - pollCreateTitle: "Umfrage Erstellen", - pollEditTitle: "Umfrage Bearbeiten", - pollQuestion: "Frage", - pollQuestionPlaceholder: "Was möchtest du fragen?", - pollOptions: "Optionen (eine pro Zeile)", - pollOutcome: "Ergebnis", - pollNoVotesYet: "Noch keine Stimmen", - pollTie: "Gleichstand", - pollOptionsPlaceholder: "Platz\nPark\nBar", - pollAnonymous: "Anonym", - pollAnonymousLabel: "Anonyme Umfrage", - pollMultiple: "Mehrfachauswahl", - pollMultipleLabel: "Mehrere Antworten erlauben", - pollDeadline: "Frist", - pollTags: "Schlagwörter", - pollPublishButton: "Umfrage Veröffentlichen", - pollUpdateButton: "Aktualisieren", - pollCloseButton: "Schließen", - pollDeleteButton: "Löschen", - pollVoteButton: "Abstimmen", - pollChangeVote: "Stimme Ändern", - pollVoters: "Abstimmende", - pollComments: "Kommentare", - pollStatusLabel: "Status", - pollStatusOpen: "OFFEN", - pollStatusClosed: "GESCHLOSSEN", - pollSearchPlaceholder: "Umfragen suchen...", - pollsNoItems: "Keine Umfragen gefunden.", - viewPoll: "Umfrage Ansehen", - pollChatCreate: "Umfrage Erstellen", - pollInChat: "Umfrage", - blogTitle: "Blogs", - blogDescription: "Modul zum Entdecken und Verwalten von Blogs.", - blogFilterAll: "ALLE", - blogFilterMine: "MEINE", - blogFilterRecent: "NEUESTE", - blogFilterFavorites: "FAVORITEN", - blogFilterTop: "TOP", - blogCreateButton: "Blog Erstellen", - blogSearchPlaceholder: "Blogs suchen...", - blogComments: "Kommentare", - blogAllowComments: "Kommentare erlauben", - blogCommentsClosed: "Der Autor hat die Kommentare für diesen Blog geschlossen.", - blogNoItems: "Keine Blogs gefunden.", - viewBlog: "Blog Ansehen", + noPopularMessages: "Noch keine beliebten Nachrichten veröffentlicht", forumTitle: "Foren", forumCategoryLabel: "Kategorie", forumTitleLabel: "Titel", @@ -1461,22 +1574,43 @@ module.exports = { forumCreateButton: "Forum erstellen", forumCreateSectionTitle: "Forum erstellen", forumDescription: "Diskutiere offen mit anderen Bewohnern in deinem Netzwerk.", - forumSearchPlaceholder: "Forum durchsuchen...", forumFilterAll: "ALLE", forumFilterMine: "MEINE", forumFilterRecent: "NEUESTE", forumFilterTop: "TOP", + forumMineSectionTitle: "Deine Foren", + forumRecentSectionTitle: "Neueste Foren", + forumAllSectionTitle: "Foren", forumDeleteButton: "Löschen", forumParticipants: "Teilnehmer", forumMessages: "Nachrichten", + forumLastMessage: "Letzte Nachricht", forumMessageLabel: "Nachricht", forumMessagePlaceholder: "Schreibe deine Nachricht...", forumSendButton: "Senden", + forumVisitForum: "Forum besuchen", noForums: "Keine Foren gefunden.", - forumVisitButton: "Forum Ansehen", - forumTopicLabel: "Thema", + forumVisitButton: "Forum besuchen", + forumCatGENERAL: "Allgemein", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", + forumCatPOLITICS: "Politik", + forumCatTECH: "Technik", + forumCatSCIENCE: "Wissenschaft", + forumCatMUSIC: "Musik", + forumCatART: "Kunst", + forumCatGAMING: "Gaming", + forumCatBOOKS: "Bücher", + forumCatFILMS: "Filme", + forumCatPHILOSOPHY: "Philosophie", + forumCatSOCIETY: "Gesellschaft", + forumCatPRIVACY: "Privatsphäre", + forumCatCYBERWARFARE: "Cyberkrieg", + forumCatSURVIVALISM: "Survivalismus", imageTitle: "Bilder", imageDescription: "Bildinhalte in deinem Netzwerk entdecken und verwalten.", + imagePluginTitle: "Titel", + imagePluginDescription: "Beschreibung", imageMineSectionTitle: "Deine Bilder", imageCreateSectionTitle: "Bild hochladen", imageUpdateSectionTitle: "Bild aktualisieren", @@ -1485,12 +1619,14 @@ module.exports = { imageTopSectionTitle: "Top-Bilder", imageFavoritesSectionTitle: "Favoriten", imageGallerySectionTitle: "Galerie", + imageMemeSectionTitle: "Memes", imageFilterAll: "ALLE", imageFilterMine: "MEINE", imageFilterRecent: "NEUESTE", imageFilterTop: "TOP", imageFilterFavorites: "FAVORITEN", imageFilterGallery: "GALERIE", + imageFilterMeme: "MEMES", imageCreateButton: "Bild hochladen", imageUpdateButton: "Aktualisieren", imageDeleteButton: "Löschen", @@ -1503,6 +1639,7 @@ module.exports = { imageTitlePlaceholder: "Optional", imageDescriptionLabel: "Beschreibung", imageDescriptionPlaceholder: "Optional", + imageMemeLabel: "MEME?", imageNoFile: "Keine Bilddatei angegeben", noImages: "Keine Bilder vorhanden.", imageSearchPlaceholder: "Titel, Tags, Beschreibung, Autor suchen...", @@ -1510,6 +1647,7 @@ module.exports = { imageSortOldest: "Älteste zuerst", imageSortTop: "Meistgewählt", imageSearchButton: "Suche", + imageMessageAuthorButton: "Nachricht", imageUpdatedAt: "Aktualisiert", imageNoMatch: "Keine Bilder gefunden.", feedTitle: "Feed", @@ -1517,6 +1655,7 @@ module.exports = { createFeedButton: "Feed senden!", feedPlaceholder: "Was passiert? (max. 280 Zeichen)", TODAYButton: "HEUTE", + CREATEButton: "Feed erstellen", totalOpinions: "Meinungen gesamt", moreVoted: "Meistgewählt", noFeedsFound: "Keine Feeds gefunden.", @@ -1539,12 +1678,20 @@ module.exports = { concept: "Konzept", description: "Beschreibung", meme: "Meme", + activityContact: "Kontakt", + activityBy: "Name", + activityPixelia: "Neues Pixel hinzugefügt", + viewImage: "Bild anzeigen", + playAudio: "Audio abspielen", + playVideo: "Video abspielen", typeRecent: "NEUESTE", - typeTop: "TOP", + errorActivity: "Fehler beim Abrufen der Aktivität", typePost: "BLOGS", + recentButton: "NEUESTE", typeTribe: "STÄMME", typeLarp: "L.A.R.P.", typeInhabitants: "BEWOHNER", + activityProfileUpdated: "hat sein Profil aktualisiert", typeAbout: "BEWOHNER", typeCurriculum: "LEBENSLÄUFE", typeImage: "BILDER", @@ -1591,6 +1738,8 @@ module.exports = { typeCourtsSettlementAccepted: "Gerichte · Vergleich angenommen", typeCourtsNomination: "Gerichte · Nominierung", typeCourtsNominationVote: "Gerichte · Nominierungsabstimmung", + activitySupport: "Neue Allianz geschmiedet", + activityJoin: "Neuem PUB beigetreten", question: "Frage", deadline: "Frist", status: "Status", @@ -1604,8 +1753,12 @@ module.exports = { date: "Datum", category: "Kategorie", attendees: "Teilnehmer", + activitySpread: "->", + visitLink: "Link besuchen", + viewDocument: "Dokument anzeigen", location: "Ort", contentWarning: "Betreff", + personName: "Bewohnername", bankWalletConnected: "ECOin Wallet", bankUbiReceived: "UBI erhalten", bankTx: "Tx", @@ -1615,6 +1768,7 @@ module.exports = { link: "Link", activityProjectFollow: "%OASIS% unterstützt jetzt dieses Projekt %PROJECT%", activityProjectUnfollow: "%OASIS% unterstützt dieses Projekt nicht mehr %PROJECT%", + activityProjectPledged: "%OASIS% hat %AMOUNT% für das Projekt %PROJECT% zugesagt", following: "UNTERSTÜTZT", unfollowing: "NICHT MEHR UNTERSTÜTZT", pledged: "ZUGESAGT", @@ -1660,17 +1814,26 @@ module.exports = { courtsVotesSlashTotal: "JA / GESAMT", courtsOpenVote: "Abstimmung eröffnen", courtsAnswerTitle: "Auf die Klage antworten", + courtsStanceADMIT: "Zugeben", + courtsStanceDENY: "Bestreiten", + courtsStancePARTIAL: "Teilweise", + courtsStanceCOUNTERCLAIM: "Gegenklage", + courtsStanceNEUTRAL: "Neutral", courtsVerdictResult: "Ergebnis", courtsVerdictOrders: "Anordnungen", courtsSettlementText: "Bedingungen", courtsSettlementAccepted: "Angenommen", + courtsSettlementPending: "Ausstehend", courtsJudge: "Richter", courtsThSupports: "Unterstützungen", courtsFilterOpenCase: 'FALL ERÖFFNEN', + courtsEvidenceFileLabel: 'Beweisdatei (Bild, Audio, Video oder PDF)', + courtsCaseMediators: 'Mediatoren', courtsCaseMediatorsPh: 'Oasis-IDs der Mediatoren, durch Komma getrennt', courtsMediatorsLabel: 'Mediatoren', courtsThCase: 'Fall', courtsThCreatedAt: 'Startdatum', + courtsThActions: 'Aktionen', courtsPublicPrefLabel: 'Sichtbarkeit nach Klärung', courtsPublicPrefYes: 'Ich stimme zu, dass dieser Fall vollständig öffentlich sein kann', courtsPublicPrefNo: 'Ich bevorzuge, die Details privat zu halten', @@ -1678,11 +1841,8 @@ module.exports = { courtsMethodMEDIATION: 'Mediation', courtsNoCases: 'Keine Fälle.', reportsTitle: "Berichte", - reportsDescription: "Verwalte und verfolge die Meldungen in deinem Netzwerk.", - reportsSearchPlaceholder: "Meldungen suchen...", + reportsDescription: "Berichte zu Problemen, Fehlern, Missbrauch und Inhaltswarnungen in deinem Netzwerk verwalten und verfolgen.", reportsFilterAll: "ALLE", - reportsFilterRecent: "NEU", - reportsFilterTop: "TOP", reportsFilterMine: "MEINE", reportsFilterFeatures: "FUNKTIONEN", reportsFilterBugs: "FEHLER", @@ -1695,13 +1855,18 @@ module.exports = { reportsFilterInvalid: "UNGÜLTIG", reportsCreateButton: "Bericht erstellen", reportsTitleLabel: "Titel", + reportsDescriptionLabel: "Beschreibung", + reportsDescriptionPlaceholder: "Bitte eine ausführliche Beschreibung angeben.", reportsCategory: "Kategorie", + reportsCategoryLabel: "Kategorie", reportsCategoryFeatures: "Funktionen", reportsCategoryBugs: "Fehler", reportsCategoryAbuse: "Missbrauch", - reportsCategoryContent: "Inhalt", + reportsCategoryContent: "Inhaltsprobleme", reportsUpdateButton: "Aktualisieren", reportsDeleteButton: "Löschen", + reportsDateLabel: "Datum", + reportsUploadFile: "Medien hochladen (max: 50MB)", reportsMineSectionTitle: "Deine Berichte", reportsFeaturesSectionTitle: "Funktionsanfragen", reportsBugsSectionTitle: "Fehler", @@ -1709,6 +1874,11 @@ module.exports = { reportsContentSectionTitle: "Inhaltsprobleme", reportsAllSectionTitle: "Berichte", reportsNoItems: "Keine Berichte vorhanden.", + reportsValidationTitle: "Bitte einen gültigen Titel eingeben.", + reportsValidationDescription: "Beschreibung darf nicht leer sein.", + reportsValidationCategory: "Bitte eine Kategorie auswählen.", + reportsCreatedAt: "Erstellt am", + reportsCreatedBy: "Von", reportsSeverity: "Schweregrad", reportsSeverityLow: "Niedrig", reportsSeverityMedium: "Mittel", @@ -1719,10 +1889,13 @@ module.exports = { reportsStatusUnderReview: "In Prüfung", reportsStatusResolved: "Gelöst", reportsStatusInvalid: "Ungültig", + reportsUpdateStatusButton: "Status aktualisieren", + reportsAnonymityOption: "Anonym einreichen", + reportsAnonymousAuthor: "Anonym", reportsConfirmButton: "BERICHT BESTÄTIGEN!", reportsConfirmations: "Bestätigungen", reportsConfirmedSectionTitle: "Bestätigte Berichte", - reportsCreateTaskButton: "Aufgabe erstellen", + reportsCreateTaskButton: "AUFGABE ERSTELLEN", reportsOpenSectionTitle: "Offene Berichte", reportsUnderReviewSectionTitle: "Berichte in Prüfung", reportsResolvedSectionTitle: "Gelöste Berichte", @@ -1766,20 +1939,6 @@ module.exports = { reportsRequestedActionLabel: 'Gewünschte Maßnahme', reportsRequestedActionPlaceholder: 'Entfernen, verbergen, markieren, warnen, etc.', tribesTitle: "Stämme", - tribeFilterAll: "ALLE", - tribeFilterRecent: "NEUESTE", - tribeFilterMine: "MEINE", - tribeFilterMembership: "MITGLIEDSCHAFT", - tribeFilterSubtribes: "UNTERSTÄMME", - tribeFilterTop: "TOP", - tribeFilterGallery: "GALERIE", - tribeFeedFilterTOP: "TOP", - tribeFeedFilterMINE: "MEINE", - tribeFeedFilterALL: "ALLE", - tribeFeedFilterRECENT: "NEUESTE", - courtsStanceDENY: "Bestreiten", - courtsStanceADMIT: "Einräumen", - courtsStancePARTIAL: "Teilweise einräumen", tribeAllSectionTitle: "Stämme", tribeMineSectionTitle: "Deine Stämme", tribeCreateSectionTitle: "Stamm erstellen", @@ -1791,6 +1950,13 @@ module.exports = { tribeviewSubTribeButton: "Unter-Stamm besuchen", tribeRootLabel: "WURZEL", tribeDescription: "Stämme in deinem Netzwerk erkunden oder erstellen.", + tribeFilterAll: "ALLE", + tribeFilterMine: "MEINE", + tribeFilterMembership: "MITGLIEDSCHAFT", + tribeFilterRecent: "NEUESTE", + tribeFilterTop: "TOP", + tribeFilterSubtribes: "UNTER-STÄMME", + tribeFilterGallery: "GALERIE", tribeMainTribeLabel: "HAUPTSTAMM", tribeCreateButton: "Stamm erstellen", tribeUpdateButton: "Aktualisieren", @@ -1809,8 +1975,13 @@ module.exports = { tribeModeLabel: "Einladungsmodus", tribeIsAnonymousLabel: "STATUS", tribeMembersCount: "Mitgliederzahl", + tribeInviteCodePlaceholder: "Einladungscode eingeben", + tribeJoinByCodeButton: "Mit Code beitreten", + tribeJoinButton: "BEITRETEN", tribeEnterInvite: "CODE EINGEBEN", tribeLeaveButton: "VERLASSEN", + tribeYes: "JA", + tribeNo: "NEIN", tribePublic: "ÖFFENTLICH", tribePrivate: "PRIVAT", tribeGenerateInvite: "EINZELNE EINLADUNG", @@ -1819,8 +1990,13 @@ module.exports = { tribeRemoveInvitation: "EINLADUNG ENTFERNEN", tribeCreatedAt: "Erstellt am", tribeAuthor: "Von", + tribeAuthorLabel: "AUTOR", tribeStrict: "Streng", tribeOpen: "Öffnen", + tribeFeedFilterRECENT: "NEUESTE", + tribeFeedFilterMINE: "MEINE", + tribeFeedFilterALL: "ALLE", + tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Weiterleitungen", tribeFeedRefeed: "Weiterleiten", tribeFeedMessagePlaceholder: "Einen Feed schreiben…", @@ -1830,12 +2006,17 @@ module.exports = { tribeNotFound: "Stamm nicht gefunden!", createTribeTitle: "Stamm erstellen", updateTribeTitle: "Stamm aktualisieren", + tribeSectionOverview: "Übersicht", tribeSectionInhabitants: "Bewohner", tribeSectionVotations: "ABSTIMMUNGEN", tribeSectionEvents: "TERMINE", + tribeSectionReports: "Berichte", tribeSectionTasks: "AUFGABEN", tribeSectionFeed: "FEED", tribeSectionForum: "FORUM", + tribeSectionMarket: "Markt", + tribeSectionJobs: "Stellen", + tribeSectionProjects: "Projekte", tribeSectionMedia: "Medien", tribeSectionImages: "BILDER", tribeSectionAudios: "AUDIOS", @@ -1873,6 +2054,11 @@ module.exports = { tribeTaskStatusClosed: "SCHLIESSEN", tribeTaskAssign: "ZUWEISEN", tribeTaskUnassign: "ENTZIEHEN", + tribeReportCreate: "Bericht erstellen", + tribeReportsEmpty: "Noch keine Berichte.", + tribeReportTitle: "Titel", + tribeReportDescription: "Beschreibung", + tribeReportCategory: "Kategorie", tribeVotationCreate: "Abstimmung erstellen", tribeVotationsEmpty: "Noch keine Abstimmungen.", tribeVotationTitle: "Titel", @@ -1890,6 +2076,27 @@ module.exports = { tribeForumCategory: "Kategorie", tribeForumReply: "Antworten", tribeForumReplies: "Antworten", + tribeMarketCreate: "Angebot erstellen", + tribeMarketEmpty: "Noch keine Angebote.", + tribeMarketTitle: "Titel", + tribeMarketDescription: "Beschreibung", + tribeMarketPrice: "Preis", + tribeMarketImage: "Bild", + tribeMarketCategory: "Kategorie", + tribeJobCreate: "Stelle erstellen", + tribeJobsEmpty: "Noch keine Stellen.", + tribeJobTitle: "Titel", + tribeJobDescription: "Beschreibung", + tribeJobLocation: "Ort", + tribeJobSalary: "Gehalt", + tribeJobDeadline: "Frist", + tribeProjectCreate: "Projekt erstellen", + tribeProjectsEmpty: "Noch keine Projekte.", + tribeProjectTitle: "Titel", + tribeProjectDescription: "Beschreibung", + tribeProjectGoal: "Ziel", + tribeProjectFunded: "Finanziert", + tribeProjectDeadline: "Frist", tribeMediaUpload: "Medien hochladen", readDocument: "Dokument lesen", tribeCreateImage: "Bild erstellen", @@ -1900,6 +2107,7 @@ module.exports = { tribeMediaEmpty: "Noch keine Medien.", tribeMediaTitle: "Titel", tribeMediaDescription: "Beschreibung", + tribeMediaType: "Typ", tribeMediaTypeImage: "Bild", tribeMediaTypeVideo: "Video", tribeMediaTypeAudio: "Audio", @@ -1909,6 +2117,11 @@ module.exports = { tribeInviteCodeText: "Einladungscode: ", oasisVersionLabel: "Oasis-Version", tribeInviteCodeHint: "Teile diesen Code mit jemandem, den du einladen möchtest. Er kann über /invites → Tribes beitreten.", + tribeGroupTribe: "Stamm", + tribeGroupOffice: "Büro", + tribeGroupNetwork: "Netzwerk", + tribeGroupEconomy: "Wirtschaft", + tribeGroupMedia: "Medien", tribeStatusOpen: "OFFEN", tribeStatusClosed: "GESCHLOSSEN", tribeStatusInProgress: "IN BEARBEITUNG", @@ -1918,17 +2131,33 @@ module.exports = { tribePriorityCritical: "KRITISCH", tribeTaskFilterAll: "ALLE", tribeMediaFilterAll: "ALLE", + tribeReportCatBug: "BUG", + tribeReportCatAbuse: "MISSBRAUCH", + tribeReportCatContent: "INHALT", + tribeReportCatOther: "SONSTIGES", tribeForumCatGeneral: "ALLGEMEIN", tribeForumCatProposal: "VORSCHLAG", tribeForumCatQuestion: "FRAGE", tribeForumCatAnnouncement: "ANKÜNDIGUNG", + tribeMarketCatGoods: "WAREN", + tribeMarketCatServices: "DIENSTE", + tribeMarketCatFood: "LEBENSMITTEL", + tribeMarketCatOther: "SONSTIGES", tribeStatusLabel: "Status", tribeSubTribes: "UNTER-STÄMME", tribeSubTribesCreate: "Unter-Stamm Erstellen", tribeSubTribesStrictDenied: "Der strikte Modus des Stammes erlaubt Ihnen nicht, neue Unter-Stämme zu erstellen. Bitte wenden Sie sich an den Administrator.", + tribeSubTribesEmpty: "Noch keine Unter-Stämme erstellt.", + tribeActivityJoined: "BEIGETRETEN", + tribeActivityLeft: "VERLASSEN", + tribeActivityFeed: "FEED", tribeActivityRefeed: "WEITERLEITUNG", + tribeGroupAnalytics: "Analysen", + tribeGroupCreative: "Kreativ", tribeSectionActivity: "AKTIVITÄT", tribeSectionTrending: "TRENDS", + tribeSectionOpinions: "MEINUNGEN", + tribeSectionPixelia: "PIXELIA", tribeSectionTags: "TAGS", tribeSectionSearch: "SUCHE", tribeActivityEmpty: "Noch keine Aktivität.", @@ -1940,15 +2169,25 @@ module.exports = { tribeTrendingPeriodWeek: "Diese Woche", tribeTrendingPeriodAll: "Gesamte Zeit", tribeTrendingEngagement: "Beteiligung", + tribeOpinionsEmpty: "Noch keine Meinungen.", + tribeOpinionsCast: "Abstimmen", + tribeOpinionsRankings: "Ranglisten", + tribeOpinionsAlreadyVoted: "Bereits abgestimmt", tribeTopCategory: "Meistgewählt", + tribePixeliaTitle: "Pixelia", + tribePixeliaDescription: "Gemeinschaftliche Pixel-Art-Leinwand.", + tribePixeliaPaint: "Malen", + tribePixeliaContributors: "Mitwirkende", + tribePixeliaTotalPixels: "gemalte Pixel", tribeTagsEmpty: "Keine Tags gefunden.", + tribeTagsCloud: "Tag-Wolke", + tribeTagsContentWith: "Inhalt mit Tag", tribeSearchPlaceholder: "Stamminhalt durchsuchen...", tribeSearchEmpty: "Keine Ergebnisse gefunden.", tribeSearchResults: "Ergebnisse", tribeSearchMinChars: "Mindestens 2 Zeichen eingeben, um zu suchen.", agendaTitle: "Agenda", agendaDescription: "Hier findest du alle dir zugewiesenen Einträge.", - agendaSearchPlaceholder: "Agenda durchsuchen...", agendaFilterAll: "ALLE", agendaFilterToday: "HEUTE", agendaFilterUpcoming: "BEVORSTEHEND", @@ -1962,14 +2201,23 @@ module.exports = { agendaFilterReports: "BERICHTE", agendaFilterTransfers: "ÜBERWEISUNGEN", agendaFilterJobs: "STELLEN", - agendaFilterIndustry: "INDUSTRIE", - agendaFilterHousing: "WOHNEN", agendaFilterProjects: "PROJEKTE", agendaFilterCalendars: "KALENDER", agendaNoItems: "Keine Zuweisungen gefunden.", + agendaAuthor: "Von", agendaDiscardButton: "Verwerfen", agendaRestoreButton: "Wiederherstellen", + agendaCreatedAt: "Erstellt am", + agendaTitleLabel: "Titel", agendaMembersCount: "Mitglieder", + agendaDescriptionLabel: "Beschreibung", + agendaStatus: "Status", + agendaVisibility: "Sichtbarkeit", + agendaEventDate: "Termindatum", + agendaEventLocation: "Ort", + agendaEventPrice: "Preis", + agendaEventUrl: "URL", + agendaTaskStart: "Startzeit", agendaLocationLabel: "Ort", agendaYes: "JA", agendaNo: "NEIN", @@ -1979,6 +2227,11 @@ module.exports = { agendareportCategory: "Kategorie", agendareportSeverity: "Schweregrad", agendareportStatus: "Status", + agendareportDescription: "Beschreibung", + agendaTaskEnd: "Endzeit", + agendaTaskPriority: "Priorität", + agendaTransferFrom: "Von", + agendaTransferTo: "An", agendaTransferConcept: "Konzept", agendaTransferAmount: "Betrag", agendaTransferDeadline: "Frist", @@ -1988,7 +2241,47 @@ module.exports = { voteNow: "Jetzt abstimmen", alreadyVoted: "Du hast bereits abgestimmt.", noOpinionsFound: "Keine Meinungen gefunden.", + RECENTButton: "NEUESTE", TOPButton: "TOP", + interestingButton: "INTERESSANT", + necessaryButton: "NOTWENDIG", + funnyButton: "LUSTIG", + disgustingButton: "WIDERLICH", + sensibleButton: "SENSIBEL", + propagandaButton: "PROPAGANDA", + adultOnlyButton: "NUR FÜR ERWACHSENE", + boringButton: "LANGWEILIG", + confusingButton: "VERWIRREND", + usefulButton: "NÜTZLICH", + informativeButton: "INFORMATIV", + wellResearchedButton: "GUT RECHERCHIERT", + accurateButton: "ZUTREFFEND", + needsSourcesButton: "QUELLEN NÖTIG", + wrongButton: "FALSCH", + lowQualityButton: "NIEDRIGE QUALITÄT", + creativeButton: "KREATIV", + insightfulButton: "AUFSCHLUSSREICH", + actionableButton: "UMSETZBAR", + inspiringButton: "INSPIRIEREND", + loveButton: "LIEBE", + clearButton: "KLAR", + upliftingButton: "ERHEBEND", + unnecessaryButton: "UNNÖTIG", + rejectedButton: "ABGELEHNT", + misleadingButton: "IRREFÜHREND", + offTopicButton: "THEMENFREMD", + duplicateButton: "DUPLIKAT", + clickbaitButton: "CLICKBAIT", + spamButton: "SPAM", + trollButton: "TROLL", + nsfwButton: "NSFW", + violentButton: "GEWALT", + toxicButton: "TOXISCH", + harassmentButton: "BELÄSTIGUNG", + hateButton: "HASS", + scamButton: "BETRUG", + triggeringButton: "TRIGGERND", + opinionsCreatedAt: "Erstellt am", opinionsTotalCount: "Meinungen gesamt", voteInteresting: "Interessant", voteNecessary: "Notwendig", @@ -2025,39 +2318,27 @@ module.exports = { voteCreative: "Kreativ", voteSpam: "Spam", voteAdultOnly: "Nur für Erwachsene", + publishBlog: "Blog veröffentlichen", privateMessage: "PN", pmSendTitle: "Private Nachrichten", pmSend: "Senden!", - pmCancel: "Abbrechen", - pmReset: "Zurücksetzen", - pmSharedKeyHint: "Teile diesen Schlüssel mit dem Empfänger über einen anderen Kanal. Er wird zum Entschlüsseln benötigt.", - pmDescription: "Verwende dieses Formular, um eine verschlüsselte Nachricht/Datei an andere Bewohner zu senden.", - pmComposeTitle: "Nachricht senden", + pmDescription: "Verwende dieses Formular, um eine verschlüsselte Nachricht an andere Bewohner zu senden.", pmRecipients: "Empfänger", pmRecipientsHint: "Oasis-IDs durch Kommas getrennt eingeben", - pmLimitsHint: "Bis zu 7 Empfänger pro Nachricht.", - pmTextPlaceholder: "Text auf 7000 Zeichen begrenzt.", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "Betreff", pmSubjectHint: "Nachrichtenbetreff eingeben", pmText: "Nachricht", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Entschlüsseln", - fileShareTitle: "Datei teilen", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "Datei", - fileShareDownload: "Herunterladen", - fileShareMutualError: "Du kannst Dateien nur mit Bewohnern mit gegenseitiger Unterstützung teilen.", - fileShareNoFile: "Keine Datei ausgewählt.", - fileShareTooLarge: "Die Datei überschreitet die zulässige Größe.", - fileShareFailed: "Die Datei konnte nicht vorbereitet werden.", - fileShareSendError: "Die Datei konnte nicht gesendet werden.", - fileShareUnavailable: "Diese Datei ist gerade nicht verfügbar. Versuche es später erneut.", pmCrypterBadKey: "Der geteilte Schlüssel ist falsch!", pmCrypterTooLong: "Die Nachricht ist zu lang, um mit dem Crypter verschlüsselt zu werden. Bitte kürze sie und versuche es erneut.", pmCrypterCipherLabel: "Erneut verschlüsselte Nachricht", pmCrypterCharsLabel: "Zeichen", pmInvalidRecipients: "Ungültige Oasis-ID (sollte wie @…=.ed25519 aussehen).", pmEncryptionLabel: "Verschlüsselung", + pmFile: "Anhang", private: "Privat", privateDescription: "Deine verschlüsselten Nachrichten.", privateInbox: "POSTEINGANG", @@ -2067,27 +2348,19 @@ module.exports = { noPrivateMessages: "Keine privaten Nachrichten.", pmFromLabel: "Von:", pmToLabel: "An:", + pmInvalidMessage: "Ungültige Nachricht", pmNoSubject: "(kein Betreff)", pmSubjectLabel: "Betreff:", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "Das regierende Haus des L.A.R.P hat gewechselt.", - politicalBotParliamentTitle: "Im Parlament hat ein neuer Regierungszyklus begonnen.", - politicalBotTribeTitle: "In einem deiner Stämme hat ein neuer Regierungszyklus begonnen.", - politicalBotLarpIntro: "Jetzt regiert {house} in diesem Zyklus: {cycle}", - politicalBotParliamentIntro: "Die aktuelle Regierung ist {gov}", - politicalBotParliamentIntroLeader: "Die aktuelle Regierung ist {gov}, ausgeübt von {leader}", - politicalBotTribeIntro: "Die aktuelle Regierung im Stamm {tribe} ist {gov}", - politicalBotTribeIntroLeader: "Die aktuelle Regierung im Stamm {tribe} ist {gov}, ausgeübt von {leader}", - politicalBotLeaderLabel: "Anführer", + pmBodyLabel: "Inhalt", + pmBotJobs: "42-StellenBOT", + pmBotProjects: "42-ProjekteBOT", + pmBotMarket: "42-MarktBOT", inboxJobSubscribedTitle: "Neues Abonnement deines Stellenangebots", pmInhabitantWithId: "Bewohner mit OASIS-ID:", pmHasSubscribedToYourJobOffer: "hat dein Stellenangebot abonniert", + inboxProjectCreatedTitle: "Neues Projekt erstellt", + pmHasCreatedAProject: "hat ein Projekt erstellt", inboxMarketItemSoldTitle: "Artikel verkauft", - inboxShopSoldTitle: "Produkt verkauft", pmYourItem: "Dein Artikel", pmHasBeenSoldTo: "wurde verkauft an", pmFor: "für", @@ -2095,6 +2368,7 @@ module.exports = { pmHasPledged: "hat zugesagt", pmToYourProject: "für dein Projekt", blockchain: 'BlockExplorer', + blockchainTitle: 'BlockExplorer', blockchainDescription: 'Blöcke in der Blockchain erkunden und visualisieren.', blockchainNoBlocks: 'Keine Blöcke in der Blockchain gefunden.', blockchainStatsTitle: 'Statistiken', @@ -2106,36 +2380,42 @@ module.exports = { blockchainBlockCarbon: 'CO₂-Fußabdruck', blockchainBlockType: 'Typ', blockchainBlockTimestamp: 'Zeitstempel', + blockchainBlockContent: 'Block', + blockchainBlockURL: 'URL:', + blockchainContent: 'Block', + blockchainContentPreview: 'Vorschau des Blockinhalts', blockchainLatestDatagram: 'Letztes Datagramm', - blockchainDatagram: "Datagramm", - blockchainDetails: "Blockdetails ansehen", + blockchainDatagram: 'Datagramm', + blockchainDetails: 'Blockdetails anzeigen', blockchainViewBlockexplorer: "Im Blockexplorer ansehen", + blockchainBlockInfo: 'Blockinformationen', + blockchainBlockDetails: 'Details des ausgewählten Blocks', blockchainBack: 'Zurück zum BlockExplorer', blockchainContentDeleted: "Dieser Inhalt wurde gelöscht (Tombstone)", visitContent: "Inhalt besuchen", - favoriteAdd: "Zu Favoriten anheften", - pmContentTooltip: "Private Nachricht senden", - favoriteRemove: "Von Favoriten lösen", - reportContent: "Inhalt melden", banking: 'Bankwesen', bankingTitle: 'Bankwesen', - bankingDescription: "Die ECOin-Wirtschaft: Guthaben, Grundeinkommen, Steuern und Industriewert.", + bankingDescription: 'Den aktuellen Wert von ECOin und die entsprechende UBI-Zuweisung erkunden, die pro Epoche basierend auf Teilnahme und Vertrauen verteilt wird.', bankOverview: 'Übersicht', bankEpochs: 'Epochen', bankRules: 'Regeln', pending: 'Ausstehend', closed: 'Geschlossen', + bankBack: 'Zurück zum Bankwesen', bankViewTx: 'Tx anzeigen', bankClaimNow: 'Jetzt beanspruchen', bankClaimUBI: 'UBI beanspruchen!', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: 'Keine ausstehenden UBI-Zuweisungen für diese Epoche.', bankEpoch: 'Epoche', bankPool: 'Pool (diese Epoche)', bankWeightsSum: 'Gewichtssumme', + bankAllocations: 'Zuweisungen', bankNoAllocations: 'Keine Zuweisungen gefunden.', bankNoEpochs: 'Keine Epochen gefunden.', bankEpochAllocations: 'Epochen-Zuweisungen', @@ -2149,11 +2429,10 @@ module.exports = { bankEpochId: 'Epochen-ID', bankRuleHash: 'Regelversions-Hash', bankViewEpoch: 'Epoche anzeigen', - bankYourIndustryBalance: "Dein Industrieanteil", - bankYourUbiMonth: "Dein BGE (diesen Monat)", - bankYourFundsMonth: "Deine Mittel (diesen Monat)", - bankIndustryBalance: "Industrieproduktion (geschätzt)", bankUserBalance: 'Dein Kontostand', + ecoWalletNotConfigured: 'ECOin Wallet nicht konfiguriert', + editWallet: 'Geldbörse bearbeiten', + addWallet: 'Geldbörse hinzufügen', bankAddresses: 'Adressen', bankNoAddresses: 'Keine Adressen gefunden.', bankUser: 'Oasis-ID', @@ -2178,10 +2457,18 @@ module.exports = { search: 'Suchen!', bankLocal: 'Lokal', bankFromOasis: 'Oasis', + bankMyAddress: 'Deine Adresse', + bankRemoveMyAddress: 'Meine Adresse entfernen', + bankNotRemovableOasis: 'Adressen können nicht lokal entfernt werden', + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "KEIN GUTHABEN!", bankUbiAvailableOk: "VERFÜGBAR!", - bankUbiAvailability: "BGE (Netzwerk)", + bankUbiAvailability: "UBI-Verfügbarkeit", bankAlreadyClaimedThisMonth: "Diesen Monat bereits beansprucht", bankUbiThisMonth: "UBI (diesen Monat)", bankUbiLastClaimed: "UBI (zuletzt beansprucht)", @@ -2195,6 +2482,13 @@ module.exports = { shopsTitle: "Shops", shopDescription: "Shops im Netzwerk entdecken und verwalten.", shopTitle: "Shop", + shopFilterAll: "ALLE", + shopFilterMine: "MEINE", + shopFilterRecent: "NEUESTE", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUKTE", + shopFilterPrices: "PREISE", + shopFilterFavorites: "FAVORITEN", shopUpload: "Shop erstellen", shopAllSectionTitle: "Alle Shops", shopMineSectionTitle: "Meine Shops", @@ -2211,10 +2505,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "Im Clearnet veröffentlichen", + shopClearnetUnpublish: "Aus Clearnet entfernen", + shopClearnetUrlLabel: "Clearnet-URL", + shopClearnetWarning: "Die Veröffentlichung im Clearnet macht diesen Shop für externe Suchmaschinen und Crawler indexierbar.", shopAddFavorite: "Favorit hinzufügen", shopRemoveFavorite: "Favorit entfernen", shopOpen: "OFFEN", shopClosed: "GESCHLOSSEN", + shopOpenShop: "Shop öffnen", + shopCloseShop: "Shop schließen", shopMakePrivate: "PRIVAT MACHEN (E2E)", shopMakePublic: "ÖFFENTLICH MACHEN", shopProducts: "Produkte", @@ -2242,6 +2542,8 @@ module.exports = { shopOrderPrice: "Preis", shopOrderBuyer: "Käufer", shopBackToShop: "Zurück zum Shop", + shopShareUrl: "URL teilen", + shopVisitShop: "SHOP BESUCHEN", marketVisitItem: "ARTIKEL ANZEIGEN", shopStatus: "STATUS", shopCreatedAt: "ERSTELLT", @@ -2250,10 +2552,12 @@ module.exports = { shopLocation: "Standort", shopTags: "Tags", shopVisibility: "Sichtbarkeit", + shopImage: "Bild", shopShortDescription: "Kurzbeschreibung", shopShortDescriptionPlaceholder: "Kurzbeschreibung fuer Shopkarten (max. 160 Zeichen)", shopTitlePlaceholder: "Name Ihres Shops", shopDescriptionPlaceholder: "Detaillierte Beschreibung Ihres Shops", + shopUrlPlaceholder: "https://ihre-shop-url.com", shopLocationPlaceholder: "Stadt, Land", shopTagsPlaceholder: "tag1, tag2, tag3", mapTitlePlaceholder: "Kartentitel", @@ -2271,6 +2575,7 @@ module.exports = { bankUnitMinutes: "Minuten", bankUnitDays: "Tage", bankExchangeNoData: 'Keine Daten verfügbar', + bankExchangeIndex: 'ECOin-Wert (1h)', bankInflation: 'ECOin-Inflation (anual)', bankInflationMonthly: 'ECOin-Inflation (mensual)', bankExchangeChartTitle: 'Progression des ECOin-Werts im Zeitverlauf', @@ -2284,17 +2589,38 @@ module.exports = { bankingSyncStatusOutdated: 'Veraltet', statsTitle: 'Statistiken', statistics: "Statistiken", + statsInhabitant: "Bewohner-Statistiken", statsDescription: "Entdecke Statistiken über dein Netzwerk.", ALLButton: "ALLE", MINEButton: "MEINE", TOMBSTONEButton: "TOMBSTONES", + statsYou: "Du", + statsUserId: "Oasis-ID", statsCreatedAt: "Erstellt am", statsYourContent: "Inhalt", statsYourOpinions: "Meinungen", + statsYourTombstone: "Tombstones", + statsNetwork: "Netzwerk", + statsTotalInhabitants: "Bewohner", + statsDiscoveredTribes: "Stämme (Öffentlich)", + statsPrivateDiscoveredTribes: "Stämme (Privat)", statsNetworkContent: "Inhalt", + statsYourMarket: "Markt", + statsYourJob: "Stellen", + statsYourProject: "Projekte", + statsYourTransfer: "Überweisungen", + statsYourForum: "Foren", statsNetworkOpinions: "Meinungen", + statsDiscoveredMarket: "Markt", + statsDiscoveredJob: "Stellen", + statsDiscoveredProject: "Projekte", + statsBankingTitle: "Bankwesen", statsEcoWalletLabel: "ECOIN Wallet", statsEcoWalletNotConfigured: "Nicht konfiguriert!", + statsTotalEcoAddresses: "Adressen gesamt", + statsDiscoveredTransfer: "Überweisungen", + statsDiscoveredForum: "Foren", + statsNetworkTombstone: "Tombstones", statsBookmark: "Lesezeichen", statsEvent: "Termine", statsTask: "Aufgaben", @@ -2316,12 +2642,16 @@ module.exports = { statsAiExchange: "KI", statsPUBs: 'PUBs', statsPost: "Beiträge", + statsOasisID: "Oasis-ID", statsSize: "Gesamt (Größe)", statsBlockchainSize: "Blockchain (Größe)", statsBlobsSize: "Blobs (Größe)", statsActivity7d: "Aktivität (letzte 7 Tage)", statsActivity7dTotal: "7-Tage-Gesamt", statsActivity30dTotal: "30-Tage-Gesamt", + statsKarmaScore: "KARMA-Punktzahl", + statsPublic: "Öffentlich", + statsPrivate: "Privat", day: "TAG", messages: "Nachrichten", statsProject: "Projekte", @@ -2333,14 +2663,30 @@ module.exports = { statsProjectsCancelled: "Abgebrochen", statsProjectsGoalTotal: "Ziel gesamt", statsProjectsPledgedTotal: "Zugesagt gesamt", + statsProjectsSuccessRate: "Erfolgsquote", + statsProjectsAvgProgress: "Durchschnittlicher Fortschritt", + statsProjectsMedianProgress: "Medianer Fortschritt", + statsProjectsActiveFundingAvg: "Durchschn. aktive Finanzierung", + statsJobsTitle: "Stellen", + statsJobsTotal: "Stellen gesamt", + statsJobsOpen: "Öffnen", + statsJobsClosed: "Geschlossen", + statsJobsOpenVacants: "Offene Stellen", + statsJobsSubscribersTotal: "Abonnenten gesamt", + statsJobsAvgSalary: "Durchschnittsgehalt", + statsJobsMedianSalary: "Mediangehalt", statsMarketTitle: "Markt", statsMarketTotal: "Artikel gesamt", statsMarketForSale: "Zum Verkauf", statsMarketReserved: "Reserviert", statsMarketClosed: "Geschlossen", statsMarketSold: "Verkauft", + statsMarketRevenue: "Umsatz", + statsMarketAvgSoldPrice: "Durchschn. Verkaufspreis", statsUsersTitle: "Bewohner", user: "Bewohner", + statsTombstoneTitle: "Tombstones", + statsNetworkTombstones: "Netzwerk-Tombstones", statsTombstoneRatio: "Tombstone-Anteil (%)", statsParliamentCandidature: "Parlamentskandidaturen", statsParliamentTerm: "Parlamentsperioden", @@ -2367,21 +2713,30 @@ module.exports = { aiConfiguration: "Prompt setzen", aiPromptUsed: "Prompt", aiClearHistory: "Chatverlauf löschen", + aiSharePrompt: "Diese Antwort zum kollektiven Training hinzufügen?", + aiShareYes: "Ja", + aiShareNo: "Nein", + aiSharedLabel: "Zum Training hinzugefügt", + aiRejectedLabel: "Nicht zum Training hinzugefügt", aiServerError: "Die KI konnte nicht antworten. Bitte versuche es erneut.", aiInputPlaceholder: "Was gibt es Neues?", typeAiExchange: "KI", aiApproveTrain: "Zum kollektiven Training hinzufügen", aiRejectTrain: "Nicht trainieren", + aiTrainPending: "Genehmigung ausstehend", aiTrainApproved: "Für Training genehmigt", aiTrainRejected: "Für Training abgelehnt", aiSnippetsUsed: "Verwendete Fragmente", aiSnippetsLearned: "Erlernte Fragmente", statsAITraining: "KI-Training", + aiApproveCustomTrain: "Mit dieser benutzerdefinierten Antwort trainieren", aiCustomAnswerPlaceholder: "Schreibe deine benutzerdefinierte Antwort…", + statsAIExchanges: "Austausche", marketMineSectionTitle: "Deine Artikel", marketCreateSectionTitle: "Artikel erstellen", marketUpdateSectionTitle: "Aktualisieren", marketAllSectionTitle: "Markt", + marketRecentSectionTitle: "Neueste Marktartikel", marketTitle: "Markt", marketDescription: "Ein Marktplatz zum Austausch von Waren oder Dienstleistungen in deinem Netzwerk.", marketFilterAll: "ALLE", @@ -2411,11 +2766,14 @@ module.exports = { marketItemDeadline: "Frist", marketItemIncludesShipping: "Versand inklusive?", marketItemHighestBid: "Höchstgebot", + marketItemHighestBidder: "Höchstbietender", marketItemStock: "Bestand", marketOutOfStock: "Nicht vorrätig", + marketItemBidTime: "Gebotszeit", marketActionsUpdate: "Aktualisieren", marketUpdateButton: "Aktualisieren", marketActionsDelete: "Löschen", + marketActionsSold: "Als verkauft markieren", marketActionsChangeStatus: "STATUS ÄNDERN", marketActionsBuy: "KAUFEN!", marketAuctionBids: "Aktuelle Gebote", @@ -2424,82 +2782,19 @@ module.exports = { marketNoItems: "Noch keine Artikel vorhanden.", marketYourBid: "Dein Gebot", marketCreateFormImageLabel: "Medien hochladen (max: 50MB)", + marketSearchLabel: "Suche", marketSearchPlaceholder: "Titel oder Tags suchen", marketMinPriceLabel: "Mindestpreis", marketMaxPriceLabel: "Höchstpreis", + marketSortLabel: "Sortieren nach", marketSortRecent: "Neueste zuerst", marketSortPrice: "Preis", marketSortDeadline: "Frist", marketSearchButton: "Suche", marketAuctionEndsIn: "Endet", marketAuctionEnded: "Beendet", + marketMyBidBadge: "Du hast geboten", marketNoItemsMatch: "Keine Artikel gefunden.", - housingTitle: "Wohnen", - housingDescriptionText: "Entdecke und verwalte Orte in deinem Netzwerk.", - modulesHousingLabel: "Wohnen", - modulesHousingDescription: "Modul zum Entdecken und Verwalten von Orten.", - typeHousing: "WOHNEN", - housingFilterAll: "ALLE", - housingFilterMine: "MEINE", - housingFilterRequested: "ANGEFRAGT", - housingFilterSale: "VERKAUF", - housingFilterRent: "MIETE", - housingFilterCouchsurfing: "COUCHSURFING", - housingFilterOpen: "VERFÜGBAR", - housingFilterClosed: "NICHT VERFÜGBAR", - housingFilterRecent: "NEU", - housingFilterTop: "TOP", - housingType: "Angebot", - housingTypeSALE: "Zu verkaufen", - housingTypeRENT: "Zu vermieten", - housingTypeCOUCHSURFING: "Couchsurfing", - housingProperty: "Objekt", - housingPropertyAPARTMENT: "Wohnung", - housingPropertyHOUSE: "Haus", - housingPropertyROOM: "Zimmer", - housingPropertyLAND: "Grundstück", - housingPropertyOTHER: "Andere", - housingTitleLabel: "Titel", - housingTitlePlaceholder: "Name des Ortes", - housingDescription: "Beschreibung", - housingDescriptionPlaceholder: "Beschreibe den Ort, die Gegend, was enthalten ist: Bäder, Garten, Betten…", - housingRules: "Regeln", - housingRulesPlaceholder: "Haustiere, Rauchen, Ruhezeiten, Mindestaufenthalt…", - housingPlace: "Ort", - housingPlacePlaceholder: "Land, Stadt, Adresse", - housingPrice: "Preis", - housingPriceLabel: "Preis (ECO)", - housingSizeLabel: "Fläche (m²)", - housingFree: "GRATIS", - housingPerMonth: "/Monat", - housingRooms: "Zimmer", - housingSize: "Fläche", - housingCapacity: "Gäste", - housingAvailableFrom: "Verfügbar ab", - housingAvailableTo: "Verfügbar bis", - housingTags: "Schlagwörter", - spreadEditWarning: "Dieser Inhalt verliert seine Spreads beim Bearbeiten", - housingRequests: "Anfragen", - housingRequestButton: "Anfragen", - housingCancelButton: "Anfrage zurückziehen", - housingRequestedBadge: "ANGEFRAGT", - housingStatus: "Status", - housingStatusOPEN: "VERFÜGBAR", - housingStatusCLOSED: "NICHT VERFÜGBAR", - housingSetOpen: "Verfügbar machen", - housingSetClosed: "Nicht verfügbar machen", - housingCreateButton: "Ort erstellen", - housingUpdateButton: "Aktualisieren", - housingDeleteButton: "Löschen", - housingSearchPlaceholder: "Orte suchen...", - housingSearchButton: "Suchen", - housingMinPrice: "Min. Preis", - housingMaxPrice: "Max. Preis", - housingSortRecent: "Neueste", - housingSortPrice: "Niedrigster Preis", - housingSortRequests: "Meist angefragt", - housingSortRating: "Bestbewertet", - housingNoItems: "Keine Orte gefunden.", jobsTitle: "Stellen", jobsCandidatesTitle: "Vorgeschlagene Kandidaten", jobsCandidatesDescription: "Einwohner, deren Fähigkeiten zu dieser Stelle passen. Sende ihnen eine private Nachricht.", @@ -2547,6 +2842,7 @@ module.exports = { jobLocationPresencial: "Vor Ort", jobLocationRemote: "Remote", jobVacantsPlaceholder: "Anzahl der Positionen", + jobSalaryPlaceholder: "Gehalt in ECO für 1 Arbeitsstunde", jobImage: "Medien hochladen (max: 50MB)", jobTasks: "Aufgaben", jobType: "Stellenart", @@ -2556,6 +2852,7 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Top-Gehalt-Stellen", createJobButton: "Stelle veröffentlichen", + viewDetailsButton: "Details anzeigen", noJobsFound: "Keine Stellenangebote gefunden.", jobAuthor: "Von", jobTypeFreelance: "Freiberufler", @@ -2567,6 +2864,10 @@ module.exports = { jobsHoursOffered: "Angebotene Stunden", jobsHoursRequested: "Im Tausch gefragte Stunden", jobsExchangeSkill: "Im Tausch gefragte Fähigkeit", + jobsHoursOfferedPlaceholder: "z. B. 4", + jobsHoursRequestedPlaceholder: "z. B. 4", + jobsExchangeSkillPlaceholder: "z. B. Tischlerei, Design", + jobExchangeHint: "Für Stundentausch-Angebote füllen Sie statt des Gehalts die folgenden Felder aus.", jobTimePartial: "Teilzeit", jobTimeComplete: "Vollzeit", jobsDeleteButton: "LÖSCHEN", @@ -2574,201 +2875,31 @@ module.exports = { jobsFilterApplied: "BEWORBEN", jobsAppliedTitle: "Meine Bewerbungen", jobsAppliedBadge: "Beworben", + jobsFilterFavs: "Favoriten", + jobsFavsTitle: "Favoriten", + jobsFilterNeeds: "Braucht Hilfe", + jobsNeedsTitle: "Braucht Hilfe", + jobsSearchLabel: "Suche", jobsSearchPlaceholder: "Titel, Tags, Beschreibung suchen...", jobsMinSalaryLabel: "Mindestgehalt", jobsMaxSalaryLabel: "Höchstgehalt", + jobsSortLabel: "Sortieren nach", jobsSortRecent: "Neueste zuerst", jobsSortSalary: "Höchstes Gehalt", jobsSortSubscribers: "Meiste Bewerber", jobsSearchButton: "Suche", + jobsFavoriteButton: "Favorit", + jobsUnfavoriteButton: "Kein Favorit", + jobsMessageAuthorButton: "PN", jobsApplicants: "Bewerber", jobsUpdatedAt: "Aktualisiert", jobSetOpen: "Als OFFEN setzen", + jobNewBadge: "NEU", jobsTagsLabel: "Tags", + jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "Keine Stellen gefunden.", - industrySearchPlaceholder: "Fabriken suchen…", - industryAllSectors: "Alle Sektoren", - industryVisitButton: "Industrie besuchen", - industryAdvanceTo: "Setzen auf", - industryAmount: "Betrag", - industryApproveBuild: "Genehmigen", - industryBlueprint: "Blueprint", - industryBlueprints: "Blueprints", - industryBuild: "Build", - industryBuildNotes: "Notizen", - industryBuildStart: "Startdatum", - industryVoteUpdateButton: "Update abstimmen", - industryVoteDeleteButton: "Löschung abstimmen", - industryRevokeVote: "Widerrufen", - industryTimeLeft: "Verbleibende Zeit", - industryBuildEnd: "Enddatum", - industryBuilds: "Builds", - industryBuildTitle: "Titel", - industryContribute: "Beitragen", - industryContributeButton: "Beitragen", - industryContributions: "Beiträge", - industryCreateBlueprintButton: "Blueprint erstellen", - industryDistribute: "Verteilen", - industryDistributeButton: "Nach Anteilen verteilen", - industryDistribution: "Verteilung", - industryEcoAmount: "Betrag (ECO)", - industryEditBlueprint: "Blueprint bearbeiten", - industryFacility: "Fabrik", - industryKindLabel: "Art", - industryKind_labor: "Arbeit", - industryKind_material: "Material", - industryKind_eco: "Mittel", - industryLaborHours: "Arbeit (Stunden)", - industryHours: "Stunden", - industryMaterialItem: "Material", - industryMaterials: "Materialien", - industryEstMaterials: "Gesamtmaterialien", - industryEstLabor: "Gesamtarbeit", - industryEstTotal: "Geschätzter Preis", - industryBuildingPrice: "Baupreis", - industryFinalPrice: "Endpreis", - industryBlueprintDescPlaceholder: "Spezifikationen, Abmessungen, Gewicht, Doku…", - industryMaterialValue: "Wert (ECO)", - industryMember: "Mitglied", - industryNewBlueprint: "Neuer Blueprint", - industryNewBuild: "Build vorschlagen", - industryEditBuild: "Produktion bearbeiten", - industryNeedBlueprint: "Erstelle zuerst einen Bauplan: jede Produktion fertigt einen.", - industryNoBlueprints: "Noch keine Blueprints.", - industryNoBuilds: "Noch keine Builds.", - industryNote: "Notiz", - industryOutput: "Ausgabe", - industryOutputKind: "Produkttyp", - industryOutputValue: "Ausgabewert (ECO, z. B. aus Verkauf)", - industryPostJob: "Stelle erstellen", - industryPot: "Topf", - industryProposeBuildButton: "Build vorschlagen", - industryProposer: "Vorschlagender", - industryAuthor: "Autor", - industrySellOnMarket: "An den Markt senden", - industrySendToProjects: "Projekt erstellen", - industrySkills: "Fähigkeiten", - industryTreasury: "Kasse", - industryKind_physical: "Physisch", - industryKind_digital: "Digital", - industryBuildStatus_PROPOSED: "VORGESCHLAGEN", - industryBuildStatus_REJECTED: "ABGELEHNT", - industryBuildStatus_APPROVED: "GENEHMIGT", - industryBuildStatus_STOCKING: "BESTÜCKUNG", - industryBuildStatus_IN_PRODUCTION: "IN PRODUKTION", - industryBuildStatus_COMPLETED: "ABGESCHLOSSEN", - industryBuildStatus_FAILED: "FEHLGESCHLAGEN", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "Ein Job passt zu deinem Lebenslauf", - jobsBotMatchIntro: "Ein Job passt zu deinem Lebenslauf.", - jobsBotMatchJob: "Job", - jobsBotMatchHint: "Du erhältst dies, weil dein Lebenslauf KI-verwaltet ist. Du kannst es im Lebenslauf abschalten.", - cvAiManaged: "KI-verwaltet", - switchOn: "EIN", - switchOff: "AUS", - cvAiManagedHint: "Wenn aktiv, meldet JobsBot per Privatnachricht, wenn ein Job zu deinem Lebenslauf passt.", - cvMatchThreshold: "Ab dieser Übereinstimmung benachrichtigen", - housingBotRequestedTitle: "Jemand hat einen deiner Orte angefragt.", - housingBotCancelledTitle: "Eine Anfrage zu einem deiner Orte wurde zurückgezogen.", - housingBotYouRequestedTitle: "Du hast einen Ort angefragt.", - housingBotYouCancelledTitle: "Du hast eine Anfrage zurückgezogen.", - housingBotUnavailableTitle: "Ein von dir angefragter Ort ist nicht mehr verfügbar.", - housingBotUnavailableSentTitle: "Du hast die Anfragenden benachrichtigt.", - industryBotAdmittedTitle: "Du wurdest in eine Fabrik aufgenommen.", - industryBotApplicationTitle: "Neuer Mitgliedsantrag in deiner Fabrik.", - industryBotInvitedTitle: "Du wurdest in eine Fabrik eingeladen.", - industryBotDissolvedTitle: "Eine Fabrik, der du angehörst, wurde aufgelöst.", - industryBotBuildApprovedTitle: "Ein Build wurde genehmigt.", - industryBotDistributedTitle: "Ein Build-Output wurde verteilt.", - industryFilterRules: "REGELN", - industryRulesTitle: "Regeln", - industryRulesIntro: "Industry lässt die Bewohner die Produktionsmittel gemeinschaftlich besitzen: Eine Fabrik ist eine netzwerkeigene Werkstatt, die niemand privat besitzt.", - industryRulesSteward: "Der Gründer ist der Verwalter — ein Treuhänder, kein Eigentümer: Er kann die Fabrik nicht allein verkaufen, privatisieren oder auflösen.", - industryRulesMembership: "Die Mitgliedschaftsrichtlinie legt fest, wie man beitritt: Offen (sofort), Per Abstimmung (durch die Stimme der Mitglieder) oder Nur mit Einladung (ein Mitglied muss zuerst einladen).", - industryRulesQuorum: "Das Quorum ist die Mindestzahl an Mitgliedern, die abstimmen müssen, damit eine Entscheidung zählt, damit eine kleine Fabrik nicht von einer Person entschieden wird.", - industryRulesMajority: "Die Mehrheit ist der Stimmenanteil zum Bestehen (0,5 = Hälfte, 1 = Einstimmigkeit); eine Entscheidung besteht, wenn die JA-Stimmen mindestens max(Quorum, Mitglieder × Mehrheit) erreichen.", - industryRulesDecisions: "Mitglieder stimmen über Aufnahme, Genehmigung von Builds und Auflösung der Fabrik ab; der Verwalter kann diese kollektiven Entscheidungen nicht übergehen.", - industryRulesBlueprints: "Blueprints sind freie (Copyleft-)Rezepte — die Eingaben (Materialien, Arbeitsstunden, Fähigkeiten) und die Ausgabe (ein physisches oder digitales Gut) — und jeder kann sie forken.", - industryRulesBuilds: "Ein Build ist ein Produktionsauftrag: VORGESCHLAGEN → GENEHMIGT (per Abstimmung) → BESTÜCKUNG → IN PRODUKTION → ABGESCHLOSSEN, und er nimmt Beiträge erst nach Genehmigung an.", - industryRulesContributions: "Mitglieder tragen Arbeit (Stunden), Material (deklarierter ECO-Wert) oder ECO zu einem Build bei; jeder Beitrag bringt Karma.", - industryRulesLaborRate: "Der Arbeitssatz ist der ECO-Wert einer Arbeitsstunde in dieser Fabrik; er wandelt Stunden in Karma um, damit Aufwand und Kapital fair vergleichbar sind: Karma = Stunden × Satz + Materialwert + ECO.", - industryRulesShares: "Dein Anteil an einem Build ist dein Karma ÷ Gesamt-Karma und bestimmt, wie viel des Ausgabewerts du erhältst.", - industryRulesDistribution: "Wenn eine Produktion abgeschlossen ist, verteilt der Steward den Topf (die Mittel der Produktion plus Verkaufswert) anteilig an die Beitragenden.", - industryRulesTreasury: "ECO-Beiträge hält der Verwalter als Treuhänder der Build-Kasse bis zur Verteilung; alle Bewegungen werden im Netzwerk aufgezeichnet und Streitfälle können an die Courts gehen.", - industryJobs: "Stellen", - industryReportButton: "Melden", - industryCreateTask: "Aufgabe erstellen", - industryOpenDispute: "Streit eröffnen", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "Einladung generieren", - industryPauseVotes: "Status-Stimmen", - industryTitle: "Industrie", - industryDescription: "Modul zur gemeinschaftlichen Verwaltung der Produktionsmittel.", - typeIndustryBuild: "BUILD", - typeIndustryBlueprint: "BLUEPRINT", - typeIndustryAllocation: "VERTEILUNG", - typeIndustry: "INDUSTRIE", - modulesIndustryLabel: "Industrie", - modulesIndustryDescription: "Modul zur gemeinschaftlichen Verwaltung der Produktionsmittel.", - industryFilterAll: "ALLE", - industryFilterMember: "MITGLIED", - industryFilterBlueprints: "BAUPLÄNE", - industryFilterBuilds: "PRODUKTIONEN", - industryFilterMine: "MEINE", - industryFilterActive: "IN BETRIEB", - industryFilterPaused: "PAUSIERT", - industryFilterDissolved: "AUFGELÖST", - industryAllTitle: "Industrie", - industryMemberTitle: "Fabriken, in denen ich bin", - industryMineTitle: "Meine Fabriken", - industryActiveTitle: "In Betrieb", - industryPausedTitle: "Pausierte Fabriken", - industryDissolvedTitle: "Aufgelöste Fabriken", - industryNoFacilitiesFound: "Keine Fabriken gefunden.", - industryCreateFacility: "Fabrik erstellen", - industryMembers: "Mitglieder", - industryMemberBadge: "MITGLIED", - industryName: "Name", - industryNamePlaceholder: "Name der Fabrik", - industryDescriptionLabel: "Beschreibung", - industryDescriptionPlaceholder: "Was produziert diese Fabrik?", - industrySector: "Sektor", - industryMembershipPolicy: "Mitgliedschaftsrichtlinie", - industryQuorum: "Quorum", - industryMajority: "Mehrheit", - industryLaborRate: "Arbeitssatz", - industryCreateButton: "Fabrik erstellen", - industryUpdateButton: "Aktualisieren", - industrySteward: "Verwalter", - industryStatusActive: "IN BETRIEB", - industryStatusPaused: "PAUSIERT", - industryStatusDissolved: "AUFGELÖST", - industryStatusLabel: "Status", - industryJoinButton: "Beitreten", - industryApplyButton: "Beitritt beantragen", - industryLeaveButton: "Verlassen", - industryInviteButton: "Einladen", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "Du brauchst eine Einladung, um beizutreten.", - industryPauseButton: "Für Pause stimmen", - industryResumeButton: "Für Fortsetzung stimmen", - industryDeleteButton: "Löschen", - industryPendingApplicants: "Ausstehende Bewerber", - industryVotesYes: "ja", - industryVotesNo: "nein", - industryAlreadyVoted: "abgestimmt", - industryAdmit: "Zulassen", - industryReject: "Ablehnen", - industryDissolveTitle: "Fabrik auflösen", - industryDissolveVote: "Für Auflösung stimmen", - industryPolicy_open: "Offen", - industryPolicy_vote: "Per Abstimmung", - industryPolicy_invite: "Nur mit Einladung", projectsTitle: "Projekte", projectsDescription: "Gemeinschaftsprojekte in deinem Netzwerk erstellen, finanzieren und verfolgen.", - projectSearchPlaceholder: "Projekte suchen...", projectCreateProject: "Projekt erstellen", projectCreateButton: "Projekt erstellen", projectUpdateButton: "AKTUALISIEREN", @@ -2812,8 +2943,14 @@ module.exports = { projectPledgeTitle: "Dieses Projekt unterstützen", projectPledgePlaceholder: "Betrag in ECO", projectBounties: "Prämien", + projectBountiesInputLabel: "Prämien (eine pro Zeile: Titel|Betrag [ECO]|Beschreibung)", + projectBountiesPlaceholder: "UI-Fehler beheben|100|Link zum Problem\\nDoku schreiben|250|Anwendungsbeispiele skizzieren", projectNoBounties: "Keine Prämien gefunden.", projectTitle: "Titel", + projectAddBountyTitle: "Neue Prämie", + projectBountyTitle: "Prämientitel", + projectBountyAmount: "Betrag (ECO)", + projectBountyDescription: "Beschreibung", projectMilestoneSelect: "Meilenstein auswählen", projectBountyCreateButton: "Prämie erstellen", projectBountyStatus: "Prämienstatus", @@ -2828,15 +2965,19 @@ module.exports = { projectMilestoneTitle: "Meilensteintitel", projectMilestoneTargetPercent: "Prozent (%)", projectMilestoneDueDate: "Datum", + projectMilestoneCreateButton: "Meilenstein erstellen", projectMilestoneStatus: "Meilensteinstatus", projectMilestoneOpen: "offen", projectMilestoneDone: "abgeschlossen", projectMilestoneDue: "fällig", + projectNoMilestones: "Keine Meilensteine gefunden.", projectMilestoneMarkDone: "Als abgeschlossen markieren", projectMilestoneTitlePlaceholder: "Meilensteintitel eingeben", projectMilestoneDescriptionPlaceholder: "Beschreibung für diesen Meilenstein eingeben", projectMilestoneDescription: "Meilensteinbeschreibung", + projectBudgetGoal: "Budget (Ziel)", projectBudgetAssigned: "Prämien zugewiesen", + projectBudgetRemaining: "Verbleibend", projectBudgetOver: "Budget überschritten: Zuweisungen übersteigen das Ziel", projectFollowers: "Follower", projectFollowersTitle: "Follower", @@ -2849,6 +2990,7 @@ module.exports = { projectBackersTotalPledged: "Zugesagt gesamt", projectBackersYourPledge: "Deine Zusage", projectBackersNone: "Noch keine Zusagen.", + projectNoRemainingBudget: "Kein verbleibendes Budget.", projectFilterBackers: "UNTERSTÜTZER", projectFilterApplied: "BEWORBEN", projectAppliedTitle: "BEWORBEN", @@ -2857,15 +2999,30 @@ module.exports = { projectBackerAmount: "Gesamtbeitrag", projectBackerPledges: "Zusagen", projectBackerProjects: "Projekte", + projectPledgeAmount: "Betrag", projectSelectMilestoneOrBounty: "Meilenstein oder Prämie auswählen", projectPledgeButton: "Zusagen", footerLicense: "GPLv3", + footerPackage: "Paket", + footerVersion: "Version", modulesModuleName: "Name", modulesModuleDescription: "Beschreibung", modulesModuleStatus: "Status", modulesTotalModulesLabel: "Geladene Module", modulesEnabledModulesLabel: "Aktiviert", modulesDisabledModulesLabel: "Deaktiviert", + modulesPopularLabel: "Beliebt", + modulesPopularDescription: "Modul zum Empfang von Beiträgen, die im Trend liegen, am meisten angesehen oder kommentiert werden.", + modulesTopicsLabel: "Themen", + modulesTopicsDescription: "Modul zum Empfang von Diskussionskategorien basierend auf gemeinsamen Interessen.", + modulesSummariesLabel: "Zusammenfassungen", + modulesSummariesDescription: "Modul zum Empfang von Zusammenfassungen langer Diskussionen oder Beiträge.", + modulesLatestLabel: "Neueste", + modulesLatestDescription: "Modul zum Empfang der neuesten Beiträge und Diskussionen.", + modulesThreadsLabel: "Diskussionen", + modulesThreadsDescription: "Modul zum Empfang von Unterhaltungen, gruppiert nach Thema oder Frage.", + modulesMultiverseLabel: "Multiversum", + modulesMultiverseDescription: "Modul zum Empfang von Inhalten anderer föderierter Peers.", modulesInvitesLabel: "Einladungen", modulesInvitesDescription: "Modul zum Verwalten und Einlösen von Einladungscodes.", modulesWalletLabel: "Geldbörse", @@ -2906,12 +3063,8 @@ module.exports = { modulesOpinionsDescription: "Modul zum Entdecken und Bewerten von Meinungen.", modulesTransfersLabel: "Überweisungen", modulesTransfersDescription: "Modul zum Entdecken und Verwalten von Smart Contracts (Überweisungen).", - modulesFediverseLabel: "Multiversum", - modulesBlogsLabel: "Blogs", - modulesBlogsDescription: "Modul zum Entdecken und Verwalten von Blogs.", - modulesPollsLabel: "Umfragen", - modulesPollsDescription: "Modul, um das Netzwerk zu fragen und Antworten zu zählen.", - modulesFediverseDescription: "Verwalte deine anderen Multiversum-Konten, einschließlich Senden und Empfangen von Inhalten.", + modulesFediverseLabel: "Fediverse", + modulesFediverseDescription: "Verwalte deine anderen Fediverse-Konten, einschließlich Senden und Empfangen von Inhalten.", modulesFeedLabel: "Feed", modulesFeedDescription: "Modul zum Entdecken und Teilen von Kurztexten (Feeds).", modulesParliamentLabel: "Parlament", @@ -2947,33 +3100,37 @@ module.exports = { visibilityMakeHidden: "Verstecken", invitesInhabitantsTitle: "Bewohner", invitesInhabitantsFollow: "Folgen", - aiNavPlaceholder: "Beschreib, was du brauchst...", + aiNavPlaceholder: "Wohin möchtest du gehen?", aiNavDisabled: "KI-Navigation ist deaktiviert.", - aiNavResultsTitle: "AINav-Vorschläge", + aiNavResultsTitle: "KI-Navigationsvorschläge", aiNavQueryLabel: "Anfrage", + aiNavResultsDescription: "Wähle die Route, die am besten zu deiner Suche passt.", + aiNavResultPath: "Route", + aiNavResultDescription: "Was du dort tun kannst", aiNavResultMatch: "Übereinstimmung", aiNavResultsEmpty: "Keine passenden Routen. Versuche stattdessen /search.", + aiNavSearchInstead: "Stattdessen suchen", modulesAINavLabel: "AINav", modulesAINavDescription: "Modul für natürlichsprachliche Anfragen zum Inhalt des Netzwerks.", + directConnect: "Direkte Verbindung", directConnectDescription: "Verbinde dich direkt mit einem Peer, indem du IP-Adresse, Port und öffentlichen Schlüssel eingibst.", peerHost: "IP", peerPort: "Port (Standard: 8008)", peerPublicKey: "Öffentlicher Schlüssel (@...ed25519)", connectAndFollow: "Verbinden", + deviceSourceLabel: "Gerätetyp", modulesPresetTitle: "Gängige Konfigurationen", - workflowsTitle: "Workflows", - workflowsDescription: "Ein Workflow legt das Thema und die aktiven Module fest.", - workflowsSet: "Workflow anwenden", - workflow_default: "Standard", - workflow_jobs: "Arbeit", - workflow_ruling: "Regierung", - workflow_politics: "Politik", - workflow_social: "Sozial", - workflow_business: "Geschäft", - workflow_night: "Nacht", - workflow_mobile: "Mobil", + modulesPreset_minimal: "Minimal", + modulesPreset_basic: "Basis", + modulesPreset_social: "Sozial", + modulesPreset_economy: "Wirtschaft", + modulesPreset_full: "Vollständig", statsCarbonFootprintTitle: "CO2-Fußabdruck", + statsCarbonFootprintNetwork: "Netzwerk-CO2-Fußabdruck", + statsCarbonFootprintYours: "Dein CO2-Fußabdruck", statsCarbonTombstone: "Tombstoning-Fußabdruck", + feedSuccessMsg: "Feed erfolgreich veröffentlicht!", + dominantOpinionLabel: "Vorherrschende Meinung", uploadMedia: "Medien hochladen (max: 50MB)", cipherPasswordDecryptLabel: "Passwort (zum Entschlüsseln)", courtsRespondentInvalid: "Ungültiger Beklagter", @@ -3007,13 +3164,14 @@ module.exports = { mapDescriptionLabel: "Beschreibung", mapDescriptionPlaceholder: "Beschreiben Sie die Karte oder den Standort...", mapTypeLabel: "Kartentyp", - mapInviteMode: "Einladung", - mapTypeSingle: "EINZELN", - mapTypeOpen: "OFFEN", - mapTypeClosed: "GESCHLOSSEN", + mapTypeSingle: "EINZELN (nur initialer Standort)", + mapTypeOpen: "OFFEN (jeder kann Markierungen hinzufügen)", + mapTypeClosed: "GESCHLOSSEN (nur Ersteller fügt Markierungen hinzu)", mapTagsLabel: "Tags", mapTagsPlaceholder: "Tags durch Kommas getrennt eingeben", mapUrlLabel: "Karten-URL", + mapPickCoordLabel: "Oder wählen Sie einen Standort auf dem Raster:", + mapMarkersLabel: "Markierungen", mapMarkersTitle: "Markierungen", mapMarkerDefault: "Markierung", mapMarkerLatLabel: "Markierung Breitengrad", @@ -3040,9 +3198,14 @@ module.exports = { padTitle: "Pad", modulesPadsLabel: "Pads", modulesPadsDescription: "Modul zur Verwaltung kollaborativer Texteditoren.", + padFilterAll: "ALLE", + padFilterMine: "MEINE", + padFilterRecent: "AKTUELL", + padFilterOpen: "OFFEN", + padFilterClosed: "GESCHLOSSEN", padCreate: "Pad Erstellen", - padUpdate: "Aktualisieren", - padDelete: "Löschen", + padUpdate: "Pad Aktualisieren", + padDelete: "Pad Löschen", padTitleLabel: "Titel", padTitlePlaceholder: "Pad-Titel eingeben...", padStatusLabel: "Status", @@ -3053,7 +3216,14 @@ module.exports = { padTagsLabel: "Tags", padTagsPlaceholder: "tag1, tag2, ...", padMembersLabel: "Mitglieder", + padVisitPad: "Pad Besuchen", + padShareUrl: "URL Teilen", padCreated: "Erstellt", + padAuthor: "Autor", + padGenerateCode: "Code Generieren", + padInviteCodeLabel: "Einladungscode", + padInviteCodePlaceholder: "Einladungscode eingeben...", + padValidateInvite: "Validieren", padStartEditing: "BEARBEITUNG STARTEN!", padEditorPlaceholder: "Schreiben beginnen...", padSubmitEntry: "Senden", @@ -3066,17 +3236,19 @@ module.exports = { padClosedSectionTitle: "Geschlossene Pads", padCreateSectionTitle: "Neues Pad Erstellen", padUpdateSectionTitle: "Pad Aktualisieren", + padInviteGenerated: "Einladungscode Generiert", typePad: "PADS", padNew: "NEU", padAddFavorite: "Zu Favoriten Hinzufügen", padRemoveFavorite: "Aus Favoriten Entfernen", - padClose: "Schließen", + padClose: "Pad schließen", padBackToEditor: "Zurück zum Editor", padSearchPlaceholder: "Pads suchen...", gamesTitle: "Spiele", gamesDescription: "Entdecke und spiele einige Spiele.", gamesFilterAll: "ALLE", gamesPlayButton: "SPIELEN!", + gamesBackToGames: "Zurück zu Spielen", modulesGamesLabel: "Spiele", modulesGamesDescription: "Modul zum Entdecken und Spielen von Spielen.", modulesGraphosLabel: "Graphos", @@ -3093,6 +3265,8 @@ module.exports = { gamesArkanoidDesc: "Brecht alle Steine mit eurem Schläger und Ball. Ein klassisches Arcade-Spiel.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Klassisches Ping-Pong gegen eine KI. Wer zuerst 5 Punkte hat, gewinnt.", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "Rennen gegen die Zeit! Weiche dem Verkehr aus und erreiche das Ziel rechtzeitig.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Steuere dein Raumschiff durch ein tödliches Asteroidenfeld. Schieße sie ab, bevor sie dich treffen.", gamesRockPaperScissorsTitle: "Schere Stein Papier", @@ -3113,6 +3287,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3120,6 +3296,7 @@ module.exports = { gamesNoScores: "Noch keine Ergebnisse.", modulesChatsLabel: "Chats", modulesChatsDescription: "Modul zum Entdecken und Verwalten verschlüsselter Chats.", + chatMessageLabel: "CHATNACHRICHTEN", chatsTitle: "Chats", chatMineSectionTitle: "Meine Chats", chatRecentTitle: "Aktuelle Chats", @@ -3129,51 +3306,58 @@ module.exports = { chatDescription: "Beschreibung", chatCategory: "Kategorie", chatStatus: "STATUS", + chatFilterAll: "ALLE", + chatFilterMine: "MEINE", + chatFilterRecent: "AKTUELL", + chatFilterFavorites: "FAVORITEN", + chatFilterOpen: "OFFEN", + chatFilterClosed: "GESCHLOSSEN", chatCreate: "Chat Erstellen", - chatUpdate: "Aktualisieren", - chatDelete: "Löschen", + chatUpdate: "Chat Aktualisieren", + chatDelete: "Chat Löschen", chatClose: "Chat Schließen", chatVisitChat: "CHAT BESUCHEN", chatUntitled: "Unbenannter Chat", chatNoItems: "Keine Chats gefunden.", chatParticipants: "Teilnehmer", - chatMessagesLabel: "Nachrichten", - chatThreadsLabel: "Threads", + chatStartChatting: "CHAT STARTEN!", + chatGenerateCode: "Code Generieren", + chatShareUrl: "URL Teilen", chatCreatedAt: "ERSTELLT", chatSearchPlaceholder: "Chats suchen...", chatStatusOpen: "OFFEN", chatStatusInviteOnly: "NUR AUF EINLADUNG", chatStatusClosed: "GESCHLOSSEN", chatSendMessage: "Senden", - chatJumpLatest: "Neueste Nachricht", - chatPickChat: "Wähle einen Chat, um ihn zu öffnen", - chatNoneYet: "Noch kein Chat verfügbar.", - activitySearchPlaceholder: "In der Aktivität suchen...", - trendingSearchPlaceholder: "In den Trends suchen...", - opinionsSearchPlaceholder: "In den Meinungen suchen...", - votesSearchPlaceholder: "In den Abstimmungen suchen...", - welcomeStepUxTitle: "Wähle deine Oberfläche", - welcomeStepUxText: "Entscheide, wie Oasis aussieht. Später in den Einstellungen änderbar.", - welcomeStepUxAction: "Diese Ansicht verwenden", - chatRateLimitTitle: "Zu viele Nachrichten", - chatRateLimitMessage: "Du hast das Nachrichtenlimit dieses Raums pro Stunde erreicht. Versuche es später erneut.", chatMessagePlaceholder: "Nachricht eingeben...", chatNoMessages: "Noch keine Nachrichten.", + chatLeave: "Chat Verlassen", + chatInviteCodeLabel: "Einladungscode eingeben", + chatJoinByInvite: "Beitreten", chatTitlePlaceholder: "Chat-Titel", chatDescriptionPlaceholder: "Chat-Beschreibung", chatTagsPlaceholder: "tag1, tag2, tag3", chatImageLabel: "Bilddatei auswählen (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "Einladungscode", chatAuthor: "Autor", + chatCreated: "Erstellt", chatAddFavorite: "Zu Favoriten Hinzufügen", chatRemoveFavorite: "Aus Favoriten Entfernen", chatPM: "PM", chatStatusLabel: "Status", chatCategoryLabel: "Kategorie", + chatParticipantsLabel: "Teilnehmer", calendarsTitle: "Kalender", calendarTitle: "Kalender", modulesCalendarsLabel: "Kalender", modulesCalendarsDescription: "Modul zum Entdecken und Verwalten von Kalendern.", typeCalendar: "KALENDER", + calendarFilterAll: "ALLE", + calendarFilterMine: "MEINE", + calendarFilterOpen: "OFFEN", + calendarFilterClosed: "GESCHLOSSEN", + calendarFilterRecent: "KÜRZLICH", + calendarFilterFavorites: "FAVORITEN", calendarCreate: "Kalender erstellen", calendarUpdate: "Aktualisieren", calendarDelete: "Löschen", @@ -3186,17 +3370,24 @@ module.exports = { calendarTagsLabel: "Tags", calendarTagsPlaceholder: "tag1, tag2...", calendarParticipantsLabel: "Teilnehmer", + calendarParticipantsCount: "Teilnehmer", + calendarVisitCalendar: "Kalender besuchen", calendarCreated: "Erstellt", + calendarAuthor: "Autor", calendarJoin: "Beitreten", calendarGenerateInvite: "Einladung erstellen", + calendarInviteCodePlaceholder: "Einladungscode eingeben...", + calendarValidateInvite: "Code prüfen", + calendarJoined: "Beigetreten", + calendarAddDate: "Datum hinzufügen", + calendarAddNote: "Notiz hinzufügen", calendarDateLabel: "Datum", calendarDatePlaceholder: "Dieses Datum beschreiben...", - calendarNoteLabel: "Notizen", + calendarNoteLabel: "Notiz", calendarNotePlaceholder: "Notiz hinzufügen...", calendarFirstDateLabel: "Datum", calendarFirstNoteLabel: "Notizen", calendarIntervalLabel: "Interval", - calendarIntervalNone: "Keine Wiederholung", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3207,6 +3398,8 @@ module.exports = { calendarsDescription: "Entdecke und verwalte Kalender in deinem Netzwerk.", calendarMonthPrev: "← Zurück", calendarMonthNext: "Weiter →", + calendarMonthLabel: "Daten", + calendarsShareUrl: "Teilen-URL", calendarAllSectionTitle: "Kalender", calendarRecentSectionTitle: "Kürzliche Kalender", calendarFavoritesSectionTitle: "Favoriten", @@ -3220,6 +3413,7 @@ module.exports = { calendarRemoveFavorite: "Aus Favoriten entfernen", calendarSearchPlaceholder: "Kalender suchen...", calendarAddEntry: "Eintrag hinzufügen", + calendarLeave: "Kalender verlassen", statsCalendar: "Kalender", statsCalendarDate: "Kalenderdaten", statsCalendarNote: "Kalendernotizen", @@ -3266,6 +3460,7 @@ module.exports = { torrentSortOldest: "Älteste", torrentSortTop: "Meistgewählt", torrentNoMatch: "Keine passenden Torrents.", + torrentMessageAuthorButton: "Nachricht an Autor", torrentUpdatedAt: "Aktualisiert", statsTorrent: "Torrents", typeTorrent: "TORRENTS", @@ -3273,18 +3468,10 @@ module.exports = { modulesTorrentsDescription: "Modul zum Entdecken und Verwalten von Torrents.", favoritesFilterTorrents: "TORRENTS", favoritesFilterMarket: "MARKT", - favoritesFilterEvents: "EREIGNISSE", - favoritesFilterForum: "FORUM", - favoritesFilterHousing: "WOHNEN", - favoritesFilterJobs: "JOBS", - favoritesFilterProjects: "PROJEKTE", - favoritesFilterReports: "BERICHTE", - favoritesFilterTasks: "AUFGABEN", - favoritesFilterTransfers: "ÜBERWEISUNGEN", - favoritesFilterVotes: "ABSTIMMUNGEN", favoritesFilterShopProducts: "SHOP-ARTIKEL", tribeSectionTorrents: "TORRENTS", tribeCreateTorrent: "Torrent Hochladen", + tribeMediaTypeTorrent: "Torrent", settingsWishTitle: "Wunsch", settingsWishDesc: "Konfiguriere den Wunsch deines Avatars.", settingsWishWhole: "Multiverse", @@ -3295,10 +3482,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "Ausgang ist ein UX-Schutz. Eingang ist ein Aufmerksamkeitsfilter.", + pmBlockedNonMutual: "Du kannst PNs nur an Bewohner mit gegenseitiger Unterstützung senden.", inhabitantsPendingFollowsTitle: "Ausstehende Unterstützungsanfragen", inhabitantsPendingAccept: "Akzeptieren", inhabitantsPendingReject: "Ablehnen", + bxEncrypted: "VERSCHLÜSSELT", + bxEncryptedHexLabel: "Chiffretext (Vorschau)", tribeSectionGovernance: "GOVERNANCE", + tribeSubStatusPublic: "ÖFFENTLICH", + tribeSubStatusPrivate: "PRIVAT", + tribeSubInheritedPrivate: "PRIVAT (vom Haupt-Stamm geerbt)", tribePadInviteRequired: "Kein Zugriff auf das Pad. Bitte um Einladung.", tribeChatInviteRequired: "Kein Zugriff auf den Chat. Bitte um Einladung.", tribeGovernanceDesc: "Interne Governance dieses Stammes.", @@ -3338,31 +3531,44 @@ module.exports = { tribeGovNoHistorical: "Noch kein Eintrag", logsTitle: "Logbuch", logsDescription: "Erfasse deine Erfahrung im Netzwerk.", + logsReadMore: "Mehr lesen", logsViewTitle: "Logbuch", logsColumnType: "Typ", logsView: "Ansehen", + logsManualPrompt: "Schreibe dein Logbuch", logsUpdateButton: "Aktualisieren", - logsEditTitle: "Log aktualisieren", + logsEditTitle: "Eintrag bearbeiten", + logsCreateDescription: "Erfasse deine Erfahrung...", logsCreateTitle: "Eintrag erstellen", logsWriteButton: "Schreiben", logsTextPlaceholder: "Beschreibe deine Erfahrungen...", + logsTextField: "Text", + logsLabelPlaceholder: "Bezeichnung...", + logsLabelField: "Schreibe dein Logbuch (Bezeichnung)", + logsAiDisabledWarn: "Aktiviere das KI-Modul in /modules, um KI-Einträge zu verwenden.", + logsAiContextValue: "Blockchain-Tag", + logsAiContext: "Kontext", + logsAiModStatus: "AImod", + logsModeApply: "Anwenden!", logsModeAIWritten: "AI-Assistant", logsModeManual: "Manuell", logsModeAI: "KI", + logsColumnDelete: "Löschen", + logsColumnEdit: "Bearbeiten", logsColumnLog: "Logbuch", logsColumnDate: "Datum", logsDelete: "Löschen", - logsEdit: "Aktualisieren", + logsEdit: "Bearbeiten", logsFilterAlways: "IMMER", logsFilterYear: "LETZTES JAHR", logsFilterMonth: "LETZTER MONAT", logsFilterWeek: "LETZTE WOCHE", logsFilterToday: "HEUTE", - logsComments: "Kommentare", - logsAuthorEntries: "Einträge", + logsFilterRecent: "AKTUELL", + logsFilterAll: "ALLE", logsCreate: "Eintrag erstellen", logsExport: "Logbuch exportieren", - logsExportOne: "Log exportieren", + logsExportOne: "Exportieren", logsViewDetails: "Details ansehen", logsGenerateButton: "Text generieren", logsSearchText: "In Logbüchern suchen...", @@ -3372,25 +3578,31 @@ module.exports = { modulesLogsLabel: "Logbuch", modulesLogsDescription: "Modul, um (über einen KI-Assistenten) deine Erfahrungen aufzuzeichnen.", statsLogsTitle: "Logbuch", + statsLogsEntries: "Einträge", typeLog: "LOGBUCH", + blockchainCycle: "Zyklus", larpTitle: "L.A.R.P.", larpDescription: "Eine Live-Rollenspiel-Schicht für kollaboratives Experimentieren.", - larpNoHousesMatch: "Keine Häuser entsprechen deiner Suche.", - larpSearchPlaceholder: "Häuser durchsuchen...", larpAllHouses: "Häuser:", larpFilterRuling: "REGIERT", larpFilterHouses: "HÄUSER", larpFilterRules: "FAQ", larpRulesTitle: "L.A.R.P.-FAQ", + larpRulesEntryTitle: "Starthaus", larpRulesEntryText: "Jede Bewohnerin beginnt standardmäßig in ACADEMIA. Von ACADEMIA aus wählst du ein Haus im Panel \"Haus beitreten\": dadurch öffnet sich seine Eintrittsprüfung. Beantworte die 10 Fragen und sende sie ab. Wenn du die Schwelle (7/10) erreichst, wirst du automatisch in das Haus aufgenommen; andernfalls wirst du abgelehnt und bleibst in ACADEMIA. In beiden Fällen speichert das System deine OasisID und den Zyklus des Versuchs und verhindert weitere Versuche, bis die Abklingzeit von 30 Zyklen abgelaufen ist.", + larpRulesLeaveText: "Mitglieder eines beliebigen Hauses können jederzeit von ihrer Hausseite zu ACADEMIA zurückkehren; dies setzt ihre Mitgliedschaft zurück, hebt aber die Test-Abklingzeit nicht auf.", + larpRulesGovernanceTitle: "Regierungswand", larpRulesGovernanceText: "In ihrem Zyklus trägt das regierende Haus das \"Regiert\"-Abzeichen und ist das einzige, dessen Wand öffentlich unter dem REGIERT-Tab erscheint.", - larpRulesWallText: "Jedes Haus hat eine Wand, auf der seine Mitglieder schreiben. Die Wand eines Hauses ist privat, außer der des regierenden Hauses und der der ACADEMIA, die alle lesen können.", + larpRulesSignTitle: "L.A.R.P.-Emblem", larpRulesSignText: "Bewohner können dies aktivieren (unter /profile/edit > Sensoren), um das Bild ihres aktuellen Hauses als Siegel auf ihrem Profil, in der Autoren- und Bewohnerkarte anzuzeigen. Das Siegel verlinkt zur Hausseite.", larpPostsTitle: "Wand", larpPostsEmpty: "Noch keine Beiträge.", + larpPostLabel: "Neuer Beitrag", larpPostPlaceholder: "Was hat dieses Haus zu sagen?", + larpPostPublic: "Öffentlich (für alle sichtbar, sonst nur für Mitglieder)", larpPostSubmit: "Veröffentlichen", + larpPostMembersOnly: "Nur Mitglieder", larpCycleLabel: "Zyklus:", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", @@ -3427,9 +3639,12 @@ module.exports = { bankTaxesLookupNote: "Gib eine Block-ID ein, um sie in deinem lokalen Feed nachzuschlagen.", bankTaxesUserNoteChipsClick: "Klicke auf einen Chip, um seinen Block im Blockexplorer zu inspizieren.", mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntry: "Einladungscode", larpRulesInviteEntryText: "Hausmitglieder können Einladungscodes erzeugen, die direkten Zugang zum Haus gewähren.", larpRulesOneHouseText: "Du kannst zu jedem Zeitpunkt höchstens einem Haus angehören. Gehörst du keinem an, bist du in ACADEMIA. Jede Nicht-ACADEMIA-Hausseite zeigt seinen Mitgliedern eine Schaltfläche „Haus verlassen\", die die Mitgliedschaft zurück nach ACADEMIA setzt. Das Verlassen hebt die Test-Abklingzeit NICHT auf.", - larpRulesTestEntryText: "Im WILL-Test gewichtet jede Option ein oder mehrere Häuser; beim Absenden zählt das System die Punkte und nimmt dich automatisch im Haus mit der höchsten Punktzahl auf.", + larpRulesOneHouseTitle: "Ein Haus auf einmal", + larpRulesTestEntry: "WILL-Test", + larpRulesTestEntryText: "Jede Option gewichtet ein oder mehrere Häuser; beim Absenden zählt das System die Punkte und nimmt dich automatisch in das Haus mit der höchsten Punktzahl auf.", larpWillTestHint: "Nach Abschluss wirst du dem Haus zugewiesen, das am besten zu deinen Antworten passt. Deine einzelnen Antworten werden lokal verarbeitet und nie gespeichert — nur die resultierende Hauszuweisung wird in deinem Feed veröffentlicht.", larpWillTestTitle: "WILL-Test", settingsLanDesc: "Diesen Peer periodisch an andere Oasis-Instanzen im selben lokalen Netzwerk ankündigen. Deaktivieren, um UDP-Broadcasts zu stoppen.", @@ -3448,30 +3663,36 @@ module.exports = { aiExchangeMarkHelpful: "+1 hilfreich", larpCyclesUntilRuling: "Nächster Regierungszyklus", larpVisitTribe: "Stamm besuchen", - larpStartJourney: "Reise beginnen", larpGoverning: "Regiert:", - larpStartHere: "Fang hier an:", larpMyHouse: "Mein Haus:", larpMembersCount: "Mitglieder", + larpMembersTitle: "Mitglieder", + larpNoMembers: "Noch keine Mitglieder.", larpRolesLabel: "Rollen", larpFunctionLabel: "Funktion", larpMonthLabel: "Regierungszyklus", larpLeaveToAcademia: "Zurück zu ACADEMIA", - larpAcademiaJoinTitle: "L.A.R.P.-Häuser", + larpAcademiaJoinTitle: "Haus beitreten", + larpAcademiaJoinHint: "Lege den psychologischen Profiltest ab, um dem Haus zugewiesen zu werden, das am besten zu dir passt, oder löse einen Einladungscode ein, den dir ein Mitglied eines Hauses gegeben hat.", + larpTakeTestButton: "Profiltest ablegen", larpInviteRedeem: "Haus beitreten", larpInviteCreate: "Einladungscode erzeugen", larpInvitePlaceholder: "Einladungscode", larpInviteBannerTitle: "Neuer Einladungscode", larpInviteBannerHint: "Teile diesen Code mit jemandem in ACADEMIA. Er läuft in 30 Zyklen oder nach einmaliger Nutzung ab.", + larpTestAssignedHouse: "Zugewiesenes Haus", larpTestRankingTitle: "Punktzahl nach Haus", invitesHousesTitle: "L.A.R.P.", invitesHouseInviteCodePlaceholder: "Haus-Einladungscode", invitesHouseJoinButton: "Haus beitreten", ecoTaxLabel: "ECO Tax", + ecoinTaxLabel: "ECOin Tax", bankTaxes: "Steuern", bankOverviewYourTaxes: "Deine Steuern", + bankTaxesNetworkTitle: "Netzwerksteuern", bankTaxesEcoTaxTitle: "ECO Tax", bankTaxesArchTaxTitle: "ARCH Tax", + bankTaxesUserTitle: "Deine Steuern", bankTaxesUserAmount: "Deine ECO Tax (zu zahlender Beitrag)", bankTaxesArchTaxUserAmount: "Deine ARCH Tax (zu zahlender Beitrag)", bankTaxesArchTaxFirstBlock: "Alter deines ersten Blocks", @@ -3522,13 +3743,16 @@ module.exports = { bankRulesEcoTaxRate: "Rate", bankRulesArchTaxRate: "Rate", bankRulesCarbonFactor: "Kohlenstofffaktor", + statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "Noch keine Tombstones im Netzwerk.", blockchainBlockNotAvailable: "Dieser Block ist in deinem lokalen Feed nicht verfügbar. Er gehört möglicherweise einem Peer, von dem du noch nicht replizierst.", blockchainBackToBlockexplorer: "Zurück zum Blockexplorer", audioFilterBcs: "BCS", audioTranscodeButton: "TRANSCODE", + audioTranscodeTitle: "BCS-Transcode", audioTranscodeDetailTitle: "Transcode", audioTranscodeDetailDescription: "Entschlüssle die eingebettete Nutzlast und die ursprüngliche Blockchain-Kompositionskarte.", + audioTranscodeStegoTitle: "In die Wellenform eingebettet", audioTranscodeStegoOasisId: "Von", audioTranscodeStegoTimestamp: "Erzeugt am", audioTranscodeStegoMessage: "TEXT", @@ -3536,7 +3760,9 @@ module.exports = { audioTranscodeStegoUnknown: "—", audioTranscodeStegoNotFound: "Aus diesem Audio konnte keine steganographische Nutzlast entschlüsselt werden.", audioTranscodeCompositionEmpty: "Dieses Audio enthält keine gespeicherte Blockchain-Komposition.", + audioBackToAudio: "Zurück zum Audio", audioBackToBcs: "Zurück zu BCS", + audioAuthorLabel: "Autor", melodyCompositionTitle: "Blockchain-Kompositionskarte", melodyFilterMine: "MEINS", melodyByLabel: "Von", @@ -3550,6 +3776,7 @@ module.exports = { melodyUploadBcsNameNote: "Das Audio wird unter dem automatisch generierten Namen veröffentlicht", larpLastAttemptTitle: "Dein letzter Versuch", larpLastAttemptHouse: "Haus", + larpLastAttemptResult: "Ergebnis", larpLastAttemptWhen: "Wann", larpLastAttemptCooldown: "Nächster Versuch in", larpTestTitle: "Eintrittsprüfung", @@ -3558,14 +3785,30 @@ module.exports = { larpTestCooldownActive: "Nächster Test verfügbar in", larpTestCooldownDays: "Zyklen", larpTestResultTitle: "Testergebnis", + larpTestPassed: "Test bestanden — willkommen!", + larpTestFailed: "Test nicht bestanden", larpTestScore: "Punktzahl", larpTestNextAttempt: "Du kannst in 30 Zyklen einen weiteren Test absolvieren.", larpGoToHouse: "Zu deinem Haus", larpBackToAcademia: "Zurück zu ACADEMIA", + larpBackToHouses: "◀ Häuser", larpBadgeYou: "Du", larpBadgeRuling: "Regiert", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Modul für die Live-Rollenspielschicht von Oasis (die 9 Häuser).", + melodyUploadTitlePlaceholder: "Composition title", + melodyUploadDescPlaceholder: "Optional description", + bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", + bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", + bankTaxesExample: "Example", + bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", + audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + mapClickToCreate: "Click on the map to select a location", + mapClickToAddMarker: "Click on the map to add a marker", + chatLabel: "CHATS", + gameScoreLabel: "GAME SCORES", larpProfileQ1: "Wenn du vor einem komplexen Problem stehst, was tust du zuerst?", larpProfileQ1O1: "Mit anderen reden, um Konsens zu finden", larpProfileQ1O2: "Einen Prototyp zum Testen bauen", diff --git a/src/client/assets/translations/oasis_en.js b/src/client/assets/translations/oasis_en.js index 3ad88f5..e14c720 100644 --- a/src/client/assets/translations/oasis_en.js +++ b/src/client/assets/translations/oasis_en.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { en: { + bbAdd: "Add", + bbQuickActions: "Quick actions", + bbPickTitle: "Pin a module", + bbPickDesc: "Choose a module for the + slot in the bottom bar.", + bbPickNone: "None", + bbManage: "Edit", + bbYourBar: "Your bar", + bbRemove: "Remove", + bbFull: "Maximum reached", + bbDone: "Done", + activityChangeFilter: "Change filter", + emptyConnectHint: "Connect to a pub to sync with the network.", + emptyConnectCta: "Connect to a pub", + menuCommunity: "Community", + activityGroupCommunity: "Community & Governance", + activityGroupOrg: "Organization", + activityGroupComm: "Communication", + activityGroupEconomy: "Economy", + activityGroupMedia: "Content & Media", + activityGroupOther: "Others", + languageName: "English", shopOrderStatusPaid: "Payment received", shopOrderStatusReceived: "Received", shopOrderMarkPaid: "Payment received", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "You have no purchases yet.", shopOrderPmSeller: "PM Seller", shopOrderSeller: "Seller", + shopOrderPaymentConcept: "Payment", shopOrderInvoice: "Invoice", shopOrderInvoiceConcept: "Invoice", shopOrderStatusPending: "Pending", shopOrderStatusAccepted: "Accepted", shopOrderStatusRejected: "Rejected", shopOrderStatusShipped: "Shipped", + shopOrderStatusDelivered: "Delivered", shopOrderAccept: "Accept", shopOrderReject: "Reject", shopOrderMarkShipped: "Mark shipped", + shopOrderMarkDelivered: "Mark delivered", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "Smart-contract", shopOrderContractConcept: "Shop order", shopOrdersPending: "Pending orders", all: "All", @@ -50,13 +75,14 @@ module.exports = { viewJson: "View JSON", matchScore: "Match score", preferencesLabel: "Preferences", - inhabitantProfileTitle: "Inhabitant", + inhabitantProfileTitle: "Inhabitant profile", contentWarningLabel: "Content warning", invalidPost: "Invalid content", invalidPostHint: "This message has invalid/empty text.", spreadBy: "By", spreadChron: "Spread", spreaded: "Spread", + spreadMore: "more", spreadContentUnavailable: "Content not yet available (pending replication)", filteredByTag: "Filtered by tag", filteredByTagTitle: "Filtered by tag", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "No severity", calendarDeleteDate: "Delete Date", calendarIntervalUntil: "Until", + bookmarkCategory: "Category", bookmarkLastVisit: "Last visit", profileClearnetBookmarksLabel: "Bookmarks", + forumFilterHot: "Hot", invitesPort: "Port", currentlyUnrecheable: "ERROR!", peerHostValidation: "Valid IPv4 (e.g. 192.168.1.100) or hostname (e.g. pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "Annual max estimate", statsCarbonNetwork: "Network total", statsCarbonOfEstMax: "of estimated max capacity", - statsCarbonYearProgress: "of the year elapsed", - statsNetworkTitle: "Network", - statsStorageTitle: "Storage", - statsEcoTaxTitle: "ECO Tax", - statsAccountTitle: "Account", - statsAveragesTitle: "Averages", statsCarbonOfNetwork: "of network total", statsCarbonUser: "Your footprint", statsContentCountColumn: "Count", @@ -154,6 +176,10 @@ module.exports = { statsTopTagsTitle: "Top Tags", statsTopTypesTitle: "Top Content Types", statsTotalMsgs: "Total messages", + feedViewDetails: "View details", + eventFileLabel: "Attach image", + taskFileLabel: "Attach image", + courtsRespondentRequired: "Respondent ID is required", extended: "Multiverse", extendedDescription: [ "When you support someone you may download posts from the inhabitants they support, and those posts show up here, sorted by recency.", @@ -197,27 +223,36 @@ module.exports = { graphosShowing: "Showing", graphosYou: "You", graphosTotalNodes: "Total nodes", + manualMode: "Manual Mode", mentions: "Mentions", mentionsDescription: [ - strong("Every message that @mention you"), - ".", + strong("Posts that @mention you"), + ", sorted by recency.", ], + nextPage: "Next", + previousPage: "Previous", noMentions: "You haven't received @mentions, yet.", - mentionsSearchPlaceholder: "Search mentions...", privateReminders: "REMINDERS", inboxFiltersLabel: "Filters:", inboxToggleToMutuals: "Switch to mutuals", inboxToggleToWhole: "Switch to whole", + privateFrom: "From", + privateTo: "To", privateDate: "Date", pmReply: "Reply", pmReplies: "replies", + pmNew: "new", + pmMarkRead: "Mark as read", inReplyTo: "IN REPLY TO", pmPreview: "Preview", pmPreviewTitle: "Message preview", peers: "Peers", searchDescription: "Description", + imageSearch: "Image Search", searchFromLabel: "From", searchToLabel: "To", + searchMinOpinionsLabel: "Min opinions", + searchInhabitantLabel: "Inhabitant (Oasis ID)", searchQueryLabel: "Query", searchPerPageLabel: "Results per page", settings: "Settings", @@ -230,12 +265,26 @@ module.exports = { melodyDescription: 'Play the melody of your blockchain — each block becomes a note.', melodyEmpty: 'No notes yet — your blockchain has not produced any block.', melodyCompositionTitle: 'Blockchain Composition Map', + melodyCompositionHelp: 'Each block of your blockchain becomes a musical note based on its type and size.', + melodyStatsTitle: 'Type breakdown', + melodyInhabitantLabel: 'Inhabitant', melodyTotalBlocks: 'Notes', + melodyType: 'Type', + melodyCount: 'Blocks', melodyFilterAll: 'ALL', + melodyFilterShare: 'Upload Melody', + melodyShareTitle: 'Share your melody', + melodyShareHelp: 'Publish a snapshot of your current melody so others can listen and remix it.', + melodyShareTitleLabel: 'Title (optional)', + melodyShareTitlePlaceholder: 'A blockchain ghost', + melodyShareSubmit: 'Publish Melody', + melodyNoShared: 'No blockchain melodies yet.', melodyRegenerate: 'Regenerate', melodyDownload: "Download BCS", melodyUploadBcsTitle: "Publish", melodyUploadBcsButton: "Publish", + melodyUploadTitlePlaceholder: "Composition title", + melodyUploadDescPlaceholder: "Optional description", melodyUploadStegoLabel: "Hidden message (steganography)", melodyUploadStegoPlaceholder: "Optional.", melodyUploadStegoMaxLabel: "max 280 characters", @@ -246,34 +295,51 @@ module.exports = { melodyFilterMine: "MINE", melodyByLabel: "By", melodyAllEmpty: "No BCS compositions from other inhabitants yet.", + profileActivityTitle: 'Activity', + profileActivityMore: 'View all activity', profileContentSectionTitle: 'Avatar Content', profileContentHelp: 'Choose which of your modules will be displayed on your avatar.', profileSensorsHelp: 'Optional metrics shown on your avatar.', profileClearnetHelp: 'Modules that can be accessed from outside Oasis.', profileHubAll: 'All', + profileHubTitle: 'Public Content', + footerPulseTitle: 'Pulse', + footerPulsePeers: 'Peers', + footerPulseInbox: 'Inbox', + footerPulseSync: 'Last Sync', + footerPulseEcoValue: 'ECO/h', + footerPulseLastActivity: 'Last Activity', footerLicenseLabel: 'License', profileSwitchToOasis: 'Return to Oasis', - profileVisibilityFediverse: "Multiverse", + profileSwitchToClearnet: 'Dive into Clearnet', + profileVisibilityFediverse: "Fediverse", + profileSwitchToFediverse: "Dive into Fediverse", + coordLabel: 'Coordinate (e.g., A3)', + coordPlaceholder: 'Enter coordinate', contributorsTitle: "Contributors", + pixeliaBy: "by", colorLabel: 'Pick a color', paintButton: 'Paint it!', + invalidCoordinate: 'Incorrect coordinate', + goToMuralButton: "View Mural", totalPixels: 'Total Pixels', modules: "Modules", modulesViewTitle: "Modules", modulesViewDescription: "Set your environment by enabling or disabling modules.", inbox: "Inbox", multiverse: "Multiverse", - fediverse: "Multiverse", - fediverseDescription: "Manage your other multiverse accounts, including sending and receiving content.", + fediverse: "Fediverse", + fediverseDescription: "Manage your other fediverse accounts, including sending and receiving content.", fediverseStatus: "Status", - fediverseDisconnected: "Multiverse disconnected. Check %LINK% or connection status.", - fediverseSettingsTitle: "Multiverse", + fediverseDisconnected: "Fediverse disconnected. Check %LINK% or connection status.", + fediverseSettingsTitle: "Fediverse", fediverseInstanceLabel: "Address", fediverseTokenLabel: "Access token", fediverseTokenHelp: "Set your access token. This will be used to manage your Mastodon account from inside Oasis.", fediverseConnect: "Connect it", fediverseConnectedAs: "Connected as", fediverseDisconnect: "Disconnect", + fediverseEmpty: "When you connect other fediverse accounts from %LINK%, you can manage them from here.", fediverseEmptyLink: "your settings", fediverseComposePlaceholder: "What's on your mind?", fediverseReplyPlaceholder: "Write your reply…", @@ -282,15 +348,21 @@ module.exports = { fediverseReply: "Reply", fediverseBoosted: "boosted", fediverseNoPosts: "Your timeline is empty.", + fediverseThread: "Thread", fediverseTimeline: "Timelines", + fediverseManageTimeline: "Manage timeline", fediverseFollowers: "Followers", fediverseFollowing: "Following", fediversePosts: "Posts", fediverseJoined: "Joined", + fediverseOverview: "Overview", fediverseRefresh: "Refresh", fediverseWrite: "Write", fediversePreview: "Preview", + fediverseEdit: "Edit", + fediverseOpenFeed: "Visit feed", fediverseManage: "Manage", + fediverseStatusNotConnected: "Not connected", fediverseError: "Could not contact Mastodon.", fediverseErrInstance: "Invalid instance URL.", fediverseErrAuth: "Invalid or expired token.", @@ -301,6 +373,17 @@ module.exports = { fediverseErrPost: "Could not publish.", fediverseErrMedia: "Could not upload the file.", fediverseErrEmpty: "Write something or attach a file.", + popularLabel: "Highlights", + topicsLabel: "Topics", + latestLabel: "Latest", + summariesLabel: "Summaries", + threadsLabel: "Threads", + multiverseLabel: "Multiverse", + inboxLabel: "Inbox", + invitesLabel: "Invites", + walletLabel: "Wallet", + legacyLabel: "Keys", + cipherLabel: "Crypter", bookmarksLabel: "Bookmarks", videosLabel: "Videos", torrentsLabel: "Torrents", @@ -311,14 +394,18 @@ module.exports = { inhabitantsLabel: "Inhabitants", trendingLabel: "Trending", eventsLabel: "Events", + tasksLabel: "Tasks", apply: "Apply", menuPersonal: "Personal", + menuContent: "Content", + menuBlogs: "Blogs", menuGovernance: "Governance", menuOffice: "Office", - menuNetwork: "Community", + menuMultiverse: "Multiverse", + menuNetwork: "Network", menuCreative: "Creative", menuEconomy: "Economy", - menuMedia: "Library", + menuMedia: "Media", menuTools: "Tools", comment: "Comment", subtopic: "Subtopic", @@ -328,6 +415,13 @@ module.exports = { follow: "Support", block: "Block", unblock: "Unblock", + newerPosts: "Newer posts", + olderPosts: "Older posts", + feedRangeEmpty: "The given range is empty for this feed. Try viewing the ", + seeFullFeed: "full feed", + feedEmpty: "The Oasis network has never seen posts from this account.", + beginningOfFeed: "This is the beginning of the feed", + noNewerPosts: "No newer posts have been received yet.", relationshipNotFollowing: "You are not supported", relationshipTheyFollow: "Supports you", relationshipMutuals: "Mutual support", @@ -337,20 +431,25 @@ module.exports = { relationshipBlockedBy: "You are blocked", relationshipMutualBlock: "Mutual block", relationshipNone: "You are not supporting", + relationshipConflict: "Conflict", relationshipBlockingPost: "Blocked post", viewLikes: "View spreads", spreadedDescription: "List of posts spread by the inhabitant.", + totalspreads: "Total spreads", + attachFiles: "Attach files", preview: "Preview", publish: "Write", contentWarningPlaceholder: "Add a subject to the post (optional)", - publishWarningPlaceholder: "Body capped at 7000 characters.", - publishTooLong: "Your post is too long. Please shorten it.", + privateWarningPlaceholder: "Add inhabitants to send a private post (optional)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.", ], commentWarning: [ "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.", ], + commentPublic: "public", + commentPrivate: "private", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -370,6 +469,7 @@ module.exports = { ".", ], publishCustom: "Write advanced post", + subtopicLabel: "Create a subtopic of this post", messagePreview: "Post Preview", mentionsMatching: "Matching Mentions", mentionsName: "Name", @@ -398,107 +498,38 @@ module.exports = { settingsReplicationHopsLabel: "Hops", settingsLanEnable: "Enable LAN broadcasting", saveSettings: "Save settings", + exportTitle: "Export data", exportDescription: "Set password (min 32 characters long) to encrypt your key", exportDataTitle: "Backup", exportDataDescription: "Download your data (secret key excluded!)", exportDataButton: "Download database", + pubWallet: "PUB Wallet", + pubWalletDescription: "Set the PUB wallet URL. This will be used for PUB transactions (including the UBI).", + pubWalletConfiguration: "Save configuration", + importTitle: "Import data", importDescription: "Import your encrypted secret (private key) to enable your avatar", + importAttach: "Attach encrypted file (.enc)", + passwordLengthInfo: "Password must be at least 32 characters long.", passwordImport: "Write your password to decrypt data that will be saved at your system home (name: secret)", exportPasswordPlaceholder: "Use lowercase, uppercase, numbers & symbols", - fileInfo: "Your encrypted secret key will be downloaded to your device (name: oasis.enc)", - developer: "Developer", - devTitle: "Developer", - welcomeBannerText: "Welcome to OASIS. Follow these quick steps to set up your avatar.", - aiSuggestionBanner: "Suggestion:", - aiSuggestionsEnable: "AI Suggestions", - welcomeBannerAction: "Start wizard!", - welcomeTitle: "Welcome", - welcomeStepGreetingAction: "Send Feed", - welcomeJoinPubButton: "Join Pub", - welcomeStepLarpAction: "Join \"The Academy\"", - welcomeStepLarpText: "Join our live action role playing game.", - welcomeStepLarpTitle: "Join L.A.R.P.", - welcomeStepGreetingTitle: "Send a \"Hello world!\"", - welcomeStepGreetingText: "Send a message so other inhabitants can discover you.", - welcomeJoinPub: "Join", - welcomeDescription: "A few things worth doing before you start.", - welcomeProgress: "Completed", - welcomeStepDone: "done", - welcomeStepPending: "pending", - welcomeSkip: "Skip for now", - welcomeStepLanguageTitle: "Choose your language", - welcomeStepLanguageText: "Oasis speaks different languages. You can change it whenever you want.", - welcomeStepProfileTitle: "Edit your profile", - welcomeStepProfileText: "Pick a name, add a description and set a picture so other inhabitants can recognise you.", - welcomeStepProfileAction: "Save Profile", - welcomeStepFederationTitle: "Join Main Network", - welcomeStepFederationText: "Connect to our pub to meet other inhabitants and start replicating.", - welcomeStepFederationAction: "Go to invites", - welcomeStepBackupTitle: "Backup your ID", - welcomeStepBackupText: "Your identity is a key file on this device.", - welcomeStepBackupWarning: "IF YOU LOSE IT, NOBODY CAN RECOVER IT FOR YOU.", - welcomeStepBackupAction: "Backup!", - welcomeCompleteTitle: "All set.", - welcomeCompleteText: "Your node is ready. From here the network grows with the people you follow.", - welcomeFindPeople: "Find inhabitants", - devFilterFiles: "FILES", - devFilterSearch: "SEARCH", - devFilterModules: "MODULES", - devFilterTranslations: "TRANSLATIONS", - devFilterStyles: "STYLES", - devFilterTests: "TESTS", - devVersion: "Version", - devNodeVersion: "Node", - devStatsTests: "Test suites", - devParentFolder: "Parent folder", - devOpenFolder: "open", - devDescription: "Explore the source code running on this node.", - devRoot: "root", - devSearchPlaceholder: "Text to find in the code", - devAnyExtension: "Any file", - devStatsFiles: "Files", - devStatsLines: "Lines", - devStatsModules: "Modules", - devStatsLanguages: "Languages", - devNotViewable: "not viewable", - devEmptyFolder: "This folder is empty.", - devSize: "Size", - devShowing: "Showing", - devLine: "line", - devReportBug: "Report Bug", - devCreateTask: "Create Task", - devRaw: "RAW", - devReportContext: "Found while reading the Oasis source code", - devTaskPrefix: "Review", - devPrevPage: "Previous lines", - devNextPage: "Next lines", - devMatches: "matches", - devFilesScanned: "files scanned", - devNoResults: "No matches, yet.", - devColModule: "Module", - devColState: "State", - devColModel: "Model", - devColView: "View", - devColRoutes: "Routes", - devColTests: "Tests", - devOn: "on", - devOff: "off", - modulesDevLabel: "Developer", - modulesDevDescription: "Module to manage the Oasis source code.", + fileInfo: "Your encrypted secret key will be saved at your system home (name: oasis.enc)", themeIntro: "Choose a theme.", setTheme: "Set theme", language: "Language", languageDescription: "If you'd like to use another language, select it here.", - setLanguage: "Set Language", + setLanguage: "Set language", + peerConnections: "Peers", peerConnectionsIntro: "Manage all your connections with other peers.", peerConnectionsTitle: "Connections", online: "Online", offline: "Offline", discovered: 'Discovered', unknown: 'Unknown', + lanBroadcastLabel: "LAN broadcast", lanBroadcastActive: "Active (UDP multicast on LAN)", + lanBroadcastDisabled: "Disabled (pub mode)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -511,6 +542,8 @@ module.exports = { noConnections: "No peers connected.", noDiscovered: "No peers discovered.", noUnknownPeers: "No unknown peers in the friend graph.", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -520,6 +553,9 @@ module.exports = { peerImport: "Import", peerImportTitle: "Import peer list", peerImportPlaceholder: "Paste one multiserver address per line, or upload a .txt file…", + noSupportedConnections: "No peers supported.", + noBlockedConnections: "No peers blocked.", + noRecommendedConnections: "No peers recommended.", connectionActionIntro: "", startNetworking: "Start networking", @@ -533,19 +569,9 @@ module.exports = { homePageDescription: "Select which module you want as your home page.", saveHomePage: "Set home", invites: "Invites", + invitesTitle: "Invites", + invitesInvites: "Invitations", invitesDescription: "Manage and apply invite codes in your network.", - invitesPubsHint: "Paste a pub invite code to federate with it and start replicating its inhabitants.", - invitesFederationsHint: "Pubs you are federated with: refresh them, drop the unreachable ones or export the list.", - invitesHousesHint: "Enter a house code to join a LARP house and take part in its economy.", - invitesTribesHint: "Enter a tribe code to become a member and reach its private content.", - invitesChatsHint: "Enter a chat code to join the conversation and its threads.", - invitesPadsHint: "Enter a pad code to write together on a shared document.", - invitesCalendarsHint: "Enter a calendar code to share dates and reminders with others.", - invitesEventsHint: "Enter an event code to join a private event.", - invitesForumsHint: "Enter a forum code to read and reply in a private forum.", - invitesMapsHint: "Enter a map code to add and see places on a shared map.", - invitesShopsHint: "Enter a shop code to join a shop and its catalogue.", - invitesInhabitantsHint: "Enter an inhabitant code to follow each other and exchange content.", invitesTribesTitle: "Tribes", invitesTribeInviteCodePlaceholder: "Enter tribe invite code", invitesTribeJoinButton: "Join Tribe", @@ -576,6 +602,7 @@ module.exports = { invitesAlreadyFederated: "You are already federated with this pub.", invitesFederationsTitle: "Federations", invitesAcceptedInvites: "Federated", + invitesNoInvites: "No invitations accepted, yet.", invitesUnfollow: "Unfollow", invitesFollow: "Follow", invitesUnfollowedInvites: "Unfederated", @@ -595,24 +622,39 @@ module.exports = { panicMode: "Panic Mode!", encryptData: "Set password (min 32 characters long) to encrypt your blockchain", decryptData: "Enter password to decrypt your blockchain", + panicModeDescription: "Encrypt/Decrypt or DELETE your blockchain", removeDataDescription: "WARNING: This process cannot be undone.", + encryptPanicButton: "Encrypt blockchain", + decryptPanicButton: "Decrypt blockchain", removePanicButton: "DELETE ALL YOUR DATA!", searchTitle: "Search", searchDescriptionLabel: "Search for content in your network.", + searchLanguagesLabel: "Languages", + searchSkillsLabel: "Skills", searchPlaceholder:"Search for content...", + searchDateLabel:"Date", searchLocationLabel:"Location", searchPriceLabel:"Price", + searchUrlLabel:"URL", searchCategoryLabel:"Category", + searchStartLabel:"Start Time", + searchEndLabel:"End Time", searchPriorityLabel:"Priority", searchStatusLabel:"Status", statusLabel:"Status", + totalVotesLabel:"Total Votes", noResultsFound:"No results found.", votesOption:"Vote Options", voteYesLabel:"Yes", voteNoLabel:"No", allTypesLabel: "UNLIMITED", + ABSTENTIONLabel:"ABSTENTION", YESLabel:"YES", NOLabel:"NO", + FOLLOW_MAJORITYLabel: "FOLLOW MAJORITY", + CONFUSEDLabel: "CONFUSED", + NOT_INTERESTEDLabel: "NOT INTERESTED", + StatusLabel:"Status", votesOptionYesLabel:"Yes", votesOptionNoLabel:"No", votesOptionAbstentionLabel:"Abstention", @@ -620,6 +662,7 @@ module.exports = { votesCreatedByLabel:"Created by", voteStatusOpen:"OPEN", voteStatusClosed:"CLOSED", + imageSearchLabel: "Enter words to search for images labelled with them.", commentDescription: ({ parentUrl }) => [ " commented on ", a({ href: parentUrl }, " thread"), @@ -630,24 +673,57 @@ module.exports = { a({ href: parentUrl }, " a post"), ], subtopicTitle: ({ authorName }) => [`Subtopic on @${authorName}'s post`], + mysteryDescription: "posted a mysterious post", oasisDescription: "OASIS Project Network", searchSubmit: "Let's Search...", + postLabel: "POSTS", + aboutLabel: "INHABITANTS", + feedLabel: "FEEDS", votesLabel: "VOTATIONS", + reportLabel: "REPORTS", + imageLabel: "IMAGES", + videoLabel: "VIDEOS", + audioLabel: "AUDIOS", + documentLabel: "DOCUMENTS", + torrentLabel: "TORRENTS", pdfFallbackLabel: "PDF Document", + eventLabel: "EVENTS", + taskLabel: "TASKS", + transferLabel: "TRANSFERS", + curriculumLabel: "CURRICULUM", + bookmarkLabel: "BOOKMARKS", + tribeLabel: "TRIBES", + marketLabel: "MARKET", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "WALLETS", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "Submit", + subjectLabel: "Subject", editProfile: "Edit Avatar", + profileQrAlt: "Profile QR code", + profileQrHint: "Scan to copy this Oasis ID.", oasisIdLabel: "OasisID", - spreadContent: "Replicate", + spreadLabel: "Spread", + spreadHint: "Spread this to your supporters (replicates via your feed).", welcomePmSubject: "Hello World!", - welcomePmBody: "Welcome to Oasis — a libre, peer-to-peer, federated and encrypted social network with a distributed invite-only architecture.\n\nSome interesting places to start:\n\n- /profile — Edit your avatar, name, description and which modules appear on your public side.\n- /settings — Tune your local instance: language, theme, modules, privacy and sync.\n- /invites — Add a pub to federate with the wider network.\n- /peers — See who is connected, rescan your LAN, export or import peer lists.\n- /inhabitants — Discover and visit other people's avatars.\n- /tribes — Create or join private groups with end-to-end encryption.\n- /inbox — Read and send private messages.\n- /banking — See your ECO balance, UBI, ECO tax and the wealth-redistribution rules.\n- /activity — See recent activity, yours and your network's.\n- /publish — Write a post or share an image, audio, video or document.\n- /search — Search the network's content by type.\n\nSome interesting places to connect:\n\n- /multiverse — Manage your other multiverse accounts, including sending and receiving content.\n\nThis message was sent privately from you to yourself, so no connection was required to receive it.", + welcomePmBody: "Welcome to Oasis — a libre, peer-to-peer, federated and encrypted social network with a distributed invite-only architecture.\n\nSome interesting places to start:\n\n- /profile — Edit your avatar, name, description and which modules appear on your public side.\n- /settings — Tune your local instance: language, theme, modules, privacy and sync.\n- /invites — Add a pub to federate with the wider network.\n- /peers — See who is connected, rescan your LAN, export or import peer lists.\n- /inhabitants — Discover and visit other people's avatars.\n- /tribes — Create or join private groups with end-to-end encryption.\n- /inbox — Read and send private messages.\n- /banking — See your ECO balance, UBI, ECO tax and the wealth-redistribution rules.\n- /activity — See recent activity, yours and your network's.\n- /publish — Write a post or share an image, audio, video or document.\n\nSome interesting places to connect:\n\n- /fediverse — Manage your other fediverse accounts, including sending and receiving content.\n\nThis message was sent privately from you to yourself, so no connection was required to receive it.", indexingTitle: "Synchronizing", indexingMessage: "Oasis is trying to syncronize a huge network of inhabitants. Just wait!", indexingRefreshNote: "This page refreshes every 10 seconds.", indexingEditProfileLink: "Set up my avatar", + profileVisibilityTitle: "Public profile visibility", + profileVisibilityHint: "Toggle which fields other inhabitants see on your profile. By default only Karma is shown.", profileSensorsSectionTitle: "Sensors", profileVisibilityActivity: "Activity Level", + profileVisibilityDevice: "Device", profileDeviceLockedHint: "This sensor is always on: it lets other users see which device you connect from and cannot be disabled.", profileVisibilityKarma: "KARMA Scoring", profileVisibilityUbi: "UBI", @@ -655,6 +731,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "GPG Key", + profileVisibilityClearnet: "Publish my profile", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Shops", profileClearnetJobsLabel: "Jobs", @@ -708,6 +785,7 @@ module.exports = { " or connection status.", ], walletSentToLine: ({ destination, amount }) => `Sent ECO ${amount} to ${destination}`, + walletSettingsTitle: "Wallet", walletSettingsDescription: "Integrate Oasis with your ECOin wallet.", walletSettingsDocLink: "ECOin installation guide", walletStatusMessages: { @@ -729,19 +807,37 @@ module.exports = { cipherTitle: "Crypter", cipherDescription: "Encrypt and decrypt your text symmetrically (using a shared password).", randomPassword: "Random Password", + cipherEncryptTitle: "Encrypt Text", + cipherEncryptDescription: "Enter text to encrypt", + cipherTextLabel: "Text to Encrypt", cipherTextPlaceholder: "Enter text to encrypt...", cipherPasswordLabel: "Set password (min 32 characters long) to encrypt your text", cipherPasswordDecryptLabel: "Set password (min 32 characters long) to decrypt your text", cipherPasswordPlaceholder: "Enter a password...", cipherEncryptButton: "Encrypt", + cipherDecryptTitle: "Decrypt Text", + cipherDecryptDescription: "Enter text to decrypt", cipherEncryptedMessageLabel: "Encrypted Text", cipherDecryptedMessageLabel: "Decrypted Text", + cipherPasswordUsedLabel: "Password used to encrypt (keep it!)", cipherEncryptedTextPlaceholder: "Enter the encrypted text...", + cipherIvLabel: "IV", + cipherIvPlaceholder: "Enter the initialization vector...", cipherDecryptButton: "Decrypt", password: "Password", text: "Text", encryptedText: "Encrypted Text", iv: "Initialization Vector (IV)", + encryptTitle: "Encrypt your text", + encryptDescription: "Enter the text you want to encrypt and provide a password.", + encryptButton: "Encrypt", + decryptTitle: "Decrypt your text", + decryptDescription: "Enter the encrypted text and provide the same password used for encryption.", + decryptButton: "Decrypt", + passwordLengthError: "Password must be at least 32 characters long.", + missingFieldsError: "Text, password or IV not provided.", + encryptionError: "Error encrypting text.", + decryptionError: "Error decrypting text.", bookmarkTitle: "Bookmarks", bookmarkDescription: "Discover and manage bookmarks in your network.", bookmarkAllSectionTitle: "Bookmarks", @@ -767,6 +863,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "Optional", bookmarkTagsLabel: "Tags", bookmarkTagsPlaceholder: "Enter tags separated by commas", + bookmarkCategoryLabel: "Category", + bookmarkCategoryPlaceholder: "Optional", bookmarkLastVisitLabel: "Last Visit", bookmarkSearchPlaceholder: "Search URL, tags, category, author...", bookmarkSortRecent: "Most recent", @@ -781,6 +879,8 @@ module.exports = { noLastVisit: "No last visit", videoTitle: "Videos", videoDescription: "Explore and manage video content in your network.", + videoPluginTitle: "Title", + videoPluginDescription: "Description", videoMineSectionTitle: "Your Videos", videoCreateSectionTitle: "Upload Video", videoUpdateSectionTitle: "Update Video", @@ -812,6 +912,7 @@ module.exports = { videoSortOldest: "Oldest", videoSortTop: "Most voted", videoSearchButton: "Search", + videoMessageAuthorButton: "PM", videoUpdatedAt: "Updated", videoNoMatch: "No videos match your search.", documentTitle: "Documents", @@ -852,6 +953,8 @@ module.exports = { documentUpdatedAt: "Updated", audioTitle: "Audios", audioDescription: "Explore and manage audio content in your network.", + audioPluginTitle: "Title", + audioPluginDescription: "Description", audioMineSectionTitle: "Your Audios", audioCreateSectionTitle: "Upload Audio", audioUpdateSectionTitle: "Update Audio", @@ -862,12 +965,17 @@ module.exports = { audioFilterMine: "MINE", audioFilterRecent: "RECENT", audioFilterTop: "TOP", + audioFilterBlockchain: "BLOCKCHAIN", audioFilterBcs: "BCS", audioTranscodeButton: "TRANSCODE", audioTranscodeDetailTitle: "Transcode", audioTranscodeDetailDescription: "Decode the embedded payload and the original blockchain composition map.", ecoTaxLabel: "ECO Tax", + ecoinTaxLabel: "ECOin Tax", bankTaxes: "Taxes", + bankTaxesNetworkTitle: "Network Taxes", + bankTaxesUserTitle: "Your taxes", + bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", bankTaxesUserNoteChips: "The ECO Tax chip shown next to each item changes color (green / yellow / red) depending on the item size relative to the largest block ever seen on the network, softened by the number of active inhabitants — a larger solar-punk network distributes the load and pushes more items into the green band.", bankTaxesUserNoteChipsClick: "Click any chip to inspect its block in the blockexplorer.", bankTaxesEcoTaxTitle: "ECO Tax", @@ -880,6 +988,7 @@ module.exports = { bankTaxesArchTaxRate: "Rate", bankTaxesArchTaxFirstBlock: "Your first block age", bankTaxesArchTaxUserAmount: "Your ARCH tax (price to return)", + bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", bankTaxesArchTaxNoteIntro: "Your ARCH tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Newer inhabitants and those specifically dedicated to archival, replication and maintenance tasks will likely have some task dedicated to keeping the network archive healthy on your behalf. Or they may dedicate themselves to ", bankTaxesArchTaxNoteBold: "reducing the ARCH tax directly", bankTaxesArchTaxNoteOutro: " through pruning agreements, deduplication of redundant data, and shared maintenance infrastructure, and their ongoing contribution may be required by networking consensus.", @@ -888,9 +997,10 @@ module.exports = { bankRulesArchTaxRate: "Rate", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", + bankTaxesExample: "Example", bankTaxesUserNoteOtherParams: "ECO Tax is currently derived from the carbon footprint of each block, but it may incorporate other parameters over time — energy spent on replication, redundant storage across peers, blob bandwidth, computational cost of decryption, mining footprint of associated transactions, or any other measurable load the network agrees to value.", bankTaxesUserAmount: "Your ECO tax (price to return)", - bankOverviewYourTaxes: "Your Taxes", + bankOverviewYourTaxes: "Your taxes", bankTaxesTotalBlocks: "Total blocks", bankTaxesTotalBytes: "Total bytes", bankTaxesTotalCarbon: "Total carbon", @@ -901,6 +1011,7 @@ module.exports = { bankTaxesTotalLifetime: "Total (lifetime)", bankTaxesTotalAnnual: "Total (annual)", bankTaxesTotalMonthly: "Total (monthly)", + bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", bankTaxesRate: "Rate", bankTaxesSpan: "Sampling span", bankTaxesLookupTitle: "Inspect a block", @@ -941,7 +1052,11 @@ module.exports = { bankRulesChipTitle: "ECO Tax chip color band", bankRulesChipNote: "Each item's chip is colored relative to the largest message ever observed in the network and softened by the number of active inhabitants — a larger network distributes the load and pushes more items into the green band.", bankRulesChipFormula: "band = ratio / reducer, where ratio = sizeBytes / maxBlockBytes, reducer = 1 + log10(inhabitants). high ≥ 0.66, mid ≥ 0.33, otherwise low.", + statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "No tombstones in the network yet.", + audioTranscodeTitle: "BCS Transcode", + audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", + audioTranscodeStegoTitle: "Embedded in the waveform", audioTranscodeStegoOasisId: "By", audioTranscodeStegoTimestamp: "Generated at", audioTranscodeStegoMessage: "TEXT", @@ -949,7 +1064,9 @@ module.exports = { audioTranscodeStegoUnknown: "—", audioTranscodeStegoNotFound: "No steganographic payload could be decoded from this audio.", audioTranscodeCompositionEmpty: "This audio does not include a stored blockchain composition.", + audioBackToAudio: "Back to audio", audioBackToBcs: "Back to BCS", + audioAuthorLabel: "Author", audioCreateButton: "Upload Audio", audioUpdateButton: "Update", audioDeleteButton: "Delete", @@ -976,7 +1093,6 @@ module.exports = { audioFilterFavorites: "FAVORITES", favoritesTitle: "Favorites", favoritesDescription: "All your favorited content in one place.", - favoritesSearchPlaceholder: "Search favorites...", favoritesFilterAll: "ALL", favoritesFilterRecent: "RECENT", favoritesFilterAudios: "AUDIOS", @@ -992,13 +1108,18 @@ module.exports = { favoritesNoItems: "No favorites yet.", yourContacts: "Your Contacts", allInhabitants: "Inhabitants", - allCVs: "CVs", + allCVs: "All CVs", discoverPeople: "Discover inhabitants in your network.", + allInhabitantsButton: "ALL", + contactsButton: "SUPPORTS", + CVsButton: "CVs", + matchSkills: "Match Skills", + matchSkillsButton: "MATCH SKILLS", + suggestedButton: "SUGGESTED", searchInhabitantsPlaceholder: "FILTER inhabitants BY NAME …", filterLocation: "FILTER inhabitants BY LOCATION …", filterLanguage: "FILTER inhabitants BY LANGUAGE …", filterSkills: "FILTER inhabitants BY SKILLS …", - cvSkillsCount: "Skills", applyFilters: "Apply Filters", locationLabel: "Location", languagesLabel: "Languages", @@ -1007,18 +1128,17 @@ module.exports = { mutualFollowers: "Mutual Followers", suggestedFollowsYou: "Follows you", latestInteractions: "Latest Interactions", + viewAvatar: "View Avatar", + viewCV: "View CV", suggestedSectionTitle: "Suggested", topkarmaSectionTitle: "Top Karma", topecoSectionTitle: "Top Eco", topactivitySectionTitle: "Top Activity", - "TOP INACTIVITYButton": "TOP INACTIVITY", - topinactivitySectionTitle: "Least active inhabitants", blockedSectionTitle: "Blocked", gallerySectionTitle: "GALLERY", - topSectionTitle: "TOP", + blockedButton: "BLOCKED", blockedLabel: "Blocked User", inhabitantviewDetails: "View Details", - cvVisitButton: "Visit CV", keepReading: "Keep reading...", oasisId: "ID", noInhabitantsFound: "No inhabitants found, yet.", @@ -1027,11 +1147,10 @@ module.exports = { deviceLabel: "Device", parliamentTitle: "Parliament", parliamentDescription: "Explore forms of government and collective management laws.", - parliamentFilterGovernment: "GOVERNMENT", + parliamentFilterGovernment: "GOVERMENT", parliamentFilterCandidatures: "CANDIDATURES", parliamentFilterProposals: "PROPOSALS", parliamentFilterLaws: "LAWS", - parliamentFilterRevocations: "REVOCATIONS", parliamentFilterHistorical: "HISTORICAL", parliamentFilterLeaders: "LEADERS", parliamentFilterRules: "RULES", @@ -1057,8 +1176,10 @@ module.exports = { parliamentPoliciesDeclined: "LAWS DECLINED", parliamentPoliciesDiscarded: "LAWS DISCARDED", parliamentEfficiency: "% EFFICIENCY", + parliamentFilterRevocations: 'REVOCATIONS', parliamentRevocationFormTitle: 'Revocate Law', parliamentRevocationLaw: 'Law', + parliamentRevocationTitle: 'Title', parliamentRevocationReasons: 'Reasons', parliamentRevocationPublish: 'Publish Revocation', parliamentCurrentRevocationsTitle: 'Current Revocations', @@ -1071,12 +1192,23 @@ module.exports = { parliamentNoGovernments: "There are not governments, yet.", parliamentCandidatureFormTitle: "Propose Candidature", parliamentCandidatureIdPh: "Oasis ID (@...) or Tribe name", + parliamentCandidatureSlogan: "Slogan (max 140 chars)", + parliamentCandidatureSloganPh: "A short motto", parliamentCandidatureMethod: "Method", parliamentCandidatureProposeBtn: "Publish Candidature", + parliamentThDate: "Proposal date", + parliamentThSlogan: "Slogan", + parliamentThSince: "Profile since", + parliamentThVotes: "Votes recieved", + parliamentThVoteAction: "Vote", + parliamentTypeUser: "Inhabitant", + parliamentTypeTribe: "Tribe", parliamentVoteBtn: "Vote", parliamentProposalFormTitle: "Propose Law", parliamentProposalDescription: "Description (≤1000)", parliamentProposalPublish: "Publish Proposal", + parliamentFinalize: "Finalize", + parliamentDeadline: "Deadline", parliamentNoProposals: "There are not law proposals, yet.", parliamentNoLaws: "There are not laws approved, yet.", parliamentMethodDEMOCRACY: "Democracy", @@ -1094,21 +1226,29 @@ module.exports = { parliamentVotesSlashTotal: "Votes/Total", parliamentVotesNeeded: "Votes Needed", parliamentFutureLawsTitle: "Future Laws", + parliamentNoFutureLaws: "No future laws, yet.", parliamentVoteAction: "Vote", + parliamentLeadersTitle: "Leaders", parliamentThLeader: "AVATAR", parliamentPopulation: "Population", + parliamentThType: "Type", + parliamentThInPower: "In power", parliamentThPresented: "CANDIDATURES", parliamentNoLeaders: "No leaders, yet.", parliamentCandidaturesListTitle: "List of Candidatures", parliamentTimeRemaining: "Time remaining", + parliamentNoLeader: "No winning candidature, yet.", parliamentElectionsStatusTitle: "Next Government", parliamentProposalDeadlineLabel: "Deadline", parliamentProposalTimeLeft: "Time left", + parliamentProposalOnTrack: "On track to pass", + parliamentProposalOffTrack: "Not enough support yet", parliamentRulesTitle: "How Parliament works", parliamentRulesIntro: "Election resolve every 2 months; candidatures are continuous and reset when a government is chosen.", parliamentRulesCandidates: "Any inhabitant may propose themself, another inhabitant, or any tribe. Each inhabitant can propose up to 3 candidatures per cycle; duplicates in the same cycle are rejected.", parliamentRulesElection: "The winner is the candidature with the most votes at resolution time.", parliamentRulesTies: "Tie-break order: highest inhabitant karma; if tied, oldest profile; if still tied, earliest proposal; then lexicographic by ID.", + parliamentRulesFallback: "If no one votes, the latest proposed candidature wins. If there are no candidatures, a random tribe is selected.", parliamentRulesTerm: "The Government tab shows the current government and stats.", parliamentRulesMethods: "Forms: Anarchy (simple majority), Democracy (50%+1), Majority (80%), Minority (20%), Karmatocracy (highest-karma proposals), Dictatorship (instant approval).", parliamentRulesAnarchy: "Anarchy is the default mode: if no candidature is elected at resolution, Anarchy is proclaimed. Under Anarchy, any inhabitant can propose laws.", @@ -1130,7 +1270,11 @@ module.exports = { courtsCaseFormTitle: "Open Case", courtsCaseRespondent: "Accused / Respondent", courtsCaseRespondentPh: "Oasis ID (@...) or Tribe name", + courtsCaseMediatorsAccuser: "Mediators (accuser)", courtsCaseMethod: "Resolution method", + courtsCaseDescription: "Description (max 1000 chars.)", + courtsCaseEvidenceTitle: "Case evidence", + courtsCaseEvidenceHelp: "Attach images, audios, documents (PDF) or videos that support your case.", courtsCaseSubmit: "File Case", courtsNominateJudge: "Nominate Judge", courtsJudgeId: "Judge", @@ -1176,6 +1320,22 @@ module.exports = { courtsRulesMisconduct: "Harassment, manipulation, or fabricated evidence may lead to immediate negative resolution.", courtsRulesGlossary: "Case: record of a conflict. Evidence: materials that support claims. Verdict: decision with remedy. Appeal: request to review the verdict.", courtsFilterActions: "ACTIONS", + courtsNoActions: "No pending actions for your role.", + courtsCaseTitlePlaceholder: "Short description of the conflict", + courtsCaseSeverity: "Severity", + courtsCaseSeverityNone: "No severity tag", + courtsCaseSeverityLOW: "Low", + courtsCaseSeverityMEDIUM: "Medium", + courtsCaseSeverityHIGH: "High", + courtsCaseSeverityCRITICAL: "Critical", + courtsCaseSubject: "Topic", + courtsCaseSubjectNone: "No topic tag", + courtsCaseSubjectBEHAVIOUR: "Behaviour", + courtsCaseSubjectCONTENT: "Content", + courtsCaseSubjectGOVERNANCE: "Governance / rules", + courtsCaseSubjectFINANCIAL: "Financial / resources", + courtsCaseSubjectOTHER: "Other", + courtsHiddenRespondent: "Hidden (only visible to involved roles).", courtsThRole: "Role", courtsRoleAccuser: "Accuser", courtsRoleDefence: "Defence", @@ -1186,19 +1346,51 @@ module.exports = { courtsAssignJudgeBtn: "Choose judge", trendingTitle: "Trending", exploreTrending: "Explore the most popular content in your network.", + bookmarkButton: "BOOKMARKS", + transferButton: "TRANSFERS", + eventButton: "EVENTS", + taskButton: "TASKS", votesButton: "VOTATIONS", - shopProductButton: "SHOP", + reportButton: "REPORTS", + feedButton: "FEED", + marketButton: "MARKET", + imageButton: "IMAGES", + audioButton: "AUDIOS", + videoButton: "VIDEOS", + documentButton: "DOCUMENTS", + torrentButton: "TORRENTS", + noTrendingFound: "No trending content found.", + noContentMessage: "No trending content available, yet.", + trendingDescription: "Description", + trendingDate: "Date", + trendingLocation: "Location", + trendingPrice: "Price", + trendingUrl: "URL", + trendingCategory: "Category", + trendingStart: "Start", + trendingEnd: "End", + trendingPriority: "Priority", + trendingStatus: "Status", + trendingFrom: "From", + trendingTo: "To", + trendingConcept: "Concept", + trendingAmount: "Amount", + trendingDeadline: "Deadline", + trendingItemStatus: "Item Status", + trendingTotalVotes: "Total Votes", trendingTotalOpinions: "Total Opinions", - trendingNoContentMessage: "No trending available, yet.", + trendingNoContentMessage: "No trending content available, yet.", + trendingAuthor: "By", + trendingCreatedAtLabel: "Created At", trendingTotalCount: "Total Count", tasksTitle: "Tasks", tasksDescription: "Discover and manage tasks in your network.", - taskSearchPlaceholder: "Search tasks...", taskTitleLabel: "Title", taskDescriptionLabel: "Description", taskStartTimeLabel: "Start Time", taskEndTimeLabel: "End Time", taskPriorityLabel: "Priority", + taskPrioritySelect: "Select priority", taskPriorityUrgent: "Urgent", taskPriorityHigh: "High", taskPriorityMedium: "Medium", @@ -1208,13 +1400,13 @@ module.exports = { taskVisibilityLabel: "Visibility", taskPublic: "public", taskPrivate: "private", + taskCreatedAt: "Created At", + taskBy: "By", taskStatus: "Status", taskStatusOpen: "Open", taskStatusInProgress: "In Progress", taskStatusClosed: "Closed", taskAssignedTo: "Assigned to", - taskAssignedChip: "Assigned", - taskUnassignedChip: "Unassigned", taskAssignees: "Assignees", taskAssignButton: "Assign to Me", taskUnassignButton: "Unassign", @@ -1227,6 +1419,7 @@ module.exports = { taskFilterInProgress: "IN-PROGRESS", taskFilterClosed: "CLOSED", taskFilterAssigned: "ASSIGNED", + taskFilterArchived: "ARCHIVED", taskFilterUrgent: "URGENT", taskFilterHigh: "HIGH", taskFilterMedium: "MEDIUM", @@ -1239,21 +1432,23 @@ module.exports = { taskInProgressTitle: "In Progress Tasks", taskClosedTitle: "Closed Tasks", taskAssignedTitle: "Assigned Tasks", + taskArchivedTitle: "Archived Tasks", + taskPublicTitle: "Public Tasks", + taskPrivateTitle: "Private Tasks", notasks: "No tasks available.", noLocation: "No location specified", taskSetStatus: "Set Status", + eventTitle: "Events", eventDateLabel: "Date", - eventRecurrenceLabel: "Recurrence", - eventRecurrenceUntil: "Repeat until", - eventRecurrenceHint: "Leave the repeat-until date empty for a one-off event.", - eventUpcomingDates: "Upcoming dates", eventsTitle: "Events", eventsDescription: "Discover and manage events in your network.", - eventSearchPlaceholder: "Search events...", + eventDescription: "Description", eventPrice: "Price", eventStatus: "Status", + eventOrganizer: "Organizer", eventAllSectionTitle: "Events", eventMineSectionTitle: "Your Events", + eventArchivedTitle: "Archived Events", eventCreateSectionTitle: "Create Event", eventUpdateSectionTitle: "Update Event", eventDeleteButton: "Delete", @@ -1261,26 +1456,11 @@ module.exports = { eventAddToCalendar: "Add to Calendar", eventVisitCalendar: "Visit Calendar", viewTask: "View Task", - viewEvent: "View Event", - viewPad: "View Pad", - viewMap: "View Map", - viewCalendar: "View Calendar", viewReport: "View Report", viewTransfer: "View Transfer", viewItem: "View Item", viewShop: "View Shop", viewProject: "View Project", - viewJob: "View Job", - viewPlace: "View Place", - generatePdf: "Generate PDF", - sharePm: "Share via PM", - galleryImages: "Images (max-size: 50MB each)", - galleryPhotos: "Images", - galleryAddPhoto: "Add Image", - galleryRemovePhoto: "Remove", - galleryVideo: "Video (max-size: 50MB)", - galleryAddVideo: "Add Video", - galleryRemoveVideo: "Remove video", viewVotation: "View Votation", voteCastTitle: "Cast Vote", voteResults: "Results", @@ -1288,15 +1468,29 @@ module.exports = { eventDescriptionLabel: "Description", eventDescriptionPlaceholder: "Enter event description...", eventUpdateButton: "Update", + eventAttendeesLabel: "Attendees", + eventAttendeesPlaceholder: "Enter attendees, separated by commas...", eventTagsLabel: "Tags", + eventTagsPlaceholder: "Enter event tags, separated by commas...", + eventTags: "Tags", eventPriceLabel: "Price", eventUrlLabel: "URL", eventAttendees: "Attendees", + noAttendees: "No attendees yet", + eventCreatedAt: "Created At", eventLocation: "Location", eventLocationLabel: "Location", + eventNoLocation: "No location specified", + eventNoURL: "No URL specified", + eventBy: "By", noevents: "No events available.", eventDate: "Date", + eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "Attend Event", + eventUnattendButton: "Unattend Event", + eventCreatedBy: "Created By", + eventAttendeesCount: "Attendees Count", + eventCreatedByYou: "You created this event", eventFilterAll: "ALL", eventFilterMine: "MINE", eventFilterToday: "TODAY", @@ -1304,9 +1498,18 @@ module.exports = { eventFilterMonth: "THIS MONTH", eventFilterYear: "THIS YEAR", eventFilterArchived: "ARCHIVED", + eventTodayTitle: "Today's Events", + eventThisWeekTitle: "This Week's Events", + eventThisMonthTitle: "This Month's Events", + eventThisYearTitle: "This Year's Events", eventPrivacyLabel: "Visibility", eventPublic: "Public", eventPrivate: "Private", + eventPublicTitle: "Public Events", + eventNoPrice: "Free Event", + eventNoImage: "No image uploaded", + eventAttendConfirmation: "You are now attending this event", + eventUnattendConfirmation: "You are no longer attending this event", eventAttended: "Attended", eventUnattended: "Unattended", eventStatusOpen: "Open", @@ -1317,14 +1520,9 @@ module.exports = { tagsTopSectionTitle: "Top Tags", tagsCloudSectionTitle: "Tags Cloud", tagsFilterAll: "ALL", - tagsFilterMine: "MINE", - tagsFilterRecent: "RECENT", - tagsMineSectionTitle: "My Tags", - tagsRecentSectionTitle: "Recent Tags", tagsFilterTop: "TOP", tagsFilterCloud: "CLOUD", tagsNoItems: "No tags available.", - tagsSearchPlaceholder: "Search tags...", tagsTableHeaderTag: "TAG/LINK:", tagsTableHeaderCount: "COUNTER:", transfersTitle: "Transfers", @@ -1364,12 +1562,18 @@ module.exports = { transfersDeadline: "Deadline", transfersTags: "Tags", transfersStatus: "Status", + transfersStatusUnconfirmed: "UNCONFIRMED", + transfersStatusClosed: "CLOSED", + transfersStatusDiscarded: "DISCARDED", + transfersCreatedAt: "Created At", transfersConfirmations: "CONFIRMATIONS", - transfersExportContract: "Generate Bill", + transfersContainingBlock: "Containing block", + transfersExportContract: "Create Contract", transfersConfirmButton: "Confirm Transfer", transfersNoItems: "No transfers found.", transfersMineSectionTitle: "Your Transfers", transfersUBISectionTitle: "UBI Transfers", + transfersMarketSectionTitle: "Market Transfers", transfersTopSectionTitle: "Top Transfers", transfersPendingSectionTitle: "Pending Transfers", transfersUnconfirmedSectionTitle: "Unconfirmed Transfers", @@ -1377,13 +1581,21 @@ module.exports = { transfersDiscardedSectionTitle: "Discarded Transfers", transfersCreateSectionTitle: "Create transfer", transfersAllSectionTitle: "Transfers", + transfersFilterFavs: "Favorites", + transfersFavsSectionTitle: "Favorite transfers", + transfersSearchLabel: "Search", transfersSearchPlaceholder: "Search concept, tags, users...", transfersMinAmountLabel: "Min amount", transfersMaxAmountLabel: "Max amount", + transfersSortLabel: "Sort by", transfersSortRecent: "Most recent", transfersSortAmount: "Highest amount", transfersSortDeadline: "Closest deadline", transfersSearchButton: "Search", + transfersFavoriteButton: "Favorite", + transfersUnfavoriteButton: "Unfavorite", + transfersMessageUserButton: "Message", + transfersExpiringSoonBadge: "EXPIRING", transfersExpiredBadge: "EXPIRED", transfersUpdatedAt: "Updated", transfersNoMatch: "No transfers match your search.", @@ -1396,9 +1608,6 @@ module.exports = { voteClosedTitle: "Closed Votations", voteAllSectionTitle: "Votations", voteCreateButton: "Create Votation", - voteErrorTitle: "Check the form", - voteErrorDeadlineMin: "The deadline must be at least {days} days from now.", - voteErrorGeneric: "The vote could not be saved.", voteUpdateButton: "Update", voteDeleteButton: "Delete", voteOptionYes: "YES", @@ -1428,6 +1637,16 @@ module.exports = { voteNoQuestion: "No question provided", voteUnknownCreator: "Unknown Creator", voteUnknownDate: "Unknown Date", + errorVoteNotFound: "Vote not found", + errorAlreadyVoted: "You have already opined.", + errorVoteClosedCannotEdit: "You cannot edit a closed votation", + errorVoteDeadlinePassed: "The deadline for this votation has passed", + errorRetrievingVote: "Error retrieving vote", + errorCreatingVote: "Error creating vote", + errorVoteAlreadyVoted: "Cannot edit after opinion has been cast", + errorDeletingOldVote: "Error deleting old opinion", + errorCreatingUpdatedVote: "Error creating updated opinion", + errorCreatingTombstone: "Error creating tombstone", voteDetailSectionTitle: 'Vote details', voteCommentsLabel: 'Comments', voteCommentsForumButton: 'Open discussion', @@ -1437,6 +1656,7 @@ module.exports = { voteNewCommentButton: 'Post comment', voteNewCommentLabel: 'Add a comment', cvTitle: "CV", + cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Edit CV", cvCreateSectionTitle: "Create CV", cvDescription: "Manage and share your professional skills and information.", @@ -1455,121 +1675,29 @@ module.exports = { cvLocationLabel: "Location", cvStatusLabel: "Status", cvPreferencesLabel: "Preferences", + cvOasisContributorLabel: "Oasis Contributor", cvPersonal: "Personal", cvOasis: "Oasis Contributor (optional)", - cvOasisContributorView: "Contribution", + cvOasisContributorView: "Oasis Contribution", cvEducational: "Educational (optional)", cvEducationalView: "Educational", cvProfessional: "Professional (optional)", cvProfessionalView: "Professional", cvAvailability: "Availability (optional)", + cvAvailabilityView: "Availability", cvUpdateButton: "Update", cvCreateButton: "Create CV", + cvContactLabel: "Contact", cvCreatedAt: "Created At", cvUpdatedAt: "Updated At", cvEditButton: "Update", cvDeleteButton: "Delete", cvNoCV: "No CV found.", blogSubject: "Subject", - blogMessage: "Message", + blogMessage: "Body capped at 8096 characters.", blogImage: "Upload media (max-size: 50MB)", - blogMedia: "Attachments (up to 8 images and a video)", blogPublish: "Preview", - pollsTitle: "Polls", - dataTitle: "Matches", - dataDescription: "Explore and visualize the matches in your network.", - dataFilterAll: "ALL", - dataFilterMine: "MINE", - dataFilterRecent: "RECENT", - dataFilterTop: "TOP", - dataKindInhabitants: "INHABITANTS", - dataKindJobs: "JOBS", - dataKindProjects: "PROJECTS", - dataKindEvents: "EVENTS", - dataKindTribes: "TRIBES", - dataKindMarket: "MARKET", - dataKindHousing: "HOUSING", - dataKindIndustry: "INDUSTRY", - dataKindTasks: "TASKS", - dataKindReports: "REPORTS", - dataKindVotes: "VOTATIONS", - dataSearchPlaceholder: "Search the cross-data...", - dataCohesionTitle: "Cohesion Coefficient (CC)", - dataCcShort: "CC", - dataCohesionHint: "Average affinity between everything the network has published.", - dataAffinity: "Affinity", - dataCommonTerms: "Keyword", - dataStatPeople: "CVs", - dataStatConnected: "Connected", - dataStatSkills: "Skills", - dataBestMatch: "Best Match", - dataStatIsolated: "Isolated", - dataStatEntities: "Entities compared", - dataStatTerms: "Distinct terms", - dataStatPairs: "Connected pairs", - dataMatchesTitle: "Matches", - dataMatchesHint: "Personalized algorithm suggestions.", - dataTermsTitle: "Keywords", - dataTermsHint: "The terms that appear across the most content.", - dataConnections: "Linked matches", - dataTopicTitle: "Everything related to", - dataTopicHint: "What the algorithm relates to this topic", - dataNoMatches: "No cross-data to show.", - dataNoProfile: "Publish content or write your CV so the algorithm can learn what interests you.", - pollsDescription: "Module to ask the network and count the answers.", - pollFilterAll: "ALL", - pollFilterMine: "MINE", - pollFilterRecent: "RECENT", - pollFilterTop: "TOP", - pollFilterVoted: "VOTED", - pollFilterOpen: "OPEN", - pollFilterClosed: "CLOSED", - pollCreateButton: "Create Poll", - pollCreateTitle: "Create Poll", - pollEditTitle: "Edit Poll", - pollQuestion: "Question", - pollQuestionPlaceholder: "What do you want to ask?", - pollOptions: "Options (one per line)", - pollOutcome: "Result", - pollNoVotesYet: "No votes yet", - pollTie: "Tie", - pollOptionsPlaceholder: "Plaza\nPark\nBar", - pollAnonymous: "Anonymous", - pollAnonymousLabel: "Anonymous poll", - pollMultiple: "Multiple choice", - pollMultipleLabel: "Allow multiple answers", - pollDeadline: "Deadline", - pollTags: "Tags", - pollPublishButton: "Publish Poll", - pollUpdateButton: "Update", - pollCloseButton: "Close", - pollDeleteButton: "Delete", - pollVoteButton: "Vote", - pollChangeVote: "Change Vote", - pollVoters: "Voters", - pollComments: "Comments", - pollStatusLabel: "Status", - pollStatusOpen: "OPEN", - pollStatusClosed: "CLOSED", - pollSearchPlaceholder: "Search polls...", - pollsNoItems: "No polls found.", - viewPoll: "View Poll", - pollChatCreate: "Create Poll", - pollInChat: "Poll", - blogTitle: "Blogs", - blogDescription: "Module to discover and manage blogs.", - blogFilterAll: "ALL", - blogFilterMine: "MINE", - blogFilterRecent: "RECENT", - blogFilterFavorites: "FAVORITES", - blogFilterTop: "TOP", - blogCreateButton: "Create Blog", - blogSearchPlaceholder: "Search blogs...", - blogComments: "Comments", - blogAllowComments: "Allow comments", - blogCommentsClosed: "The author has closed comments on this blog.", - blogNoItems: "No blogs found.", - viewBlog: "View Blog", + noPopularMessages: "No popular messages published, yet", forumTitle: "Forums", forumCategoryLabel: "Category", forumTitleLabel: "Title", @@ -1577,55 +1705,70 @@ module.exports = { forumCreateButton: "Create forum", forumCreateSectionTitle: "Create forum", forumDescription: "Talk openly with other inhabitants in your network.", - forumSearchPlaceholder: "Search forums...", forumFilterAll: "ALL", forumFilterMine: "MINE", forumFilterRecent: "RECENT", forumFilterTop: "TOP", + forumMineSectionTitle: "Your Forums", + forumRecentSectionTitle: "Recent Forums", + forumAllSectionTitle: "Forums", forumDeleteButton: "Delete", forumParticipants: "participants", forumMessages: "messages", + forumLastMessage: "Last message", forumMessageLabel: "Message", forumMessagePlaceholder: "Write your message...", forumSendButton: "Send", + forumVisitForum: "Visit Forum", noForums: "No forums found.", - forumVisitButton: "Visit Forum", - forumTopicLabel: "Topic", + forumVisitButton: "Visit forum", + forumCatGENERAL: "General", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", larpTitle: "L.A.R.P.", larpDescription: "A live action role-playing layer for collaborative experimentation.", - larpNoHousesMatch: "No houses match your search.", - larpSearchPlaceholder: "Search houses...", larpAllHouses: "Houses:", larpFilterRuling: "RULING", larpFilterHouses: "HOUSES", larpFilterRules: "FAQ", larpRulesTitle: "F.A.Q.", + larpRulesEntryTitle: "Starting house", larpRulesEntryText: "Every inhabitant begins in ACADEMIA by default.", - larpRulesTestEntryText: "In the WILL test each option weights one or more houses; when you submit, the system tallies the scores and auto-admits you into the highest-scoring house.", + larpRulesTestEntry: "WILL test", + larpRulesTestEntryText: "Each option weights one or more houses; when you submit, the system tallies the scores and auto-admits you into the highest-scoring house.", + larpRulesInviteEntry: "Invitation code", larpRulesInviteEntryText: "House members can generate invitation codes that grant direct access to the house.", + larpRulesOneHouseTitle: "One house at a time", larpRulesOneHouseText: "You can belong to at most one house at any given moment. If you belong to none, you are in ACADEMIA. Every non-ACADEMIA house page shows a \"Leave House\" button to its members that resets membership back to ACADEMIA. Leaving does NOT waive the test cooldown.", + larpRulesLeaveText: "Members of any house can return to ACADEMIA at any time from their house page; doing so resets their membership but does not waive the test cooldown.", + larpRulesGovernanceTitle: "Governance Wall", larpRulesGovernanceText: "During its cycle, the governing house wears the \"Ruling\" badge and is the only one whose Wall is surfaced publicly under the RULING tab.", - larpRulesWallText: "Each house has a Wall where its members write. A house Wall is private to the house, except the one of the governing house and the one of ACADEMIA, which anybody can read.", + larpRulesSignTitle: "L.A.R.P. Emblem", larpRulesSignText: "Inhabitants can choose whether to add a sensor on their avatar with the emblem of their current house.", larpPostsTitle: "Wall", larpPostsEmpty: "No posts yet.", + larpPostLabel: "New post", larpPostPlaceholder: "What does this house need to say?", + larpPostPublic: "Public (visible to anyone, otherwise only members)", larpPostSubmit: "Publish", + larpPostMembersOnly: "Members only", larpCycleLabel: "Cycle:", larpCyclesUntilRuling: "Next governance cycle", larpVisitTribe: "Visit Tribe", - larpStartJourney: "Start Journey", - larpGoverning: "Ruling:", - larpStartHere: "Start here:", + larpGoverning: "Governing:", larpMyHouse: "My House:", larpMembersCount: "Members", + larpMembersTitle: "Members", + larpNoMembers: "No members yet.", larpRolesLabel: "Roles", larpFunctionLabel: "Function", larpMonthLabel: "Governance cycle", larpLeaveToAcademia: "Leave House", - larpAcademiaJoinTitle: "L.A.R.P. Houses", + larpAcademiaJoinTitle: "Houses Current Status", larpWillTestTitle: "WILL test", larpWillTestHint: "Once completed, you will be assigned the house that best matches your answers. Your individual answers are processed locally and never stored — only the resulting house assignment is published to your feed.", + larpAcademiaJoinHint: "Take the psychological profile test to be assigned the house that fits you best, or redeem an invitation code shared by a member of a house.", + larpTakeTestButton: "Take the profile test", larpInviteRedeem: "Join House", larpInviteCreate: "Generate invitation code", larpInvitePlaceholder: "Invitation code", @@ -1634,9 +1777,11 @@ module.exports = { invitesHouseJoinButton: "Join House", larpInviteBannerTitle: "New invitation code", larpInviteBannerHint: "Share this code with someone in ACADEMIA. It expires in 30 cycles or after one use.", + larpTestAssignedHouse: "Assigned house", larpTestRankingTitle: "Score by house", larpLastAttemptTitle: "Your last attempt", larpLastAttemptHouse: "House", + larpLastAttemptResult: "Result", larpLastAttemptWhen: "When", larpLastAttemptCooldown: "Next attempt in", larpTestTitle: "Profile test", @@ -1715,16 +1860,34 @@ module.exports = { larpTestCooldownActive: "Next test available in", larpTestCooldownDays: "cycles", larpTestResultTitle: "Test result", + larpTestPassed: "Test passed — welcome!", + larpTestFailed: "Test not passed", larpTestScore: "Score", larpTestNextAttempt: "You can attempt another test in 30 cycles.", larpGoToHouse: "Go to your house", larpBackToAcademia: "Back to ACADEMIA", + larpBackToHouses: "◀ Houses", larpBadgeYou: "You", larpBadgeRuling: "Ruling", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Module for the live-action role-playing layer of Oasis (the 9 Houses).", + forumCatPOLITICS: "Politics", + forumCatTECH: "Tech", + forumCatSCIENCE: "Science", + forumCatMUSIC: "Music", + forumCatART: "Art", + forumCatGAMING: "Gaming", + forumCatBOOKS: "Books", + forumCatFILMS: "Films", + forumCatPHILOSOPHY: "Philosophy", + forumCatSOCIETY: "Society", + forumCatPRIVACY: "Privacy", + forumCatCYBERWARFARE: "Cyberwarfare", + forumCatSURVIVALISM: "Survivalism", imageTitle: "Images", imageDescription: "Explore and manage image content in your network.", + imagePluginTitle: "Title", + imagePluginDescription: "Description", imageMineSectionTitle: "Your Images", imageCreateSectionTitle: "Upload Image", imageUpdateSectionTitle: "Update Image", @@ -1733,12 +1896,14 @@ module.exports = { imageTopSectionTitle: "Top Images", imageFavoritesSectionTitle: "Favorites", imageGallerySectionTitle: "Gallery", + imageMemeSectionTitle: "Memes", imageFilterAll: "ALL", imageFilterMine: "MINE", imageFilterRecent: "RECENT", imageFilterTop: "TOP", imageFilterFavorites: "FAVORITES", imageFilterGallery: "GALLERY", + imageFilterMeme: "MEMES", imageCreateButton: "Upload Image", imageUpdateButton: "Update", imageDeleteButton: "Delete", @@ -1751,6 +1916,7 @@ module.exports = { imageTitlePlaceholder: "Optional", imageDescriptionLabel: "Description", imageDescriptionPlaceholder: "Optional", + imageMemeLabel: "¿MEME?", imageNoFile: "No image file provided", noImages: "No images available.", imageSearchPlaceholder: "Search title, tags, description, author...", @@ -1758,6 +1924,7 @@ module.exports = { imageSortOldest: "Oldest", imageSortTop: "Most voted", imageSearchButton: "Search", + imageMessageAuthorButton: "Message", imageUpdatedAt: "Updated", imageNoMatch: "No images match your search.", feedTitle: "Feed", @@ -1769,6 +1936,7 @@ module.exports = { createFeedButton: "Send Feed!", feedPlaceholder: "What's happening? (max 280 characters)", TODAYButton: "TODAY", + CREATEButton: "Create Feed", totalOpinions: "Total Opinions", moreVoted: "More Voted", noFeedsFound: "No feeds found.", @@ -1791,12 +1959,20 @@ module.exports = { concept: "Concept", description: "Description", meme: "Meme", + activityContact: "Contact", + activityBy: "Name", + activityPixelia: "New pixel added", + viewImage: "View image", + playAudio: "Play audio", + playVideo: "Play video", typeRecent: "RECENT", - typeTop: "TOP", + errorActivity: "Error retrieving activity", typePost: "BLOGS", + recentButton: "RECENT", typeTribe: "TRIBES", typeLarp: "L.A.R.P.", typeInhabitants: "INHABITANTS", + activityProfileUpdated: "updated their profile", typeAbout: "INHABITANTS", typeCurriculum: "CVS", typeImage: "IMAGES", @@ -1840,6 +2016,8 @@ module.exports = { typeCourtsSettlementAccepted: "Courts · Settlement Accepted", typeCourtsNomination: "Courts · Nomination", typeCourtsNominationVote: "Courts · Nomination Vote", + activitySupport: "New alliance forged", + activityJoin: "New PUB joined", question: "Question", deadline: "Deadline", status: "Status", @@ -1853,8 +2031,12 @@ module.exports = { date: "Date", category: "Category", attendees: "Attendees", + activitySpread: "->", + visitLink: "Visit Link", + viewDocument: "View Document", location: "Location", contentWarning: "Subject", + personName: "Inhabitant Name", bankWalletConnected: "ECOin Wallet", bankUbiReceived: "UBI Received", bankTx: "Tx", @@ -1872,6 +2054,7 @@ module.exports = { aiExchangeMarkHelpful:"+1 helpful", activityProjectFollow: "%OASIS% is now %ACTION% this project %PROJECT%", activityProjectUnfollow: "%OASIS% is now %ACTION% this project %PROJECT%", + activityProjectPledged: "%OASIS% has %ACTION% %AMOUNT% to project %PROJECT%", following: "FOLLOWING", unfollowing: "UNFOLLOWING", pledged: "PLEDGED", @@ -1917,17 +2100,26 @@ module.exports = { courtsVotesSlashTotal: "YES/TOTAL", courtsOpenVote: "Open vote", courtsAnswerTitle: "Answer", + courtsStanceADMIT: "Admit", + courtsStanceDENY: "Deny", + courtsStancePARTIAL: "Partial", + courtsStanceCOUNTERCLAIM: "Counterclaim", + courtsStanceNEUTRAL: "Neutral", courtsVerdictResult: "Result", courtsVerdictOrders: "Orders", courtsSettlementText: "Settlement", courtsSettlementAccepted: "Accepted", + courtsSettlementPending: "Pending", courtsJudge: "Judge", courtsThSupports: "Supports", courtsFilterOpenCase: 'Open Case', + courtsEvidenceFileLabel: 'Evidence file (image, audio, video or PDF)', + courtsCaseMediators: 'Mediators', courtsCaseMediatorsPh: 'Mediator Oasis IDs, separated by comma', courtsMediatorsLabel: 'Mediators', courtsThCase: 'Case', courtsThCreatedAt: 'Start date', + courtsThActions: 'Actions', courtsPublicPrefLabel: 'Visibility after resolution', courtsPublicPrefYes: 'I agree this case can be fully public', courtsPublicPrefNo: 'I prefer to keep the details private', @@ -1935,11 +2127,8 @@ module.exports = { courtsMethodMEDIATION: 'Mediation', courtsNoCases: 'No cases.', reportsTitle: "Reports", - reportsDescription: "Manage and track reports in your network.", - reportsSearchPlaceholder: "Search reports...", + reportsDescription: "Manage and track reports related to issues, bugs, abuses and content warnings in your network.", reportsFilterAll: "ALL", - reportsFilterRecent: "RECENT", - reportsFilterTop: "TOP", reportsFilterMine: "MINE", reportsFilterFeatures: "FEATURES", reportsFilterBugs: "BUGS", @@ -1952,13 +2141,18 @@ module.exports = { reportsFilterInvalid: "INVALID", reportsCreateButton: "Create Report", reportsTitleLabel: "Title", + reportsDescriptionLabel: "Description", + reportsDescriptionPlaceholder: "Please provide a detailed description.", reportsCategory: "Category", + reportsCategoryLabel: "Category", reportsCategoryFeatures: "Features", reportsCategoryBugs: "Bugs", reportsCategoryAbuse: "Abuse", - reportsCategoryContent: "Content", + reportsCategoryContent: "Content Issues", reportsUpdateButton: "Update", reportsDeleteButton: "Delete", + reportsDateLabel: "Date", + reportsUploadFile: "Upload media (max-size: 50MB)", reportsMineSectionTitle: "Your Reports", reportsFeaturesSectionTitle: "Feature Requests", reportsBugsSectionTitle: "Bugs", @@ -1966,6 +2160,11 @@ module.exports = { reportsContentSectionTitle: "Content Issues", reportsAllSectionTitle: "Reports", reportsNoItems: "No reports available.", + reportsValidationTitle: "Please enter a valid title.", + reportsValidationDescription: "Description cannot be empty.", + reportsValidationCategory: "Please select a category.", + reportsCreatedAt: "Created At", + reportsCreatedBy: "By", reportsSeverity: "Severity", reportsSeverityLow: "Low", reportsSeverityMedium: "Medium", @@ -1976,10 +2175,13 @@ module.exports = { reportsStatusUnderReview: "Under Review", reportsStatusResolved: "Resolved", reportsStatusInvalid: "Invalid", + reportsUpdateStatusButton: "Update Status", + reportsAnonymityOption: "Submit Anonymously", + reportsAnonymousAuthor: "Anonymous", reportsConfirmButton: "CONFIRM REPORT!", reportsConfirmations: "Confirmations", reportsConfirmedSectionTitle: "Confirmed Reports", - reportsCreateTaskButton: "Create Task", + reportsCreateTaskButton: "CREATE TASK", reportsOpenSectionTitle: "Open Reports", reportsUnderReviewSectionTitle: "Under Review Reports", reportsResolvedSectionTitle: "Resolved Reports", @@ -2023,20 +2225,6 @@ module.exports = { reportsRequestedActionLabel: 'Requested action', reportsRequestedActionPlaceholder: 'Remove, hide, tag, warn, etc.', tribesTitle: "Tribes", - tribeFilterAll: "ALL", - tribeFilterRecent: "RECENT", - tribeFilterMine: "MINE", - tribeFilterMembership: "MEMBERSHIP", - tribeFilterSubtribes: "SUBTRIBES", - tribeFilterTop: "TOP", - tribeFilterGallery: "GALLERY", - tribeFeedFilterTOP: "TOP", - tribeFeedFilterMINE: "MINE", - tribeFeedFilterALL: "ALL", - tribeFeedFilterRECENT: "RECENT", - courtsStanceDENY: "Deny", - courtsStanceADMIT: "Admit", - courtsStancePARTIAL: "Partially admit", tribeAllSectionTitle: "Tribes", tribeMineSectionTitle: "Your Tribes", tribeCreateSectionTitle: "Create Tribe", @@ -2048,6 +2236,13 @@ module.exports = { tribeviewSubTribeButton: "Visit Sub-Tribe", tribeRootLabel: "ROOT", tribeDescription: "Explore or create tribes on your network.", + tribeFilterAll: "ALL", + tribeFilterMine: "MINE", + tribeFilterMembership: "MEMBERSHIP", + tribeFilterRecent: "RECENT", + tribeFilterTop: "TOP", + tribeFilterSubtribes: "SUB-TRIBES", + tribeFilterGallery: "GALLERY", tribeMainTribeLabel: "MAIN TRIBE", tribeCreateButton: "Create Tribe", tribeUpdateButton: "Update", @@ -2066,8 +2261,13 @@ module.exports = { tribeModeLabel: "MODE", tribeIsAnonymousLabel: "STATUS", tribeMembersCount: "Members", + tribeInviteCodePlaceholder: "Enter invite code", + tribeJoinByCodeButton: "Join with code", + tribeJoinButton: "JOIN", tribeEnterInvite: "ENTER INVITE CODE", tribeLeaveButton: "LEAVE", + tribeYes: "YES", + tribeNo: "NO", tribePublic: "PUBLIC", tribePrivate: "PRIVATE", tribeGenerateInvite: "SINGLE INVITATION", @@ -2076,8 +2276,13 @@ module.exports = { tribeRemoveInvitation: "REMOVE INVITATION", tribeCreatedAt: "Created at", tribeAuthor: "By", + tribeAuthorLabel: "AUTHOR", tribeStrict: "Strict", tribeOpen: "Open", + tribeFeedFilterRECENT: "RECENT", + tribeFeedFilterMINE: "MINE", + tribeFeedFilterALL: "ALL", + tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Refeeds", tribeFeedRefeed: "Refeed", tribeFeedMessagePlaceholder: "Write a feed…", @@ -2087,12 +2292,17 @@ module.exports = { tribeNotFound: "Tribe not found!", createTribeTitle: "Create Tribe", updateTribeTitle: "Update Tribe", + tribeSectionOverview: "Overview", tribeSectionInhabitants: "Inhabitants", tribeSectionVotations: "VOTATIONS", tribeSectionEvents: "EVENTS", + tribeSectionReports: "Reports", tribeSectionTasks: "TASKS", tribeSectionFeed: "FEED", tribeSectionForum: "FORUM", + tribeSectionMarket: "Market", + tribeSectionJobs: "Jobs", + tribeSectionProjects: "Projects", tribeSectionMedia: "Media", tribeSectionImages: "IMAGES", tribeSectionAudios: "AUDIOS", @@ -2130,6 +2340,11 @@ module.exports = { tribeTaskStatusClosed: "CLOSE", tribeTaskAssign: "ASSIGN", tribeTaskUnassign: "UNASSIGN", + tribeReportCreate: "Create Report", + tribeReportsEmpty: "No reports, yet.", + tribeReportTitle: "Title", + tribeReportDescription: "Description", + tribeReportCategory: "Category", tribeVotationCreate: "Create Votation", tribeVotationsEmpty: "No votations, yet.", tribeVotationTitle: "Title", @@ -2147,6 +2362,27 @@ module.exports = { tribeForumCategory: "Category", tribeForumReply: "Reply", tribeForumReplies: "Replies", + tribeMarketCreate: "Create Listing", + tribeMarketEmpty: "No listings, yet.", + tribeMarketTitle: "Title", + tribeMarketDescription: "Description", + tribeMarketPrice: "Price", + tribeMarketImage: "Image", + tribeMarketCategory: "Category", + tribeJobCreate: "Create Job", + tribeJobsEmpty: "No jobs, yet.", + tribeJobTitle: "Title", + tribeJobDescription: "Description", + tribeJobLocation: "Location", + tribeJobSalary: "Salary", + tribeJobDeadline: "Deadline", + tribeProjectCreate: "Create Project", + tribeProjectsEmpty: "No projects, yet.", + tribeProjectTitle: "Title", + tribeProjectDescription: "Description", + tribeProjectGoal: "Goal", + tribeProjectFunded: "Funded", + tribeProjectDeadline: "Deadline", tribeMediaUpload: "Upload Media", readDocument: "Read Document", tribeCreateImage: "Create Image", @@ -2157,6 +2393,7 @@ module.exports = { tribeMediaEmpty: "No media, yet.", tribeMediaTitle: "Title", tribeMediaDescription: "Description", + tribeMediaType: "Type", tribeMediaTypeImage: "Image", tribeMediaTypeVideo: "Video", tribeMediaTypeAudio: "Audio", @@ -2166,6 +2403,11 @@ module.exports = { tribeInviteCodeText: "Invite code: ", oasisVersionLabel: "Oasis Version", tribeInviteCodeHint: "Share this code with someone you want to invite. They can join via /invites → Tribes.", + tribeGroupTribe: "Tribe", + tribeGroupOffice: "Office", + tribeGroupNetwork: "Network", + tribeGroupEconomy: "Economy", + tribeGroupMedia: "Media", tribeStatusOpen: "OPEN", tribeStatusClosed: "CLOSED", tribeStatusInProgress: "IN PROGRESS", @@ -2175,17 +2417,33 @@ module.exports = { tribePriorityCritical: "CRITICAL", tribeTaskFilterAll: "ALL", tribeMediaFilterAll: "ALL", + tribeReportCatBug: "BUG", + tribeReportCatAbuse: "ABUSE", + tribeReportCatContent: "CONTENT", + tribeReportCatOther: "OTHER", tribeForumCatGeneral: "GENERAL", tribeForumCatProposal: "PROPOSAL", tribeForumCatQuestion: "QUESTION", tribeForumCatAnnouncement: "ANNOUNCEMENT", + tribeMarketCatGoods: "GOODS", + tribeMarketCatServices: "SERVICES", + tribeMarketCatFood: "FOOD", + tribeMarketCatOther: "OTHER", tribeStatusLabel: "Status", tribeSubTribes: "SUB-TRIBES", tribeSubTribesCreate: "Create Sub-Tribe", tribeSubTribesStrictDenied: "Tribe strict mode does not allow you to create new sub-tribes. Please contact the administrator.", + tribeSubTribesEmpty: "No sub-tribes created, yet.", + tribeActivityJoined: "JOINED", + tribeActivityLeft: "LEFT", + tribeActivityFeed: "FEED", tribeActivityRefeed: "REFEED", + tribeGroupAnalytics: "Analytics", + tribeGroupCreative: "Creative", tribeSectionActivity: "ACTIVITY", tribeSectionTrending: "TRENDING", + tribeSectionOpinions: "OPINIONS", + tribeSectionPixelia: "PIXELIA", tribeSectionTags: "TAGS", tribeSectionSearch: "SEARCH", tribeActivityEmpty: "No activity yet.", @@ -2197,15 +2455,25 @@ module.exports = { tribeTrendingPeriodWeek: "This Week", tribeTrendingPeriodAll: "All Time", tribeTrendingEngagement: "engagement", + tribeOpinionsEmpty: "No opinions yet.", + tribeOpinionsCast: "Vote", + tribeOpinionsRankings: "Rankings", + tribeOpinionsAlreadyVoted: "Already voted", tribeTopCategory: "More Voted", + tribePixeliaTitle: "Pixelia", + tribePixeliaDescription: "Collaborative pixel art canvas.", + tribePixeliaPaint: "Paint", + tribePixeliaContributors: "contributors", + tribePixeliaTotalPixels: "pixels painted", tribeTagsEmpty: "No tags found.", + tribeTagsCloud: "Tag Cloud", + tribeTagsContentWith: "Content with tag", tribeSearchPlaceholder: "Search tribe content...", tribeSearchEmpty: "No results found.", tribeSearchResults: "Results", tribeSearchMinChars: "Enter at least 2 characters to search.", agendaTitle: "Agenda", agendaDescription: "Here you can find all your assigned items.", - agendaSearchPlaceholder: "Search agenda...", agendaFilterAll: "ALL", agendaFilterToday: "TODAY", agendaFilterUpcoming: "UPCOMING", @@ -2219,14 +2487,23 @@ module.exports = { agendaFilterReports: "REPORTS", agendaFilterTransfers: "TRANSFERS", agendaFilterJobs: "JOBS", - agendaFilterIndustry: "INDUSTRY", - agendaFilterHousing: "HOUSING", agendaFilterProjects: "PROJECTS", agendaFilterCalendars: "CALENDARS", agendaNoItems: "No assignments found.", + agendaAuthor: "By", agendaDiscardButton: "Discard", agendaRestoreButton: "Restore", + agendaCreatedAt: "Created At", + agendaTitleLabel: "Title", agendaMembersCount: "Members", + agendaDescriptionLabel: "Description", + agendaStatus: "Status", + agendaVisibility: "Visibility", + agendaEventDate: "Event Date", + agendaEventLocation: "Location", + agendaEventPrice: "Price", + agendaEventUrl: "URL", + agendaTaskStart: "Start Time", agendaLocationLabel: "Location", agendaYes: "YES", agendaNo: "NO", @@ -2236,6 +2513,11 @@ module.exports = { agendareportCategory: "Category", agendareportSeverity: "Severity", agendareportStatus: "Status", + agendareportDescription: "Description", + agendaTaskEnd: "End Time", + agendaTaskPriority: "Priority", + agendaTransferFrom: "From", + agendaTransferTo: "To", agendaTransferConcept: "Concept", agendaTransferAmount: "Amount", agendaTransferDeadline: "Deadline", @@ -2245,7 +2527,47 @@ module.exports = { voteNow: "Vote now", alreadyVoted: "You have already opined.", noOpinionsFound: "No opinions found.", + RECENTButton: "RECENT", TOPButton: "TOP", + interestingButton: "INTERESTING", + necessaryButton: "NECESSARY", + funnyButton: "FUNNY", + disgustingButton: "DISGUSTING", + sensibleButton: "SENSIBLE", + propagandaButton: "PROPAGANDA", + adultOnlyButton: "ADULT ONLY", + boringButton: "BORING", + confusingButton: "CONFUSING", + usefulButton: "USEFUL", + informativeButton: "INFORMATIVE", + wellResearchedButton: "WELL RESEARCHED", + accurateButton: "ACCURATE", + needsSourcesButton: "NEEDS SOURCES", + wrongButton: "WRONG", + lowQualityButton: "LOW QUALITY", + creativeButton: "CREATIVE", + insightfulButton: "INSIGHTFUL", + actionableButton: "ACTIONABLE", + inspiringButton: "INSPIRING", + loveButton: "LOVE", + clearButton: "CLEAR", + upliftingButton: "UPLIFTING", + unnecessaryButton: "UNNECESSARY", + rejectedButton: "REJECTED", + misleadingButton: "MISLEADING", + offTopicButton: "OFF TOPIC", + duplicateButton: "DUPLICATE", + clickbaitButton: "CLICKBAIT", + spamButton: "SPAM", + trollButton: "TROLL", + nsfwButton: "NSFW", + violentButton: "VIOLENT", + toxicButton: "TOXIC", + harassmentButton: "HARASSMENT", + hateButton: "HATE", + scamButton: "SCAM", + triggeringButton: "TRIGGERING", + opinionsCreatedAt: "Created At", opinionsTotalCount: "Total Opinions", voteInteresting: "Interesting", voteNecessary: "Necessary", @@ -2282,39 +2604,27 @@ module.exports = { voteCreative: "Creative", voteSpam: "Spam", voteAdultOnly: "Adult Only", + publishBlog: "Publish Blog", privateMessage: "PM", pmSendTitle: "Private Messages", pmSend: "Send!", - pmCancel: "Cancel", - pmReset: "Reset", - pmSharedKeyHint: "Share this key with the recipient out of band. It is required to decrypt.", - pmDescription: "Use this form to send an encrypted message/file to other inhabitants.", - pmComposeTitle: "Send a message", + pmDescription: "Use this form to send an encrypted message to other inhabitants.", pmRecipients: "Recipients", pmRecipientsHint: "Enter Oasis IDs separated by commas", - pmLimitsHint: "Up to 7 recipients per message.", - pmTextPlaceholder: "Body capped at 7000 characters.", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "Subject", pmSubjectHint: "Enter the message subject", pmText: "Message", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Decrypt", - fileShareTitle: "Share a file", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "File", - fileShareDownload: "Download", - fileShareMutualError: "You can only share files with inhabitants with mutual support.", - fileShareNoFile: "No file selected.", - fileShareTooLarge: "The file exceeds the allowed size.", - fileShareFailed: "Could not prepare the file.", - fileShareSendError: "Could not send the file.", - fileShareUnavailable: "This file is not available right now. Try again later.", pmCrypterBadKey: "Your shared key is incorrect!", pmCrypterTooLong: "The message is too long to encrypt with the Crypter. Please shorten it and try again.", pmCrypterCipherLabel: "Re-encrypted message", pmCrypterCharsLabel: "characters", pmInvalidRecipients: "Invalid Oasis ID (should be like @…=.ed25519).", pmEncryptionLabel: "Encryption", + pmFile: "Attachment", private: "Private", privateDescription: "Your encrypted messages.", privateInbox: "INBOX", @@ -2324,27 +2634,19 @@ module.exports = { noPrivateMessages: "No private messages.", pmFromLabel: "From:", pmToLabel: "To:", + pmInvalidMessage: "Invalid message", pmNoSubject: "(no subject)", pmSubjectLabel: "Subject:", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "The ruling house of the L.A.R.P has changed.", - politicalBotParliamentTitle: "A new government cycle has begun in the Parliament.", - politicalBotTribeTitle: "A new government cycle has begun in one of your Tribes.", - politicalBotLarpIntro: "Now {house} rules during this cycle: {cycle}", - politicalBotParliamentIntro: "The current government is {gov}", - politicalBotParliamentIntroLeader: "The current government is {gov}, led by {leader}", - politicalBotTribeIntro: "The current government in the Tribe {tribe} is {gov}", - politicalBotTribeIntroLeader: "The current government in the Tribe {tribe} is {gov}, led by {leader}", - politicalBotLeaderLabel: "Leader", + pmBodyLabel: "Body", + pmBotJobs: "42-JobsBOT", + pmBotProjects: "42-ProjectsBOT", + pmBotMarket: "42-MarketBOT", inboxJobSubscribedTitle: "New subscription to your job offer", pmInhabitantWithId: "Inhabitant with OASIS ID:", pmHasSubscribedToYourJobOffer: "has subscribed to your job offer", + inboxProjectCreatedTitle: "New project created", + pmHasCreatedAProject: "has created a project", inboxMarketItemSoldTitle: "Item Sold", - inboxShopSoldTitle: "Product Sold", pmYourItem: "Your item", pmHasBeenSoldTo: "has been sold to", pmFor: "for", @@ -2352,6 +2654,7 @@ module.exports = { pmHasPledged: "has pledged", pmToYourProject: "to your project", blockchain: 'BlockExplorer', + blockchainTitle: 'BlockExplorer', blockchainDescription: 'Explore and visualize the blocks in the blockchain.', blockchainNoBlocks: 'No blocks found in the blockchain.', blockchainStatsTitle: 'Stats', @@ -2363,36 +2666,42 @@ module.exports = { blockchainBlockCarbon: 'Carbon footprint', blockchainBlockType: 'Type', blockchainBlockTimestamp: 'Timestamp', + blockchainBlockContent: 'Block', + blockchainBlockURL: 'URL:', + blockchainContent: 'Block', + blockchainContentPreview: 'Preview of the block content', blockchainLatestDatagram: 'Latest Datagram', - blockchainDatagram: "Datagram", - blockchainDetails: "View Block Details", - blockchainViewBlockexplorer: "View in Blockexplorer", + blockchainDatagram: 'Datagram', + blockchainDetails: 'View block details', + blockchainViewBlockexplorer: "View blockexplorer", + blockchainBlockInfo: 'Block Information', + blockchainBlockDetails: 'Details of the selected block', blockchainBack: 'Back to Blockexplorer', blockchainContentDeleted: "This content has been tombstoned", visitContent: "Visit Content", - favoriteAdd: "Pin to Favorites", - pmContentTooltip: "Send a Private Message", - favoriteRemove: "Unpin from Favorites", - reportContent: "Report Content", banking: 'Banking', bankingTitle: 'Banking', - bankingDescription: "The ECOin economy: balance, UBI, taxes and industry value.", + bankingDescription: 'Explore the current value of ECOin and the corresponding UBI allocation, distributed per epoch based on participation and trust.', bankOverview: 'Overview', bankEpochs: 'Epochs', bankRules: 'Rules', pending: 'Pending', closed: 'Closed', + bankBack: 'Back to Banking', bankViewTx: 'View Tx', bankClaimNow: 'Claim now', bankClaimUBI: 'Claim UBI!', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: 'No pending UBI allocations for this epoch.', bankEpoch: 'Epoch', bankPool: 'Pool (this epoch)', bankWeightsSum: 'Sum of weights', + bankAllocations: 'Allocations', bankNoAllocations: 'No allocations found.', bankNoEpochs: 'No epochs found.', bankEpochAllocations: 'Epoch allocations', @@ -2406,11 +2715,10 @@ module.exports = { bankEpochId: 'Epoch ID', bankRuleHash: 'Rules Snapshot Hash', bankViewEpoch: 'View Epoch', - bankYourIndustryBalance: "Your Industry Share", - bankYourUbiMonth: "Your UBI (this month)", - bankYourFundsMonth: "Your Funds (this month)", - bankIndustryBalance: "Industry Production (estimated)", bankUserBalance: 'Your Balance', + ecoWalletNotConfigured: 'ECOin Wallet not configured', + editWallet: 'Edit wallet', + addWallet: 'Add wallet', bankAddresses: 'Addresses', bankNoAddresses: 'No addresses found.', bankUser: 'Oasis ID', @@ -2435,10 +2743,18 @@ module.exports = { search: 'Search!', bankLocal: 'Local', bankFromOasis: 'Oasis', + bankMyAddress: 'Your address', + bankRemoveMyAddress: 'Remove my address', + bankNotRemovableOasis: 'Addresses cannot be removed locally', + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "NO FUNDS!", bankUbiAvailableOk: "AVAILABLE!", - bankUbiAvailability: "UBI (network)", + bankUbiAvailability: "UBI availability", bankAlreadyClaimedThisMonth: "Already claimed this month", bankUbiThisMonth: "UBI (this month)", bankUbiLastClaimed: "UBI (last claimed)", @@ -2452,6 +2768,13 @@ module.exports = { shopsTitle: "Shops", shopDescription: "Discover and manage shops in the network.", shopTitle: "Shop", + shopFilterAll: "ALL", + shopFilterMine: "MINE", + shopFilterRecent: "RECENT", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUCTS", + shopFilterPrices: "PRICES", + shopFilterFavorites: "FAVORITES", shopUpload: "Create Shop", shopAllSectionTitle: "Shops", shopMineSectionTitle: "Your Shops", @@ -2468,10 +2791,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "Publish to Clearnet", + shopClearnetUnpublish: "Unpublish from Clearnet", + shopClearnetUrlLabel: "Clearnet URL", + shopClearnetWarning: "Publishing on Clearnet makes this shop indexable by external search engines and crawlers.", shopAddFavorite: "Add Favorite", shopRemoveFavorite: "Remove Favorite", shopOpen: "OPEN", shopClosed: "CLOSED", + shopOpenShop: "Open Shop", + shopCloseShop: "Close Shop", shopMakePrivate: "MAKE PRIVATE (E2E)", shopMakePublic: "MAKE PUBLIC", shopProducts: "Products", @@ -2499,6 +2828,8 @@ module.exports = { shopOrderPrice: "Price", shopOrderBuyer: "Buyer", shopBackToShop: "Back to Shop", + shopShareUrl: "Share URL", + shopVisitShop: "VISIT SHOP", marketVisitItem: "VISIT ITEM", shopStatus: "STATUS", shopCreatedAt: "CREATED", @@ -2507,10 +2838,12 @@ module.exports = { shopLocation: "Location", shopTags: "Tags", shopVisibility: "Visibility", + shopImage: "Image", shopShortDescription: "Short Description", shopShortDescriptionPlaceholder: "Brief description of your shop", shopTitlePlaceholder: "Name of your shop", shopDescriptionPlaceholder: "Detailed description of your shop", + shopUrlPlaceholder: "https://your-shop-url.com", shopLocationPlaceholder: "City, Country", shopTagsPlaceholder: "tag1, tag2, tag3", mapTitlePlaceholder: "Map title", @@ -2528,6 +2861,7 @@ module.exports = { bankUnitMinutes: "minutes", bankUnitDays: "days", bankExchangeNoData: 'No data available', + bankExchangeIndex: 'ECOin Value (1h)', bankInflation: 'ECOin Inflation (anual)', bankInflationMonthly: 'ECOin Inflation (mensual)', bankExchangeChartTitle: 'ECOin value progression over time', @@ -2541,17 +2875,38 @@ module.exports = { bankingSyncStatusOutdated: 'Outdated', statsTitle: 'Statistics', statistics: "Statistics", + statsInhabitant: "Inhabitant Stats", statsDescription: "Discover statistics about your network.", ALLButton: "ALL", MINEButton: "MINE", TOMBSTONEButton: "TOMBSTONES", + statsYou: "You", + statsUserId: "Oasis ID", statsCreatedAt: "Created At", statsYourContent: "Content", statsYourOpinions: "Opinions", + statsYourTombstone: "Tombstones", + statsNetwork: "Network", + statsTotalInhabitants: "Inhabitants", + statsDiscoveredTribes: "Tribes (Public)", + statsPrivateDiscoveredTribes: "Tribes (Private)", statsNetworkContent: "Content", + statsYourMarket: "Market", + statsYourJob: "Jobs", + statsYourProject: "Projects", + statsYourTransfer: "Transfers", + statsYourForum: "Forums", statsNetworkOpinions: "Opinions", + statsDiscoveredMarket: "Market", + statsDiscoveredJob: "Jobs", + statsDiscoveredProject: "Projects", + statsBankingTitle: "Banking", statsEcoWalletLabel: "ECOIN Wallet", statsEcoWalletNotConfigured: "Not configured!", + statsTotalEcoAddresses: "Total addresses", + statsDiscoveredTransfer: "Transfers", + statsDiscoveredForum: "Forums", + statsNetworkTombstone: "Tombstones", statsBookmark: "Bookmarks", statsEvent: "Events", statsTask: "Tasks", @@ -2573,12 +2928,16 @@ module.exports = { statsAiExchange: "AI", statsPUBs: 'PUBs', statsPost: "Posts", + statsOasisID: "Oasis ID", statsSize: "Total (size)", statsBlockchainSize: "Blockchain (size)", statsBlobsSize: "Blobs (size)", statsActivity7d: "Activity (last 7 days)", statsActivity7dTotal: "7-day total", statsActivity30dTotal: "30-day total", + statsKarmaScore: "KARMA Score", + statsPublic: "Public", + statsPrivate: "Private", day: "Day", messages: "Messages", statsProject: "Projects", @@ -2590,14 +2949,30 @@ module.exports = { statsProjectsCancelled: "Cancelled", statsProjectsGoalTotal: "Total goal", statsProjectsPledgedTotal: "Total pledged", + statsProjectsSuccessRate: "Success rate", + statsProjectsAvgProgress: "Average progress", + statsProjectsMedianProgress: "Median progress", + statsProjectsActiveFundingAvg: "Avg. active funding", + statsJobsTitle: "Jobs", + statsJobsTotal: "Total jobs", + statsJobsOpen: "Open", + statsJobsClosed: "Closed", + statsJobsOpenVacants: "Open vacants", + statsJobsSubscribersTotal: "Total subscribers", + statsJobsAvgSalary: "Average salary", + statsJobsMedianSalary: "Median salary", statsMarketTitle: "Market", statsMarketTotal: "Total items", statsMarketForSale: "For sale", statsMarketReserved: "Reserved", statsMarketClosed: "Closed", statsMarketSold: "Sold", + statsMarketRevenue: "Revenue", + statsMarketAvgSoldPrice: "Avg. sold price", statsUsersTitle: "Inhabitants", user: "Inhabitant", + statsTombstoneTitle: "Tombstones", + statsNetworkTombstones: "Network tombstones", statsTombstoneRatio: "Tombstone ratio (%)", statsParliamentCandidature: "Parliament (candidatures)", statsParliamentTerm: "Parliament terms", @@ -2624,21 +2999,30 @@ module.exports = { aiConfiguration: "Set prompt", aiPromptUsed: "Prompt", aiClearHistory: "Clear chat history", + aiSharePrompt: "Add this answer to collective training?", + aiShareYes: "Yes", + aiShareNo: "No", + aiSharedLabel: "Added to training", + aiRejectedLabel: "Not added to training", aiServerError: "The AI could not answer. Please try again.", aiInputPlaceholder: "What is Oasis?", typeAiExchange: "AI", aiApproveTrain: "Add to collective training", aiRejectTrain: "Do not train", + aiTrainPending: "Pending approval", aiTrainApproved: "Approved for training", aiTrainRejected: "Rejected for training", aiSnippetsUsed: "Snippets used", aiSnippetsLearned: "Snippets learned", statsAITraining: "AI training", + aiApproveCustomTrain: "Train using this custom answer", aiCustomAnswerPlaceholder: "Write your custom answer…", + statsAIExchanges: "Model Exchanges", marketMineSectionTitle: "Your Items", marketCreateSectionTitle: "Create Item", marketUpdateSectionTitle: "Update", marketAllSectionTitle: "Market", + marketRecentSectionTitle: "Recent Market", marketTitle: "Market", marketDescription: "A marketplace for exchanging goods or services in your network.", marketFilterAll: "ALL", @@ -2668,11 +3052,14 @@ module.exports = { marketItemDeadline: "Deadline", marketItemIncludesShipping: "Includes Shipping?", marketItemHighestBid: "Highest Bid", + marketItemHighestBidder: "Highest Bidder", marketItemStock: "Stock", marketOutOfStock: "Out of stock", + marketItemBidTime: "Bid Time", marketActionsUpdate: "Update", marketUpdateButton: "Update", marketActionsDelete: "Delete", + marketActionsSold: "Mark as Sold", marketActionsChangeStatus: "CHANGE STATUS", marketActionsBuy: "BUY!", marketAuctionBids: "Current Bids", @@ -2681,82 +3068,19 @@ module.exports = { marketNoItems: "No items available, yet.", marketYourBid: "Your Bid", marketCreateFormImageLabel: "Upload media (max-size: 50MB)", + marketSearchLabel: "Search", marketSearchPlaceholder: "Search title or tags", marketMinPriceLabel: "Min price", marketMaxPriceLabel: "Max price", + marketSortLabel: "Sort by", marketSortRecent: "Most recent", marketSortPrice: "Price", marketSortDeadline: "Deadline", marketSearchButton: "Search", marketAuctionEndsIn: "Ends", marketAuctionEnded: "Ended", + marketMyBidBadge: "You bid", marketNoItemsMatch: "No items match your search.", - housingTitle: "Housing", - housingDescriptionText: "Discover and manage places in your network.", - modulesHousingLabel: "Housing", - modulesHousingDescription: "Module to discover and manage places.", - typeHousing: "HOUSING", - housingFilterAll: "ALL", - housingFilterMine: "MINE", - housingFilterRequested: "REQUESTED", - housingFilterSale: "SALE", - housingFilterRent: "RENT", - housingFilterCouchsurfing: "COUCHSURFING", - housingFilterOpen: "AVAILABLE", - housingFilterClosed: "UNAVAILABLE", - housingFilterRecent: "RECENT", - housingFilterTop: "TOP", - housingType: "Offer", - housingTypeSALE: "For sale", - housingTypeRENT: "For rent", - housingTypeCOUCHSURFING: "Couchsurfing", - housingProperty: "Property", - housingPropertyAPARTMENT: "Apartment", - housingPropertyHOUSE: "House", - housingPropertyROOM: "Room", - housingPropertyLAND: "Land", - housingPropertyOTHER: "Other", - housingTitleLabel: "Title", - housingTitlePlaceholder: "Name of the place", - housingDescription: "Description", - housingDescriptionPlaceholder: "Describe the place, the neighbourhood, what is included: bathrooms, garden, beds…", - housingRules: "Rules", - housingRulesPlaceholder: "Pets, smoking, quiet hours, minimum stay…", - housingPlace: "Place", - housingPlacePlaceholder: "Country, city, address", - housingPrice: "Price", - housingPriceLabel: "Price (ECO)", - housingSizeLabel: "Size (m²)", - housingFree: "FREE", - housingPerMonth: "/month", - housingRooms: "Rooms", - housingSize: "Size", - housingCapacity: "Guests", - housingAvailableFrom: "Available from", - housingAvailableTo: "Available until", - housingTags: "Tags", - spreadEditWarning: "This content will lose spreads after editing it", - housingRequests: "Requests", - housingRequestButton: "Request", - housingCancelButton: "Cancel request", - housingRequestedBadge: "REQUESTED", - housingStatus: "Status", - housingStatusOPEN: "AVAILABLE", - housingStatusCLOSED: "UNAVAILABLE", - housingSetOpen: "Make available", - housingSetClosed: "Make unavailable", - housingCreateButton: "Create Place", - housingUpdateButton: "Update", - housingDeleteButton: "Delete", - housingSearchPlaceholder: "Search places...", - housingSearchButton: "Search", - housingMinPrice: "Min price", - housingMaxPrice: "Max price", - housingSortRecent: "Most recent", - housingSortPrice: "Lowest price", - housingSortRequests: "Most requested", - housingSortRating: "Best rated", - housingNoItems: "No places found.", jobsTitle: "Jobs", jobsCandidatesTitle: "Suggested candidates", jobsCandidatesDescription: "Inhabitants whose skills match this job. Send a private message to invite them.", @@ -2804,6 +3128,7 @@ module.exports = { jobLocationPresencial: "On-place", jobLocationRemote: "Remote", jobVacantsPlaceholder: "Number of positions", + jobSalaryPlaceholder: "Salary in ECO for 1 dedicated hour", jobImage: "Upload media (max-size: 50MB)", jobTasks: "Tasks", jobType: "Job Type", @@ -2813,6 +3138,7 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Top Salary Jobs", createJobButton: "Publish Job", + viewDetailsButton: "View Details", noJobsFound: "No job offers found.", jobAuthor: "By", jobTypeFreelance: "Freelancer", @@ -2824,6 +3150,10 @@ module.exports = { jobsHoursOffered: "Hours offered", jobsHoursRequested: "Hours requested in return", jobsExchangeSkill: "Skill wanted in exchange", + jobsHoursOfferedPlaceholder: "e.g. 4", + jobsHoursRequestedPlaceholder: "e.g. 4", + jobsExchangeSkillPlaceholder: "e.g. carpentry, design", + jobExchangeHint: "For hour-exchange jobs, fill the fields below instead of salary.", jobTimePartial: "Part-time", jobTimeComplete: "Full-time", jobsDeleteButton: "DELETE", @@ -2831,201 +3161,31 @@ module.exports = { jobsFilterApplied: "APPLIED", jobsAppliedTitle: "My applications", jobsAppliedBadge: "Applied", + jobsFilterFavs: "Favorites", + jobsFavsTitle: "Favorites", + jobsFilterNeeds: "Needs help", + jobsNeedsTitle: "Needs help", + jobsSearchLabel: "Search", jobsSearchPlaceholder: "Search title, tags, description...", jobsMinSalaryLabel: "Min salary", jobsMaxSalaryLabel: "Max salary", + jobsSortLabel: "Sort by", jobsSortRecent: "Most recent", jobsSortSalary: "Highest salary", jobsSortSubscribers: "Most applicants", jobsSearchButton: "Search", + jobsFavoriteButton: "Favorite", + jobsUnfavoriteButton: "Unfavorite", + jobsMessageAuthorButton: "PM", jobsApplicants: "Applicants", jobsUpdatedAt: "Updated", jobSetOpen: "Set open", + jobNewBadge: "NEW", jobsTagsLabel: "Tags", + jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "No jobs match your search.", - industrySearchPlaceholder: "Search facilities…", - industryAllSectors: "All sectors", - industryVisitButton: "Visit Industry", - industryAdvanceTo: "Set to", - industryAmount: "Amount", - industryApproveBuild: "Approve", - industryBlueprint: "Blueprint", - industryBlueprints: "Blueprints", - industryBuild: "Build", - industryBuildNotes: "Notes", - industryBuildStart: "Start date", - industryVoteUpdateButton: "Vote update", - industryVoteDeleteButton: "Vote delete", - industryRevokeVote: "Revoke", - industryTimeLeft: "Time left", - industryBuildEnd: "End date", - industryBuilds: "Builds", - industryBuildTitle: "Title", - industryContribute: "Contribute", - industryContributeButton: "Contribute", - industryContributions: "Contributions", - industryCreateBlueprintButton: "Create Blueprint", - industryDistribute: "Distribute", - industryDistributeButton: "Distribute by Shares", - industryDistribution: "Distribution", - industryEcoAmount: "Amount (ECO)", - industryEditBlueprint: "Edit Blueprint", - industryFacility: "Facility", - industryKindLabel: "Kind", - industryKind_labor: "Labor", - industryKind_material: "Material", - industryKind_eco: "Funds", - industryLaborHours: "Labor (hours)", - industryHours: "Hours", - industryMaterialItem: "Material", - industryMaterials: "Materials", - industryEstMaterials: "Total Materials", - industryEstLabor: "Total Labor", - industryEstTotal: "Estimated price", - industryBuildingPrice: "Building price", - industryFinalPrice: "Final price", - industryBlueprintDescPlaceholder: "Specs, dimensions, weight, docs…", - industryMaterialValue: "Value (ECO)", - industryMember: "Member", - industryNewBlueprint: "New Blueprint", - industryNewBuild: "Propose a Build", - industryEditBuild: "Edit build", - industryNeedBlueprint: "Create a blueprint first: every build produces one.", - industryNoBlueprints: "No blueprints yet.", - industryNoBuilds: "No builds yet.", - industryNote: "Note", - industryOutput: "Output", - industryOutputKind: "Product type", - industryOutputValue: "Output value (ECO, e.g. from sale)", - industryPostJob: "Create Job", - industryPot: "Pot", - industryProposeBuildButton: "Propose Build", - industryProposer: "Proposer", - industryAuthor: "Author", - industrySellOnMarket: "Send to Market", - industrySendToProjects: "Create Project", - industrySkills: "Skills", - industryTreasury: "Treasury", - industryKind_physical: "Physical", - industryKind_digital: "Digital", - industryBuildStatus_PROPOSED: "PROPOSED", - industryBuildStatus_REJECTED: "REJECTED", - industryBuildStatus_APPROVED: "APPROVED", - industryBuildStatus_STOCKING: "STOCKING", - industryBuildStatus_IN_PRODUCTION: "IN PRODUCTION", - industryBuildStatus_COMPLETED: "COMPLETED", - industryBuildStatus_FAILED: "FAILED", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "A job matches your curriculum", - jobsBotMatchIntro: "A job matches your curriculum.", - jobsBotMatchJob: "Job", - jobsBotMatchHint: "You receive this because your curriculum is AI managed. You can turn it off in your CV.", - cvAiManaged: "AI Managed", - switchOn: "ON", - switchOff: "OFF", - cvAiManagedHint: "When on, JobsBot warns you by private message when a job matches your curriculum.", - cvMatchThreshold: "Notify me from this match level", - housingBotRequestedTitle: "Somebody has requested one of your places.", - housingBotCancelledTitle: "A request on one of your places was cancelled.", - housingBotYouRequestedTitle: "You have requested a place.", - housingBotYouCancelledTitle: "You have cancelled a request.", - housingBotUnavailableTitle: "A place you requested is no longer available.", - housingBotUnavailableSentTitle: "You have told the people who requested this place.", - industryBotAdmittedTitle: "You have been admitted to a facility.", - industryBotApplicationTitle: "New membership application in your facility.", - industryBotInvitedTitle: "You have been invited to a facility.", - industryBotDissolvedTitle: "A facility you belong to has been dissolved.", - industryBotBuildApprovedTitle: "A build has been approved.", - industryBotDistributedTitle: "A build output has been distributed.", - industryFilterRules: "RULES", - industryRulesTitle: "Rules", - industryRulesIntro: "Industry lets inhabitants collectively own the means of production: a Facility is a network-owned workshop that nobody owns privately.", - industryRulesSteward: "The founder is the steward — a caretaker, not an owner: they cannot sell, privatize or dissolve the Facility on their own.", - industryRulesMembership: "Membership policy sets how people join: Open (join instantly), By vote (admitted by the members' vote) or Invite only (a member must invite them first).", - industryRulesQuorum: "Quorum is the minimum number of members that must vote for a decision to count, so a small Facility isn't decided by a single person.", - industryRulesMajority: "Majority is the fraction of votes needed to pass (0.5 = half, 1 = unanimity); a decision passes when YES votes reach at least max(quorum, members × majority).", - industryRulesDecisions: "Members vote to admit newcomers, approve builds and dissolve the Facility; the steward cannot override these collective decisions.", - industryRulesBlueprints: "Blueprints are libre (COPYLEFT) recipes — the inputs (materials, labor hours, skills) and the output (a physical or digital good) — and anyone can fork them.", - industryRulesBuilds: "A Build is a production order: PROPOSED → APPROVED (by vote) → STOCKING → IN PRODUCTION → COMPLETED, and it only accepts contributions once approved.", - industryRulesContributions: "Members contribute labor (hours), materials (declared ECO value) or ECO to a build; each contribution earns karma.", - industryRulesLaborRate: "Labor rate is the ECO value of one work hour in this Facility; it turns hours into karma so effort and capital compare fairly: karma = hours × labor rate + material value + ECO.", - industryRulesShares: "Your share of a build is your karma ÷ total karma, and it sets how much of the output value you receive.", - industryRulesDistribution: "When a build is completed the steward distributes the pot (the build's funds plus any sale value) among contributors, proportional to their shares.", - industryRulesTreasury: "ECO contributions are held by the steward as the build's treasury custodian until distribution; all movements are recorded on the network and disputes can go to Courts.", - industryJobs: "Jobs", - industryReportButton: "Report", - industryCreateTask: "Create Task", - industryOpenDispute: "Open dispute", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "Generate invite", - industryPauseVotes: "Status votes", - industryTitle: "Industry", - industryDescription: "Module to manage the means of production collectively.", - typeIndustryBuild: "BUILD", - typeIndustryBlueprint: "BLUEPRINT", - typeIndustryAllocation: "DISTRIBUTION", - typeIndustry: "INDUSTRY", - modulesIndustryLabel: "Industry", - modulesIndustryDescription: "Module to manage the means of production collectively.", - industryFilterAll: "ALL", - industryFilterMember: "MEMBER", - industryFilterBlueprints: "BLUEPRINTS", - industryFilterBuilds: "BUILDS", - industryFilterMine: "MINE", - industryFilterActive: "WORKING", - industryFilterPaused: "PAUSED", - industryFilterDissolved: "DISSOLVED", - industryAllTitle: "Industry", - industryMemberTitle: "Facilities I'm in", - industryMineTitle: "My facilities", - industryActiveTitle: "Working", - industryPausedTitle: "Paused facilities", - industryDissolvedTitle: "Dissolved facilities", - industryNoFacilitiesFound: "No facilities found.", - industryCreateFacility: "Create Facility", - industryMembers: "Members", - industryMemberBadge: "MEMBER", - industryName: "Name", - industryNamePlaceholder: "Facility name", - industryDescriptionLabel: "Description", - industryDescriptionPlaceholder: "What does this facility produce?", - industrySector: "Sector", - industryMembershipPolicy: "Membership policy", - industryQuorum: "Quorum", - industryMajority: "Majority", - industryLaborRate: "Labor rate", - industryCreateButton: "Create Facility", - industryUpdateButton: "Update", - industrySteward: "Steward", - industryStatusActive: "WORKING", - industryStatusPaused: "PAUSED", - industryStatusDissolved: "DISSOLVED", - industryStatusLabel: "Status", - industryJoinButton: "Join", - industryApplyButton: "Request to join", - industryLeaveButton: "Leave", - industryInviteButton: "Invite", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "You need an invitation to join.", - industryPauseButton: "Vote to pause", - industryResumeButton: "Vote to resume", - industryDeleteButton: "Delete", - industryPendingApplicants: "Pending applicants", - industryVotesYes: "yes", - industryVotesNo: "no", - industryAlreadyVoted: "voted", - industryAdmit: "Admit", - industryReject: "Reject", - industryDissolveTitle: "Dissolve facility", - industryDissolveVote: "Vote to dissolve", - industryPolicy_open: "Open", - industryPolicy_vote: "By vote", - industryPolicy_invite: "Invite only", projectsTitle: "Projects", projectsDescription: "Create, fund, and follow community-driven projects in your network.", - projectSearchPlaceholder: "Search projects...", projectCreateProject: "Create Project", projectCreateButton: "Create Project", projectUpdateButton: "UPDATE", @@ -3069,8 +3229,14 @@ module.exports = { projectPledgeTitle: "Back this project", projectPledgePlaceholder: "Amount in ECO", projectBounties: "Bounties", + projectBountiesInputLabel: "Bounties (one per line: Title|Amount [ECO]|Description)", + projectBountiesPlaceholder: "Fix UI bug|100|Link to issue\nWrite docs|250|Outline usage examples", projectNoBounties: "No bounties found.", projectTitle: "Title", + projectAddBountyTitle: "New Bounty", + projectBountyTitle: "Bounty Title", + projectBountyAmount: "Amount (ECO)", + projectBountyDescription: "Description", projectMilestoneSelect: "Select Milestone", projectBountyCreateButton: "Create Bounty", projectBountyStatus: "Bounty Status", @@ -3085,15 +3251,19 @@ module.exports = { projectMilestoneTitle: "Milestone Title", projectMilestoneTargetPercent: "Percent (%)", projectMilestoneDueDate: "Date", + projectMilestoneCreateButton: "Create Milestone", projectMilestoneStatus: "Milestone Status", projectMilestoneOpen: "open", projectMilestoneDone: "completed", projectMilestoneDue: "due", + projectNoMilestones: "No milestones found.", projectMilestoneMarkDone: "Mark as Done", projectMilestoneTitlePlaceholder: "Enter milestone title", projectMilestoneDescriptionPlaceholder: "Enter description for this milestone", projectMilestoneDescription: "Milestone Description", + projectBudgetGoal: "Budget (Goal)", projectBudgetAssigned: "Assigned to bounties", + projectBudgetRemaining: "Remaining", projectBudgetOver: "⚠ Over budget: assigned exceeds goal", projectFollowers: "Followers", projectFollowersTitle: "Followers", @@ -3106,6 +3276,7 @@ module.exports = { projectBackersTotalPledged: "Total pledged", projectBackersYourPledge: "Your pledge", projectBackersNone: "No pledges yet.", + projectNoRemainingBudget: "No remaining budget.", projectFilterBackers: "BACKERS", projectFilterApplied: "APPLIED", projectAppliedTitle: "APPLIED", @@ -3114,15 +3285,30 @@ module.exports = { projectBackerAmount: "Total contributed", projectBackerPledges: "Pledges", projectBackerProjects: "Projects", + projectPledgeAmount: "Amount", projectSelectMilestoneOrBounty: "Select Milestone or Bounty", projectPledgeButton: "Pledge", footerLicense: "GPLv3", + footerPackage: "Package", + footerVersion: "Version", modulesModuleName: "Name", modulesModuleDescription: "Description", modulesModuleStatus: "Status", modulesTotalModulesLabel: "Loaded Modules", modulesEnabledModulesLabel: "Enabled", modulesDisabledModulesLabel: "Disabled", + modulesPopularLabel: "Popular", + modulesPopularDescription: "Module to receive posts that are trending, most viewed, or most commented on.", + modulesTopicsLabel: "Topics", + modulesTopicsDescription: "Module to receive discussion categories based on shared interests.", + modulesSummariesLabel: "Summaries", + modulesSummariesDescription: "Module to receive summaries of long discussions or posts.", + modulesLatestLabel: "Latest", + modulesLatestDescription: "Module to receive the most recent posts and discussions.", + modulesThreadsLabel: "Threads", + modulesThreadsDescription: "Module to receive conversations grouped by topic or question.", + modulesMultiverseLabel: "Multiverse", + modulesMultiverseDescription: "Module to receive content from other federated peers.", modulesInvitesLabel: "Invites", modulesInvitesDescription: "Module to manage and apply invite codes.", modulesWalletLabel: "Wallet", @@ -3163,12 +3349,8 @@ module.exports = { modulesOpinionsDescription: "Module to discover and vote on opinions.", modulesTransfersLabel: "Transfers", modulesTransfersDescription: "Module to discover and manage smart-contracts (transfers).", - modulesFediverseLabel: "Multiverse", - modulesBlogsLabel: "Blogs", - modulesBlogsDescription: "Module to discover and manage blogs.", - modulesPollsLabel: "Polls", - modulesPollsDescription: "Module to ask the network and count the answers.", - modulesFediverseDescription: "Manage your other multiverse accounts, including sending and receiving content.", + modulesFediverseLabel: "Fediverse", + modulesFediverseDescription: "Manage your other fediverse accounts, including sending and receiving content.", modulesFeedLabel: "Feed", modulesFeedDescription: "Module to discover and share short-texts (feeds).", modulesParliamentLabel: "Parliament", @@ -3204,33 +3386,37 @@ module.exports = { visibilityMakeHidden: "Make hidden", invitesInhabitantsTitle: "Inhabitants", invitesInhabitantsFollow: "Give Support", - aiNavPlaceholder: "Describe what you need...", + aiNavPlaceholder: "Where do you want to go?", aiNavDisabled: "AI navigation is disabled.", - aiNavResultsTitle: "AINav Suggestions", + aiNavResultsTitle: "AI navigation suggestions", aiNavQueryLabel: "Query", + aiNavResultsDescription: "Pick the route that best matches what you are looking for.", + aiNavResultPath: "Route", + aiNavResultDescription: "What you can do", aiNavResultMatch: "Match", aiNavResultsEmpty: "No matching routes. Try /search instead.", + aiNavSearchInstead: "Search instead", modulesAINavLabel: "AINav", modulesAINavDescription: "Module for natural-language queries about the network's content.", + directConnect: "Direct Connect", directConnectDescription: "Connect directly to a peer by entering their IP address, port and public key. The peer will be added as a followed connection.", peerHost: "IP", peerPort: "Port (default: 8008)", peerPublicKey: "Public Key (@...ed25519)", connectAndFollow: "Connect", + deviceSourceLabel: "Device source", modulesPresetTitle: "Common Configurations", - workflowsTitle: "Workflows", - workflowsDescription: "A workflow sets the theme and which modules are active.", - workflowsSet: "Set workflow", - workflow_default: "Default", - workflow_jobs: "Jobs", - workflow_ruling: "Ruling", - workflow_politics: "Politics", - workflow_social: "Social", - workflow_business: "Business", - workflow_night: "Night", - workflow_mobile: "Mobile", + modulesPreset_minimal: "Minimal", + modulesPreset_basic: "Basic", + modulesPreset_social: "Social", + modulesPreset_economy: "Economy", + modulesPreset_full: "Full", statsCarbonFootprintTitle: "Carbon Footprint", + statsCarbonFootprintNetwork: "Network carbon footprint", + statsCarbonFootprintYours: "Your carbon footprint", statsCarbonTombstone: "Tombstoning footprint", + feedSuccessMsg: "Feed published successfully!", + dominantOpinionLabel: "Dominant opinion", uploadMedia: "Upload media (max-size: 50MB)", mapsLabel: "Maps", mapTitle: "Maps", @@ -3258,13 +3444,14 @@ module.exports = { mapDescriptionLabel: "Description", mapDescriptionPlaceholder: "Describe the map or location...", mapTypeLabel: "Map type", - mapInviteMode: "Invite", - mapTypeSingle: "SINGLE", - mapTypeOpen: "OPEN", - mapTypeClosed: "CLOSED", + mapTypeSingle: "SINGLE (only initial location)", + mapTypeOpen: "OPEN (anyone can add markers)", + mapTypeClosed: "CLOSED (only creator adds markers)", mapTagsLabel: "Tags", mapTagsPlaceholder: "Enter tags separated by commas", mapUrlLabel: "Map URL", + mapPickCoordLabel: "Or select a location on the grid:", + mapMarkersLabel: "markers", mapMarkersTitle: "Markers", mapMarkerDefault: "Marker", mapMarkerLatLabel: "Marker latitude", @@ -3278,6 +3465,10 @@ module.exports = { mapApplyZoom: "Apply Zoom", mapSearchPlaceholder: "Search description, tags, author...", mapSearchButton: "Search", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + mapClickToCreate: "Click on the map to select a location", + mapClickToAddMarker: "Click on the map to add a marker", mapUpdatedAt: "Updated", mapNoMatch: "No maps match your search.", noMaps: "No maps available.", @@ -3292,9 +3483,14 @@ module.exports = { padTitle: "Pad", modulesPadsLabel: "Pads", modulesPadsDescription: "Module to manage collaborative text editors.", + padFilterAll: "ALL", + padFilterMine: "MINE", + padFilterRecent: "RECENT", + padFilterOpen: "OPEN", + padFilterClosed: "CLOSED", padCreate: "Create Pad", - padUpdate: "Update", - padDelete: "Delete", + padUpdate: "Update Pad", + padDelete: "Delete Pad", padTitleLabel: "Title", padTitlePlaceholder: "Enter pad title...", padStatusLabel: "Status", @@ -3305,7 +3501,14 @@ module.exports = { padTagsLabel: "Tags", padTagsPlaceholder: "tag1, tag2, ...", padMembersLabel: "Members", + padVisitPad: "Visit Pad", + padShareUrl: "Share URL", padCreated: "Created", + padAuthor: "Author", + padGenerateCode: "Generate Code", + padInviteCodeLabel: "Invite Code", + padInviteCodePlaceholder: "Enter invite code...", + padValidateInvite: "Validate", padStartEditing: "START EDITING!", padEditorPlaceholder: "Start writing...", padSubmitEntry: "Submit", @@ -3321,11 +3524,12 @@ module.exports = { padClosedSectionTitle: "Closed Pads", padCreateSectionTitle: "Create New Pad", padUpdateSectionTitle: "Update Pad", + padInviteGenerated: "Invite Code Generated", typePad: "PADS", padNew: "NEW", padAddFavorite: "Add to Favorites", padRemoveFavorite: "Remove from Favorites", - padClose: "Close", + padClose: "Close Pad", padBackToEditor: "Back to editor", padSearchPlaceholder: "Search pads...", @@ -3334,6 +3538,12 @@ module.exports = { modulesCalendarsLabel: "Calendars", modulesCalendarsDescription: "Module to discover and manage calendars.", typeCalendar: "CALENDARS", + calendarFilterAll: "ALL", + calendarFilterMine: "MINE", + calendarFilterOpen: "OPEN", + calendarFilterClosed: "CLOSED", + calendarFilterRecent: "RECENT", + calendarFilterFavorites: "FAVORITES", calendarCreate: "Create Calendar", calendarUpdate: "Update", calendarDelete: "Delete", @@ -3346,17 +3556,24 @@ module.exports = { calendarTagsLabel: "Tags", calendarTagsPlaceholder: "tag1, tag2...", calendarParticipantsLabel: "Participants", + calendarParticipantsCount: "Participants", + calendarVisitCalendar: "Visit Calendar", calendarCreated: "Created", + calendarAuthor: "Author", calendarJoin: "Join Calendar", calendarGenerateInvite: "Generate invite", + calendarInviteCodePlaceholder: "Enter invite code...", + calendarValidateInvite: "Validate code", + calendarJoined: "Joined", + calendarAddDate: "Add Date", + calendarAddNote: "Add Note", calendarDateLabel: "Date", calendarDatePlaceholder: "Describe this date...", - calendarNoteLabel: "Notes", + calendarNoteLabel: "Note", calendarNotePlaceholder: "Add a note...", calendarFirstDateLabel: "Date", calendarFirstNoteLabel: "Notes", calendarIntervalLabel: "Interval", - calendarIntervalNone: "No repetition", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3367,6 +3584,8 @@ module.exports = { calendarsDescription: "Discover and manage calendars in your network.", calendarMonthPrev: "\u2190 Prev", calendarMonthNext: "Next \u2192", + calendarMonthLabel: "Dates", + calendarsShareUrl: "Share URL", calendarAllSectionTitle: "Calendars", calendarRecentSectionTitle: "Recent Calendars", calendarFavoritesSectionTitle: "Favorites", @@ -3380,6 +3599,7 @@ module.exports = { calendarRemoveFavorite: "Remove from Favorites", calendarSearchPlaceholder: "Search calendars...", calendarAddEntry: "Add Entry", + calendarLeave: "Leave Calendar", statsCalendar: "Calendars", statsCalendarDate: "Calendar Dates", statsCalendarNote: "Calendar Notes", @@ -3388,6 +3608,8 @@ module.exports = { modulesChatsDescription: "Module to discover and manage encrypted chats.", typeChat: "CHATS", typeChatMessage: "CHAT MESSAGE", + chatLabel: "CHATS", + chatMessageLabel: "CHAT MESSAGES", chatsTitle: "Chats", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3397,50 +3619,52 @@ module.exports = { chatDescription: "Description", chatCategory: "Category", chatStatus: "STATUS", + chatFilterAll: "ALL", + chatFilterMine: "MINE", + chatFilterRecent: "RECENT", + chatFilterFavorites: "FAVORITES", + chatFilterOpen: "OPEN", + chatFilterClosed: "CLOSED", chatCreate: "Create Chat", - chatUpdate: "Update", - chatDelete: "Delete", + chatUpdate: "Update Chat", + chatDelete: "Delete Chat", chatClose: "Close Chat", chatVisitChat: "VISIT CHAT", chatUntitled: "Untitled Chat", chatNoItems: "No chats found.", chatParticipants: "Participants", - chatMessagesLabel: "Messages", - chatThreadsLabel: "Threads", + chatStartChatting: "START CHATTING!", + chatGenerateCode: "Generate Code", + chatShareUrl: "Share URL", chatCreatedAt: "CREATED", chatSearchPlaceholder: "Search chats...", chatStatusOpen: "OPEN", chatStatusInviteOnly: "INVITE-ONLY", chatStatusClosed: "CLOSED", chatSendMessage: "Send", - chatJumpLatest: "Latest Message", - chatPickChat: "Pick a chat to open it", - chatNoneYet: "No chat available yet.", - activitySearchPlaceholder: "Search in activity...", - trendingSearchPlaceholder: "Search in trending...", - opinionsSearchPlaceholder: "Search in opinions...", - votesSearchPlaceholder: "Search in votations...", - welcomeStepUxTitle: "Choose your interface", - welcomeStepUxText: "Pick how Oasis should look. You can change it later in Settings.", - welcomeStepUxAction: "Use this view", - chatRateLimitTitle: "Too many messages", - chatRateLimitMessage: "You have reached the limit of messages this room accepts in an hour. Try again later.", chatMessagePlaceholder: "Type your message...", chatNoMessages: "No messages yet.", + chatLeave: "Leave Chat", + chatInviteCodeLabel: "Enter invite code", + chatJoinByInvite: "Join", chatTitlePlaceholder: "Chat title", chatDescriptionPlaceholder: "Chat description", chatTagsPlaceholder: "tag1, tag2, tag3", chatImageLabel: "Select an image file (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "Invite Code", chatAuthor: "Author", + chatCreated: "Created", chatAddFavorite: "Add to Favorites", chatRemoveFavorite: "Remove from Favorites", chatPM: "PM", chatStatusLabel: "Status", chatCategoryLabel: "Category", + chatParticipantsLabel: "Participants", gamesTitle: "Games", gamesDescription: "Discover and play some mini-games in your network.", gamesFilterAll: "ALL", gamesPlayButton: "PLAY!", + gamesBackToGames: "Back to Games", modulesGamesLabel: "Games", modulesGamesDescription: "Module to discover and play some games.", modulesGraphosLabel: "Graphos", @@ -3457,6 +3681,8 @@ module.exports = { gamesArkanoidDesc: "Break all the bricks with your paddle and ball. A classic arcade challenge.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Classic ping-pong against an AI opponent. First to 5 points wins.", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "Race against time! Dodge traffic and reach the finish line before the clock runs out.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Pilot your ship through a deadly asteroid field. Shoot them down before they hit you.", gamesRockPaperScissorsTitle: "Rock Paper Scissors", @@ -3477,6 +3703,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3485,6 +3713,7 @@ module.exports = { gamesHallDate: "Date", typeGameScore: "GAME SCORES", gamesNewRecord: "New Record", + gameScoreLabel: "GAME SCORES", uxModeTitle: "UX", uxModeDescription: "Select which UX navigation mode you want for your GUI.", uxModeMenus: "Blocks", @@ -3537,6 +3766,7 @@ module.exports = { torrentSortOldest: "Oldest", torrentSortTop: "Most voted", torrentNoMatch: "No matching torrents.", + torrentMessageAuthorButton: "Message Author", torrentUpdatedAt: "Updated", statsTorrent: "Torrents", typeTorrent: "TORRENTS", @@ -3544,18 +3774,10 @@ module.exports = { modulesTorrentsDescription: "Module to discover and manage torrents.", favoritesFilterTorrents: "TORRENTS", favoritesFilterMarket: "MARKET", - favoritesFilterEvents: "EVENTS", - favoritesFilterForum: "FORUM", - favoritesFilterHousing: "HOUSING", - favoritesFilterJobs: "JOBS", - favoritesFilterProjects: "PROJECTS", - favoritesFilterReports: "REPORTS", - favoritesFilterTasks: "TASKS", - favoritesFilterTransfers: "TRANSFERS", - favoritesFilterVotes: "VOTATIONS", favoritesFilterShopProducts: "SHOP ITEMS", tribeSectionTorrents: "TORRENTS", tribeCreateTorrent: "Upload Torrent", + tribeMediaTypeTorrent: "Torrent", settingsWishTitle: "Wish", settingsWishDesc: "Configure the wish of your Avatar. This determines how to access the whole content and your level of experiences into the network.", settingsWishWhole: "Multiverse", @@ -3566,10 +3788,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "Outbound is a UX guardrail. Inbound is an attention filter (ciphertext is still replicated).", + pmBlockedNonMutual: "You can only send private messages to habitants with mutual support.", inhabitantsPendingFollowsTitle: "Pending follow requests", inhabitantsPendingAccept: "Accept", inhabitantsPendingReject: "Reject", + bxEncrypted: "ENCRYPTED", + bxEncryptedHexLabel: "Ciphertext (preview)", tribeSectionGovernance: "GOVERNANCE", + tribeSubStatusPublic: "PUBLIC", + tribeSubStatusPrivate: "PRIVATE", + tribeSubInheritedPrivate: "PRIVATE (inherited from main tribe)", tribePadInviteRequired: "You do not have access to the pad. Ask for an invitation to access the content.", tribeChatInviteRequired: "You do not have access to the chat. Ask for an invitation to access the content.", tribeGovernanceDesc: "Internal governance for this tribe. Propose candidatures, debate rules, elect leaders.", @@ -3609,31 +3837,44 @@ module.exports = { tribeGovNoHistorical: "Not any record, yet", logsTitle: "Logs", logsDescription: "Record your experience in the network.", + logsReadMore: "Read more", logsViewTitle: "Log", logsColumnType: "Type", logsView: "View", + logsManualPrompt: "Write your log", logsUpdateButton: "Update", - logsEditTitle: "Update Log", + logsEditTitle: "Edit Log", + logsCreateDescription: "Record your experience...", logsCreateTitle: "Create Log", logsWriteButton: "Write", logsTextPlaceholder: "Describe your experiences...", + logsTextField: "Text", + logsLabelPlaceholder: "Label...", + logsLabelField: "Write your log (label)", + logsAiDisabledWarn: "Enable the AI module in /modules to use AI-written logs.", + logsAiContextValue: "Blockchain day", + logsAiContext: "Context", + logsAiModStatus: "AImod", + logsModeApply: "Apply!", logsModeAIWritten: "AI-Assistant", logsModeManual: "Manual", logsModeAI: "AI", + logsColumnDelete: "Delete", + logsColumnEdit: "Edit", logsColumnLog: "Log", logsColumnDate: "Date", logsDelete: "Delete", - logsEdit: "Update", + logsEdit: "Edit", logsFilterAlways: "ALWAYS", logsFilterYear: "LAST YEAR", logsFilterMonth: "LAST MONTH", logsFilterWeek: "LAST WEEK", logsFilterToday: "TODAY", - logsComments: "Comments", - logsAuthorEntries: "Entries", + logsFilterRecent: "RECENT", + logsFilterAll: "ALL", logsCreate: "Create Log", logsExport: "Export Logs", - logsExportOne: "Export Log", + logsExportOne: "Export", logsViewDetails: "View Details", logsGenerateButton: "Generate Text", logsSearchText: "Search in logs...", @@ -3643,7 +3884,9 @@ module.exports = { modulesLogsLabel: "Logs", modulesLogsDescription: "Module to record (via AI assistant) your experiences.", statsLogsTitle: "Logs", + statsLogsEntries: "Entries", typeLog: "LOGS", + blockchainCycle: "Cycle", } diff --git a/src/client/assets/translations/oasis_es.js b/src/client/assets/translations/oasis_es.js index b135331..0faa594 100644 --- a/src/client/assets/translations/oasis_es.js +++ b/src/client/assets/translations/oasis_es.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { es: { + bbAdd: "Añadir", + bbQuickActions: "Acciones rápidas", + bbPickTitle: "Fijar un módulo", + bbPickDesc: "Elige un módulo para el hueco + de la barra inferior.", + bbPickNone: "Ninguno", + bbManage: "Editar", + bbYourBar: "Tu barra", + bbRemove: "Quitar", + bbFull: "Máximo alcanzado", + bbDone: "Hecho", + activityChangeFilter: "Cambiar filtro", + emptyConnectHint: "Conéctate a un pub para sincronizar con la red.", + emptyConnectCta: "Conéctate a un pub", + menuCommunity: "Comunidad", + activityGroupCommunity: "Comunidad y Gobernanza", + activityGroupOrg: "Organización", + activityGroupComm: "Comunicación", + activityGroupEconomy: "Economía", + activityGroupMedia: "Contenido y Media", + activityGroupOther: "Otros", + languageName: "Castellano", shopOrderStatusPaid: "Pago recibido", shopOrderStatusReceived: "Recibido", shopOrderMarkPaid: "Marcar pago recibido", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "Aún no tienes compras.", shopOrderPmSeller: "PM Seller", shopOrderSeller: "Vendedor", + shopOrderPaymentConcept: "Pago", shopOrderInvoice: "Factura", shopOrderInvoiceConcept: "Factura", shopOrderStatusPending: "Pendiente", shopOrderStatusAccepted: "Aceptado", shopOrderStatusRejected: "Rechazado", shopOrderStatusShipped: "Enviado", + shopOrderStatusDelivered: "Entregado", shopOrderAccept: "Aceptar", shopOrderReject: "Rechazar", shopOrderMarkShipped: "Marcar enviado", + shopOrderMarkDelivered: "Marcar entregado", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "Contrato", shopOrderContractConcept: "Pedido de tienda", shopOrdersPending: "Pedidos pendientes", all: "Todos", @@ -50,13 +75,14 @@ module.exports = { viewJson: "Ver JSON", matchScore: "Puntuación de coincidencia", preferencesLabel: "Preferencias", - inhabitantProfileTitle: "Habitante", + inhabitantProfileTitle: "Perfil del habitante", contentWarningLabel: "Aviso de contenido", invalidPost: "Contenido no válido", invalidPostHint: "Este mensaje tiene texto vacío o no válido.", spreadBy: "Por", - spreadChron: "Réplicas", - spreaded: "Replicado", + spreadChron: "Difusión", + spreaded: "Difundido", + spreadMore: "más", spreadContentUnavailable: "Contenido aún no disponible (replicación pendiente)", filteredByTag: "Filtrado por etiqueta", filteredByTagTitle: "Filtrado por etiqueta", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "Sin gravedad", calendarDeleteDate: "Eliminar fecha", calendarIntervalUntil: "Hasta", + bookmarkCategory: "Categoría", bookmarkLastVisit: "Última visita", profileClearnetBookmarksLabel: "Marcadores", + forumFilterHot: "Populares", invitesPort: "Puerto", currentlyUnrecheable: "¡ERROR!", peerHostValidation: "IPv4 válida (p. ej. 192.168.1.100) o nombre de host (p. ej. pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "Estimación máx. anual", statsCarbonNetwork: "Total de la red", statsCarbonOfEstMax: "de la capacidad máx. estimada", - statsCarbonYearProgress: "del año transcurrido", - statsNetworkTitle: "Red", - statsStorageTitle: "Almacenamiento", - statsEcoTaxTitle: "Tasa ECO", - statsAccountTitle: "Cuenta", - statsAveragesTitle: "Promedios", statsCarbonOfNetwork: "del total de la red", statsCarbonUser: "Tu huella", statsContentCountColumn: "Cantidad", @@ -154,6 +176,10 @@ module.exports = { statsTopTagsTitle: "Etiquetas principales", statsTopTypesTitle: "Tipos de contenido principales", statsTotalMsgs: "Mensajes totales", + feedViewDetails: "Ver detalles", + eventFileLabel: "Adjuntar imagen", + taskFileLabel: "Adjuntar imagen", + courtsRespondentRequired: "El ID del demandado es obligatorio", extended: "Multiverso", extendedDescription: [ "Cuando apoyas a alguien, puedes descargar las publicaciones de habitantes que apoya, y esas publicaciones aparecerán aquí, ordenadas por la más reciente.", @@ -197,22 +223,29 @@ module.exports = { graphosShowing: "Mostrando", graphosYou: "Tú", graphosTotalNodes: "Nodos totales", + manualMode: "Modo Manual", mentions: "Menciones", mentionsDescription: [ - strong("Todos los mensajes que te @mencionan"), - ".", + strong("Publicaciones que te @mencionan"), + ", ordenadas por las más recientes.", ], + nextPage: "Siguiente", + previousPage: "Anterior", noMentions: "No has recibido @menciones, aún.", - mentionsSearchPlaceholder: "Buscar menciones...", privateReminders: "RECORDATORIOS", inboxFiltersLabel: "Filtros:", inboxToggleToMutuals: "Cambiar a mutuos", inboxToggleToWhole: "Cambiar a todos", + privateFrom: "De", + privateTo: "Para", privateDate: "Fecha", peers: "Nodos", searchDescription: "Descripición", + imageSearch: "Buscar Imágenes", searchFromLabel: "Desde", searchToLabel: "Hasta", + searchMinOpinionsLabel: "Mín. opiniones", + searchInhabitantLabel: "Habitante (Oasis ID)", searchQueryLabel: "Búsqueda", searchPerPageLabel: "Resultados por página", settings: "Configuración", @@ -224,38 +257,67 @@ module.exports = { melodyTitle: 'Melody', melodyDescription: 'Reproduce la melodía de tu blockchain — cada bloque se convierte en una nota.', melodyEmpty: 'Aún no hay notas — tu blockchain todavía no ha generado ningún bloque.', + melodyCompositionHelp: 'Cada bloque de tu blockchain se convierte en una nota musical según su tipo y tamaño.', + melodyStatsTitle: 'Desglose por tipo', + melodyInhabitantLabel: 'Habitante', melodyTotalBlocks: 'Notas', + melodyType: 'Tipo', + melodyCount: 'Bloques', melodyFilterAll: 'TODAS', + melodyFilterShare: 'Subir melodía', + melodyShareTitle: 'Comparte tu melodía', + melodyShareHelp: 'Publica una instantánea de tu melodía actual para que otros la escuchen y remezclen.', + melodyShareTitleLabel: 'Título (opcional)', + melodyShareTitlePlaceholder: 'Un fantasma de blockchain', + melodyShareSubmit: 'Publicar Melodía', + melodyNoShared: 'Aún no hay melodías de blockchain.', melodyRegenerate: 'Regenerar', melodyDownload: "Descargar BCS", + profileActivityTitle: 'Actividad', + profileActivityMore: 'Ver toda la actividad', profileContentSectionTitle: 'Contenido del Avatar', profileContentHelp: 'Elige qué módulos se mostrarán en tu avatar.', profileSensorsHelp: 'Métricas opcionales que se muestran en tu avatar.', profileClearnetHelp: 'Módulos que pueden ser consultados desde fuera de Oasis.', profileHubAll: 'Todo', + profileHubTitle: 'Contenido Público', + footerPulseTitle: 'Pulso', + footerPulsePeers: 'Pares', + footerPulseInbox: 'Bandeja', + footerPulseSync: 'Última sinc.', + footerPulseEcoValue: 'ECO/h', + footerPulseLastActivity: 'Última actividad', footerLicenseLabel: 'Licencia', profileSwitchToOasis: 'Volver a Oasis', - profileVisibilityFediverse: "Multiverso", + profileSwitchToClearnet: 'Sumérgete en Clearnet', + profileVisibilityFediverse: "Fediverse", + profileSwitchToFediverse: "Sumérgete en el Fediverso", + coordLabel: 'Coordenadas (e.g., A3)', + coordPlaceholder: 'Introduce coordenada', contributorsTitle: "Contribuciones", + pixeliaBy: "por", colorLabel: 'Elige un color', paintButton: 'Dibujar!', + invalidCoordinate: 'Coordenada incorrecta', + goToMuralButton: "Ver Mural", totalPixels: 'Total Pixeles', modules: "Módulos", modulesViewTitle: "Módulos", modulesViewDescription: "Define tu entorno ideal activando y desactivando módulos.", inbox: "Buzón", multiverse: "Multiverso", - fediverse: "Multiverso", - fediverseDescription: "Gestiona tus otras cuentas del multiverso, incluyendo enviar y recibir contenido.", + fediverse: "Fediverse", + fediverseDescription: "Gestiona tus otras cuentas del fediverso, incluyendo enviar y recibir contenido.", fediverseStatus: "Estado", - fediverseDisconnected: "Multiverso desconectado. Comprueba %LINK% o el estado de la conexión.", - fediverseSettingsTitle: "Multiverso", + fediverseDisconnected: "Fediverse desconectado. Comprueba %LINK% o el estado de la conexión.", + fediverseSettingsTitle: "Fediverse", fediverseInstanceLabel: "Dirección", fediverseTokenLabel: "Token de acceso", fediverseTokenHelp: "Configura tu token de acceso. Se usará para gestionar tu cuenta de Mastodon desde Oasis.", fediverseConnect: "Conectar", fediverseConnectedAs: "Conectado como", fediverseDisconnect: "Desconectar", + fediverseEmpty: "Cuando conectes otras cuentas del fediverso desde %LINK%, podrás gestionarlas desde aquí.", fediverseEmptyLink: "tus ajustes", fediverseComposePlaceholder: "¿Qué estás pensando?", fediverseReplyPlaceholder: "Escribe tu respuesta…", @@ -264,15 +326,21 @@ module.exports = { fediverseReply: "Responder", fediverseBoosted: "ha impulsado", fediverseNoPosts: "Tu timeline está vacío.", + fediverseThread: "Hilo", fediverseTimeline: "Cronologías", + fediverseManageTimeline: "Gestionar cronología", fediverseFollowers: "Seguidores", fediverseFollowing: "Siguiendo", fediversePosts: "Publicaciones", fediverseJoined: "Se unió", + fediverseOverview: "Resumen", fediverseRefresh: "Actualizar", fediverseWrite: "Escribir", fediversePreview: "Vista previa", + fediverseEdit: "Editar", + fediverseOpenFeed: "Visitar feed", fediverseManage: "Gestionar", + fediverseStatusNotConnected: "No conectado", fediverseError: "No se pudo contactar con Mastodon.", fediverseErrInstance: "URL de instancia no válida.", fediverseErrAuth: "Token no válido o caducado.", @@ -283,6 +351,17 @@ module.exports = { fediverseErrPost: "No se pudo publicar.", fediverseErrMedia: "No se pudo subir el archivo.", fediverseErrEmpty: "Escribe algo o adjunta un archivo.", + popularLabel: "Destacadas", + topicsLabel: "Tópicos", + latestLabel: "Últimas", + summariesLabel: "Resúmenes", + threadsLabel: "Hilos", + multiverseLabel: "Multiverso", + inboxLabel: "Buzón", + invitesLabel: "Invitaciones", + walletLabel: "Cartera", + legacyLabel: "Llaves", + cipherLabel: "Cifrador", bookmarksLabel: "Marcadores", videosLabel: "Vídeos", torrentsLabel: "Torrents", @@ -293,14 +372,18 @@ module.exports = { inhabitantsLabel: "Habitantes", trendingLabel: "Tendencias", eventsLabel: "Eventos", + tasksLabel: "Tareas", apply: "Aplicar", menuPersonal: "Personal", + menuContent: "Contenido", + menuBlogs: "Blogs", menuGovernance: "Gobernanza", menuOffice: "Oficina", - menuNetwork: "Comunidad", + menuMultiverse: "Multiverso", + menuNetwork: "Red", menuCreative: "Creativo", menuEconomy: "Economía", - menuMedia: "Biblioteca", + menuMedia: "Multimedia", menuTools: "Herramientas", comment: "Comentar", subtopic: "Subtema", @@ -310,6 +393,13 @@ module.exports = { follow: "Soportar", block: "Bloquear", unblock: "Desbloquear", + newerPosts: "Nuevos posts", + olderPosts: "Viejos posts", + feedRangeEmpty: "El rango aplicado está vación para éste feed. Trata de ver el ", + seeFullFeed: "feed completo", + feedEmpty: "La red Oasis no ha visto nunca posts desde ésta cuenta.", + beginningOfFeed: "Éste es el comienzo del feed", + noNewerPosts: "No se han recibido posts nuevos, aún.", relationshipNotFollowing: "No te da soporte", relationshipTheyFollow: "Te da soporte", relationshipMutuals: "Soporte mútuo", @@ -319,20 +409,25 @@ module.exports = { relationshipBlockedBy: "Te bloquea", relationshipMutualBlock: "Bloqueo mutuo", relationshipNone: "No estás dando soporte", + relationshipConflict: "Conflicto", relationshipBlockingPost: "Post bloqueado", viewLikes: "Ver replicas", - spreadedDescription: "Lista de contenidos replicados por habitante.", + spreadedDescription: "Lista de posts replicados por habitante.", + totalspreads: "Replicas totales", + attachFiles: "Adjuntar ficheros", preview: "Previsualizar", publish: "Publicar", contentWarningPlaceholder: "Añade un título a tu post (opcional)", - publishWarningPlaceholder: "Cuerpo limitado a 7000 caracteres.", - publishTooLong: "Tu publicación es demasiado larga. Acórtala, por favor.", + privateWarningPlaceholder: "Añade habitantes para enviar un post privado (opcional)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "RECUERDA: Debido a la tecnología blockchain, una vez has publicado algo, no puede ser editado ni borrado. Piénsalo bien!.", ], commentWarning: [ "RECUERDA: Debido a la tecnología blockchain, una vez has publicado algo, no puede ser editado ni borrado. Piénsalo bien!.", ], + commentPublic: "público", + commentPrivate: "privado", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -352,6 +447,7 @@ module.exports = { ".", ], publishCustom: "Escribir un post avanzado", + subtopicLabel: "Crea un subtopic para éste post", messagePreview: "Previsualización", mentionsMatching: "Menciones", mentionsName: "Nombre", @@ -374,93 +470,21 @@ module.exports = { ssbLogStream: "Blockchain", ssbLogStreamDescription: "Configura el límite de mensajes para los flujos de la blockchain.", saveSettings: "Guardar configuración", + exportTitle: "Exportar datos", exportDescription: "Establece una contraseña (min 32 caracteres de longitud) para cifrar tu secreto", exportDataTitle: "Copia de Seguridad", exportDataDescription: "Descargar tu blockchain (llave privada excluida!)", exportDataButton: "Descargar blockchain", + pubWallet: "Cartera del PUB", + pubWalletDescription: "Establece la URL del wallet del PUB. Esta se utilizará para las transacciones del PUB (incluída la RBU).", + pubWalletConfiguration: "Guardar Configuración", + importTitle: "Importar datos", importDescription: "Importa tu secreto cifrado (llave privada) para activar tu avatar", + importAttach: "Adjuntar fichero cifrado (.enc)", + passwordLengthInfo: "La contraseña debe tener mínimo 32 caracteres de longitud.", passwordImport: "Escribe tu contraseña para descifrar los datos que serán guardados en tu sistema (nombre: secret)", exportPasswordPlaceholder: "Utiliza minúsculas, mayúsculas, números y símbolos", - fileInfo: "Tu secreto cifrado se descargará en tu dispositivo (nombre: oasis.enc)", - developer: "Desarrollo", - devTitle: "Desarrollo", - welcomeBannerText: "Te damos la bienvenida a OASIS. Sigue estos pasos rápidos para configurar tu avatar.", - aiSuggestionBanner: "Sugerencia:", - aiSuggestionsEnable: "Sugerencias de la IA", - welcomeBannerAction: "¡Empezar!", - welcomeTitle: "Bienvenida", - welcomeStepGreetingAction: "Enviar feed", - welcomeJoinPubButton: "Unirse al pub", - welcomeStepLarpAction: "Unirse a \"La Academia\"", - welcomeStepLarpText: "Únete a nuestro juego de rol de acción real.", - welcomeStepLarpTitle: "Únete al L.A.R.P.", - welcomeStepGreetingTitle: "Envía un \"Hola mundo!\"", - welcomeStepGreetingText: "Envía un mensaje para que otros habitantes puedan descubrirte.", - welcomeJoinPub: "Unirse a", - welcomeDescription: "Unas pocas cosas que conviene hacer antes de empezar.", - welcomeProgress: "Completado", - welcomeStepDone: "hecho", - welcomeStepPending: "pendiente", - welcomeSkip: "Ahora no", - welcomeStepLanguageTitle: "Elige tu idioma", - welcomeStepLanguageText: "Oasis habla varios idiomas. Puedes cambiarlo cuando quieras.", - welcomeStepProfileTitle: "Edita tu perfil", - welcomeStepProfileText: "Elige un nombre, añade una descripción y pon una imagen para que el resto de habitantes te reconozca.", - welcomeStepProfileAction: "Guardar perfil", - welcomeStepFederationTitle: "Únete a la red principal", - welcomeStepFederationText: "Conéctate a nuestro pub para encontrar a otros habitantes y empezar a replicar.", - welcomeStepFederationAction: "Ir a invitaciones", - welcomeStepBackupTitle: "Copia de seguridad de tu ID", - welcomeStepBackupText: "Tu identidad es un fichero de claves en este dispositivo.", - welcomeStepBackupWarning: "SI LO PIERDES, NADIE PUEDE RECUPERARLO POR TI.", - welcomeStepBackupAction: "¡Copia de seguridad!", - welcomeCompleteTitle: "Todo listo.", - welcomeCompleteText: "Tu nodo está preparado. A partir de aquí la red crece con la gente a la que sigues.", - welcomeFindPeople: "Buscar habitantes", - devFilterFiles: "FICHEROS", - devFilterSearch: "BUSCAR", - devFilterModules: "MÓDULOS", - devFilterTranslations: "TRADUCCIONES", - devFilterStyles: "ESTILOS", - devFilterTests: "TESTS", - devVersion: "Versión", - devNodeVersion: "Node", - devStatsTests: "Suites de tests", - devParentFolder: "Carpeta superior", - devOpenFolder: "abrir", - devDescription: "Explora el código que se está ejecutando en este nodo.", - devRoot: "raíz", - devSearchPlaceholder: "Texto a buscar en el código", - devAnyExtension: "Cualquier fichero", - devStatsFiles: "Ficheros", - devStatsLines: "Líneas", - devStatsModules: "Módulos", - devStatsLanguages: "Idiomas", - devNotViewable: "no visualizable", - devEmptyFolder: "Esta carpeta está vacía.", - devSize: "Tamaño", - devShowing: "Mostrando", - devLine: "línea", - devReportBug: "Reportar Bug", - devCreateTask: "Crear Tarea", - devRaw: "RAW", - devReportContext: "Encontrado leyendo el código de Oasis", - devTaskPrefix: "Revisar", - devPrevPage: "Líneas anteriores", - devNextPage: "Líneas siguientes", - devMatches: "coincidencias", - devFilesScanned: "ficheros revisados", - devNoResults: "Todavía no hay coincidencias.", - devColModule: "Módulo", - devColState: "Estado", - devColModel: "Modelo", - devColView: "Vista", - devColRoutes: "Rutas", - devColTests: "Tests", - devOn: "activo", - devOff: "inactivo", - modulesDevLabel: "Desarrollo", - modulesDevDescription: "Módulo para gestionar el código de Oasis.", + fileInfo: "Tu secreto cifrado será guardado en tu sistema (nombre: oasis.enc)", themeIntro: "Elige un tema.", setTheme: "Establecer tema", @@ -468,13 +492,16 @@ module.exports = { languageDescription: "Si quieres utilizar otro idioma, seleccionalo aquí.", setLanguage: "Establecer lenguaje", + peerConnections: "Nodos", peerConnectionsIntro: "Maneja todas tus conexiones con otros nodos.", peerConnectionsTitle: "Conexiones", online: "Conectado", offline: "Desconectado", discovered: 'Descubiertos', unknown: 'Desconocidos', + lanBroadcastLabel: "Difusión LAN", lanBroadcastActive: "Activa (multicast UDP en la LAN)", + lanBroadcastDisabled: "Desactivada (modo pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -487,6 +514,8 @@ module.exports = { noConnections: "No hay nodos conectados.", noDiscovered: "No has descubierto nodos.", noUnknownPeers: "Sin pares desconocidos en el grafo de amistades.", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -496,6 +525,9 @@ module.exports = { peerImport: "Importar", peerImportTitle: "Importar lista de peers", peerImportPlaceholder: "Pega una dirección multiserver por línea, o sube un archivo .txt…", + noSupportedConnections: "No soportas nodos.", + noBlockedConnections: "No bloqueas nodos.", + noRecommendedConnections: "No recomiendas nodos.", connectionActionIntro: "", startNetworking: "Iniciar red", @@ -509,19 +541,9 @@ module.exports = { homePageDescription: "Selecciona qué módulo quieres como tu página de inicio.", saveHomePage: "Establecer página de inicio", invites: "Invitaciones", + invitesTitle: "Invitaciones", + invitesInvites: "Invitaciones", invitesDescription: "Maneja y aplica códigos de invitación en tu red.", - invitesPubsHint: "Pega un código de invitación de un pub para federarte con él y empezar a replicar a sus habitantes.", - invitesFederationsHint: "Pubs con los que estás federado: actualízalos, quita los inalcanzables o exporta la lista.", - invitesHousesHint: "Introduce un código de casa para unirte a una casa del LARP y participar en su economía.", - invitesTribesHint: "Introduce un código de tribu para hacerte miembro y acceder a su contenido privado.", - invitesChatsHint: "Introduce un código de chat para unirte a la conversación y a sus hilos.", - invitesPadsHint: "Introduce un código de pad para escribir con otros en un documento compartido.", - invitesCalendarsHint: "Introduce un código de calendario para compartir fechas y recordatorios.", - invitesEventsHint: "Introduce un código de evento para unirte a un evento privado.", - invitesForumsHint: "Introduce un código de foro para leer y responder en un foro privado.", - invitesMapsHint: "Introduce un código de mapa para añadir y ver lugares en un mapa compartido.", - invitesShopsHint: "Introduce un código de tienda para unirte a una tienda y a su catálogo.", - invitesInhabitantsHint: "Introduce un código de habitante para seguiros mutuamente e intercambiar contenido.", invitesTribesTitle: "Tribus", invitesTribeInviteCodePlaceholder: "Introduce el código de la tribu", invitesTribeJoinButton: "Entrar en la tribu", @@ -552,6 +574,7 @@ module.exports = { invitesAlreadyFederated: "Ya estás federado con este pub.", invitesFederationsTitle: "Federaciones", invitesAcceptedInvites: "Federadas", + invitesNoInvites: "No hay invitaciones aceptadas, aún.", invitesNoFederatedPubs: "No hay pubs federados.", invitesUnreachablePubs: "Inalcanzables", invitesNoUnreachablePubs: "No hay pubs inalcanzables.", @@ -567,24 +590,39 @@ module.exports = { panicMode: "Modo Pánico!", encryptData: "Establece una contraseña (min 32 caracteres de longitud) para cifrar tu blockchain", decryptData: "Introduce contraseña para descifrar tu blockchain", + panicModeDescription: "Cifrar/Descifrar o BORRAR tu blockchain", removeDataDescription: "CUIDADO: Éste proceso no puede ser revocado.", + encryptPanicButton: "Cifrar blockchain", + decryptPanicButton: "Descifrar blockchain", removePanicButton: "BORRAR TODOS TUS DATOS!", searchTitle: "Buscar", searchDescriptionLabel: "Buscar contenido en tu red.", + searchLanguagesLabel: "Lenguajes", + searchSkillsLabel: "Habilidades", searchPlaceholder:"Buscar contenido...", + searchDateLabel:"Fecha", searchLocationLabel:"Localización", searchPriceLabel:"Precio", + searchUrlLabel:"URL", searchCategoryLabel:"Categoría", + searchStartLabel:"Fecha de comienzo", + searchEndLabel:"Fecha de fin", searchPriorityLabel:"Prioridad", searchStatusLabel:"Estado", statusLabel:"Estado", + totalVotesLabel:"Votos Totales", noResultsFound:"No se han encontrado resultados.", votesOption:"Opiniones Votadas", voteYesLabel:"Si", voteNoLabel:"No", allTypesLabel: "SIN LÍMITES", + ABSTENTIONLabel:"ABSTENCIÓN", YESLabel:"SI", NOLabel:"NO", + FOLLOW_MAJORITYLabel: "SEGUIR MAYORIA", + CONFUSEDLabel: "CONFUSO", + NOT_INTERESTEDLabel: "SIN INTERÉS", + StatusLabel:"Estado", votesOptionYesLabel:"Si", votesOptionNoLabel:"No", votesOptionAbstentionLabel:"Abstención", @@ -592,6 +630,7 @@ module.exports = { votesCreatedByLabel:"Creado el", voteStatusOpen:"ABIERTO", voteStatusClosed:"CERRADO", + imageSearchLabel: "Introduce palabras para buscar las imagenes que hayan sido etiquetadas con ellas.", commentDescription: ({ parentUrl }) => [ " ha comentado en ", a({ href: parentUrl }, " hilo"), @@ -602,20 +641,53 @@ module.exports = { a({ href: parentUrl }, " un post"), ], subtopicTitle: ({ authorName }) => [`Ha creado un subtopic en el post de @${authorName}`], + mysteryDescription: "publicado un post misterioso", oasisDescription: "OASIS Red de Proyectos", searchSubmit: "Buscar...", + postLabel: "POSTS", + aboutLabel: "HABITANTES", + feedLabel: "FEEDS", votesLabel: "GOBIERNO", + reportLabel: "REPORTES", + imageLabel: "IMAGENES", + videoLabel: "VIDEOS", + audioLabel: "AUDIOS", + documentLabel: "DOCUMENTOS", + torrentLabel: "TORRENTS", pdfFallbackLabel: "Documento PDF", + eventLabel: "EVENTOS", + taskLabel: "TAREAS", + transferLabel: "TRANSFERENCIAS", + curriculumLabel: "CURRÍCULUM", + bookmarkLabel: "ENLACES", + tribeLabel: "TRIBUS", + marketLabel: "MERCADO", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "CARTERAS", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "Aplicar", + subjectLabel: "Asunto", editProfile: "Editar Avatar", + profileQrAlt: "Código QR del perfil", + profileQrHint: "Escanea para copiar este Oasis ID.", oasisIdLabel: "OasisID", - spreadContent: "Replicar", + spreadLabel: "Difundir", + spreadHint: "Difunde esto a quienes te apoyan (se replica vía tu feed).", welcomePmSubject: "Hello World!", - welcomePmBody: "Te damos la bienvenida a Oasis — una red social libre, peer-to-peer, federada y cifrada, con una arquitectura distribuida y solo por invitación.\n\nAlgunos lugares interesantes para empezar:\n\n- /profile — Edita tu avatar, nombre, descripción y qué módulos se muestran en tu cara pública.\n- /invites — Añade un pub para federarte con el resto de la red.\n- /peers — Mira quién está conectado, reescanea tu LAN, exporta o importa listas de peers.\n- /inhabitants — Descubre y visita los avatares de otras personas.\n- /tribes — Crea o únete a grupos privados con cifrado de extremo a extremo.\n- /inbox — Lee y envía mensajes privados.\n- /activity — Mira la actividad reciente, tuya y de tu red.\n- /publish — Escribe un post o comparte una imagen, audio, vídeo o documento.\n- /search — Busca el contenido de la red por tipo.\n\nAlgunos lugares interesantes para conectar:\n\n- /multiverse — Gestiona tus otras cuentas del multiverso, incluyendo enviar y recibir contenido.\n\nEste mensaje se envió de forma privada desde ti hacia ti, por eso no hizo falta conexión para recibirlo.", + welcomePmBody: "Bienvenido a Oasis — una red social libre, peer-to-peer, federada y cifrada, con una arquitectura distribuida y solo por invitación.\n\nAlgunos lugares interesantes para empezar:\n\n- /profile — Edita tu avatar, nombre, descripción y qué módulos se muestran en tu cara pública.\n- /invites — Añade un pub para federarte con el resto de la red.\n- /peers — Mira quién está conectado, reescanea tu LAN, exporta o importa listas de peers.\n- /inhabitants — Descubre y visita los avatares de otras personas.\n- /tribes — Crea o únete a grupos privados con cifrado de extremo a extremo.\n- /inbox — Lee y envía mensajes privados.\n- /activity — Mira la actividad reciente, tuya y de tu red.\n- /publish — Escribe un post o comparte una imagen, audio, vídeo o documento.\n\nAlgunos lugares interesantes para conectar:\n\n- /fediverse — Gestiona tus otras cuentas del fediverso, incluyendo enviar y recibir contenido.\n\nEste mensaje se envió de forma privada desde ti hacia ti, por eso no hizo falta conexión para recibirlo.", + profileVisibilityTitle: "Visibilidad del perfil público", + profileVisibilityHint: "Marca qué campos ven otros habitantes en tu perfil. Por defecto solo se muestra Karma.", profileSensorsSectionTitle: "Sensores", profileVisibilityActivity: "Nivel de actividad", + profileVisibilityDevice: "Dispositivo", profileDeviceLockedHint: "Este sensor está siempre activo: permite a otros ver desde qué dispositivo te conectas y no se puede desactivar.", profileVisibilityKarma: "Puntuación KARMA", profileVisibilityUbi: "UBI", @@ -623,6 +695,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "Llave GPG", + profileVisibilityClearnet: "Publicar mi perfil", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Tiendas", profileClearnetJobsLabel: "Empleo", @@ -676,6 +749,7 @@ module.exports = { " o el estado de tu conexión.", ], walletSentToLine: ({ destination, amount }) => `Enviar ECO ${amount} a ${destination}`, + walletSettingsTitle: "Cartera", walletSettingsDescription: "Integrar Oasis con tu cartera de ECOin.", walletSettingsDocLink: "Guía de instalación de ECOin", walletStatusMessages: { @@ -697,19 +771,37 @@ module.exports = { cipherTitle: "Cifrador", cipherDescription: "Encripta y desencripta tu texto de forma simétrica (usando una contraseña compartida).", randomPassword: "Contraseña Aleatoria", + cipherEncryptTitle: "Encriptar Texto", + cipherEncryptDescription: "Introduce el texto para encriptar", + cipherTextLabel: "Texto para Encriptar", cipherTextPlaceholder: "Introduce el texto para encriptar...", cipherPasswordLabel: "Establece una contraseña (mínimo 32 caracteres) para encriptar tu texto", cipherPasswordDecryptLabel: "Establece una contraseña (mínimo 32 caracteres) para desencriptar tu texto", cipherPasswordPlaceholder: "Introduce una contraseña...", cipherEncryptButton: "Encriptar", + cipherDecryptTitle: "Desencriptar Texto", + cipherDecryptDescription: "Introduce el texto para desencriptar", cipherEncryptedMessageLabel: "Texto Encriptado", cipherDecryptedMessageLabel: "Texto Desencriptado", + cipherPasswordUsedLabel: "Contraseña usada para encriptar (guárdala!)", cipherEncryptedTextPlaceholder: "Introduce el texto encriptado...", + cipherIvLabel: "IV", + cipherIvPlaceholder: "Introduce el vector de inicialización...", cipherDecryptButton: "Desencriptar", password: "Contraseña", text: "Texto", encryptedText: "Texto Encriptado", iv: "Vector de Inicialización (IV)", + encryptTitle: "Encripta tu texto", + encryptDescription: "Introduce el texto que deseas encriptar y proporciona una contraseña.", + encryptButton: "Encriptar", + decryptTitle: "Desencripta tu texto", + decryptDescription: "Introduce el texto encriptado y proporciona la misma contraseña utilizada para la encriptación.", + decryptButton: "Desencriptar", + passwordLengthError: "La contraseña debe tener al menos 32 caracteres.", + missingFieldsError: "Texto, contraseña o IV no proporcionados.", + encryptionError: "Error al encriptar el texto.", + decryptionError: "Error al desencriptar el texto.", bookmarkTitle: "Marcadores", bookmarkDescription: "Descubre y gestiona marcadores en tu red.", bookmarkAllSectionTitle: "Marcadores", @@ -735,6 +827,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "Opcional", bookmarkTagsLabel: "Etiquetas", bookmarkTagsPlaceholder: "Introduce tags separadas por comas", + bookmarkCategoryLabel: "Categoría", + bookmarkCategoryPlaceholder: "Opcional", bookmarkLastVisitLabel: "Última visita", bookmarkSearchPlaceholder: "Buscar URL, tags, categoría, autor...", bookmarkSortRecent: "Más recientes", @@ -749,6 +843,8 @@ module.exports = { noLastVisit: "Sin última visita", videoTitle: "Vídeos", videoDescription: "Explora y gestiona contenido de vídeo en tu red.", + videoPluginTitle: "Título", + videoPluginDescription: "Descripción", videoMineSectionTitle: "Tus vídeos", videoCreateSectionTitle: "Subir vídeo", videoUpdateSectionTitle: "Actualizar vídeo", @@ -780,6 +876,7 @@ module.exports = { videoSortOldest: "Más antiguos", videoSortTop: "Más votados", videoSearchButton: "Buscar", + videoMessageAuthorButton: "MP", videoUpdatedAt: "Actualizado", videoNoMatch: "Ningún vídeo coincide con tu búsqueda.", documentTitle: "Documentos", @@ -820,6 +917,8 @@ module.exports = { documentUpdatedAt: "Actualizado", audioTitle: "Audios", audioDescription: "Explora y gestiona contenido de audio en tu red.", + audioPluginTitle: "Título", + audioPluginDescription: "Descripción", audioMineSectionTitle: "Tus audios", audioCreateSectionTitle: "Subir audio", audioUpdateSectionTitle: "Actualizar audio", @@ -830,6 +929,7 @@ module.exports = { audioFilterMine: "MÍOS", audioFilterRecent: "RECIENTES", audioFilterTop: "TOP", + audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Subir audio", audioUpdateButton: "Actualizar", audioDeleteButton: "Eliminar", @@ -856,7 +956,6 @@ module.exports = { audioFilterFavorites: "FAVORITOS", favoritesTitle: "Favoritos", favoritesDescription: "Todos tus elementos marcados como favoritos en un solo lugar.", - favoritesSearchPlaceholder: "Buscar en favoritos...", favoritesFilterAll: "TODOS", favoritesFilterRecent: "RECIENTES", favoritesFilterAudios: "AUDIOS", @@ -872,13 +971,18 @@ module.exports = { favoritesNoItems: "Todavía no hay favoritos.", yourContacts: "Tus Contactos", allInhabitants: "Habitantes", - allCVs: "CVs", + allCVs: "Todos los CVs", discoverPeople: "Descubre habitantes en tu red.", + allInhabitantsButton: "TODOS", + contactsButton: "SOPORTES", + CVsButton: "CVs", + matchSkills: "Coincidir Habilidades", + matchSkillsButton: "COINCIDIR HABILIDADES", + suggestedButton: "SUGERIDOS", searchInhabitantsPlaceholder: "FILTRAR habitantes POR NOMBRE …", filterLocation: "FILTRAR habitantes POR UBICACIÓN …", filterLanguage: "FILTRAR habitantes POR IDIOMA …", filterSkills: "FILTRAR habitantes POR HABILIDADES …", - cvSkillsCount: "Habilidades", applyFilters: "Aplicar Filtros", locationLabel: "Ubicación", languagesLabel: "Idiomas", @@ -887,18 +991,17 @@ module.exports = { mutualFollowers: "Seguidores Mutuos", suggestedFollowsYou: "Te sigue", latestInteractions: "Últimas Interacciones", + viewAvatar: "Ver Avatar", + viewCV: "Ver CV", suggestedSectionTitle: "Sugeridos", topkarmaSectionTitle: "Mejor Karma", topecoSectionTitle: "Mejor Eco", topactivitySectionTitle: "Top Actividad", - "TOP INACTIVITYButton": "MÁS INACTIVOS", - topinactivitySectionTitle: "Habitantes menos activos", blockedSectionTitle: "Bloqueados", gallerySectionTitle: "GALERÍA", - topSectionTitle: "TOP", + blockedButton: "BLOQUEADOS", blockedLabel: "Usuario Bloqueado", inhabitantviewDetails: "Ver Detalles", - cvVisitButton: "Visitar CV", keepReading: "Seguir leyendo...", oasisId: "ID", noInhabitantsFound: "No se encontraron habitantes, aún.", @@ -911,7 +1014,6 @@ module.exports = { parliamentFilterCandidatures: "CANDIDATURAS", parliamentFilterProposals: "PROPUESTAS", parliamentFilterLaws: "LEYES", - parliamentFilterRevocations: "REVOCACIONES", parliamentFilterHistorical: "HISTÓRICO", parliamentFilterLeaders: "LÍDERES", parliamentFilterRules: "REGLAS", @@ -937,8 +1039,10 @@ module.exports = { parliamentPoliciesDeclined: "LEYES RECHAZADAS", parliamentPoliciesDiscarded: "LEYES DESCARTADAS", parliamentEfficiency: "% EFICIENCIA", + parliamentFilterRevocations: 'REVOCACIONES', parliamentRevocationFormTitle: 'Revocar Ley', parliamentRevocationLaw: 'Ley', + parliamentRevocationTitle: 'Título', parliamentRevocationReasons: 'Motivos', parliamentRevocationPublish: 'Publicar Revocación', parliamentCurrentRevocationsTitle: 'Revocaciones Actuales', @@ -951,12 +1055,23 @@ module.exports = { parliamentNoGovernments: "Todavía no hay gobiernos en el histórico.", parliamentCandidatureFormTitle: "Proponer Candidatura", parliamentCandidatureIdPh: "Oasis ID (@...) o nombre de la tribu", + parliamentCandidatureSlogan: "Lema (máx. 140 car.)", + parliamentCandidatureSloganPh: "Un lema corto", parliamentCandidatureMethod: "Método", parliamentCandidatureProposeBtn: "Publicar Candidatura", + parliamentThDate: "Fecha de propuesta", + parliamentThSlogan: "Lema", + parliamentThSince: "Perfil desde", + parliamentThVotes: "Votos recibidos", + parliamentThVoteAction: "Votar", + parliamentTypeUser: "Habitante", + parliamentTypeTribe: "Tribu", parliamentVoteBtn: "Votar", parliamentProposalFormTitle: "Proponer Ley", parliamentProposalDescription: "Descripción (≤1000)", parliamentProposalPublish: "Publicar Propuesta", + parliamentFinalize: "Finalizar", + parliamentDeadline: "Fecha límite", parliamentNoProposals: "Aún no hay propuestas de ley.", parliamentNoLaws: "Aún no hay leyes aprobadas.", parliamentMethodDEMOCRACY: "Democracia", @@ -974,21 +1089,29 @@ module.exports = { parliamentVotesSlashTotal: "Votos/Total", parliamentVotesNeeded: "Votos Necesarios", parliamentFutureLawsTitle: "Futuras leyes", + parliamentNoFutureLaws: "Aún no hay futuras leyes.", parliamentVoteAction: "Votar", + parliamentLeadersTitle: "Líderes", parliamentThLeader: "AVATAR", parliamentPopulation: "Población", + parliamentThType: "Tipo", + parliamentThInPower: "Gobiernos", parliamentThPresented: "CANDIDATURAS", parliamentNoLeaders: "Aún no hay líderes.", parliamentCandidaturesListTitle: "Lista de Candidaturas", parliamentTimeRemaining: "Tiempo restante", + parliamentNoLeader: "Aún no hay candidatura ganadora.", parliamentElectionsStatusTitle: "Próximo Gobierno", parliamentProposalDeadlineLabel: "Fecha límite", parliamentProposalTimeLeft: "Tiempo restante", + parliamentProposalOnTrack: "Con apoyo suficiente", + parliamentProposalOffTrack: "Apoyo insuficiente", parliamentRulesTitle: "Cómo funciona el Parlamento", parliamentRulesIntro: "Las elecciones se resuelven cada 2 meses; las candidaturas son continuas y se reinician cuando se elige un gobierno.", parliamentRulesCandidates: "Cualquier habitante puede proponerse a sí mismo, a otro habitante o a cualquier tribu. Cada habitante puede proponer hasta 3 candidaturas por ciclo; se rechazan duplicados en el mismo ciclo.", parliamentRulesElection: "Gana la candidatura con más votos en el momento de la resolución.", parliamentRulesTies: "Desempate: mayor karma del habitante; si persiste, perfil más antiguo; si persiste, propuesta más temprana; después, orden lexicográfico por ID.", + parliamentRulesFallback: "Si nadie vota, gana la última candidatura propuesta. Si no hay candidaturas, se selecciona una tribu al azar.", parliamentRulesTerm: "La pestaña Gobierno muestra el gobierno actual y sus estadísticas.", parliamentRulesMethods: "Formas: Anarquía (mayoría simple), Democracia (50%+1), Mayoría (80%), Minoría (20%), Karmatocracia (propuestas con mayor karma), Dictadura (aprobación instantánea).", parliamentRulesAnarchy: "Anarquía es el modo por defecto: si al resolver no se elige ninguna candidatura, se proclama la Anarquía. En Anarquía, cualquier habitante puede proponer leyes.", @@ -1010,7 +1133,11 @@ module.exports = { courtsCaseFormTitle: "Abrir caso", courtsCaseRespondent: "Acusado / Parte demandada", courtsCaseRespondentPh: "ID de Oasis (@...) o nombre de Tribu", + courtsCaseMediatorsAccuser: "Mediadores (acusación)", courtsCaseMethod: "Método de resolución", + courtsCaseDescription: "Descripción (máx. 1000 caracteres)", + courtsCaseEvidenceTitle: "Pruebas del caso", + courtsCaseEvidenceHelp: "Adjunta imágenes, audios, documentos (PDF) o vídeos que apoyen tu caso.", courtsCaseSubmit: "Presentar caso", courtsNominateJudge: "Nominar juez", courtsJudgeId: "Juez", @@ -1056,6 +1183,22 @@ module.exports = { courtsRulesMisconduct: "El acoso, la manipulación o las pruebas fabricadas pueden llevar a una resolución negativa inmediata.", courtsRulesGlossary: "Caso: registro de un conflicto. Pruebas: materiales que respaldan las afirmaciones. Veredicto: decisión con remedio. Apelación: solicitud para revisar el veredicto.", courtsFilterActions: "ACCIONES", + courtsNoActions: "No hay acciones pendientes para tu rol.", + courtsCaseTitlePlaceholder: "Breve descripción del conflicto", + courtsCaseSeverity: "Severidad", + courtsCaseSeverityNone: "Sin etiqueta de severidad", + courtsCaseSeverityLOW: "Baja", + courtsCaseSeverityMEDIUM: "Media", + courtsCaseSeverityHIGH: "Alta", + courtsCaseSeverityCRITICAL: "Crítica", + courtsCaseSubject: "Tema", + courtsCaseSubjectNone: "Sin etiqueta de tema", + courtsCaseSubjectBEHAVIOUR: "Comportamiento", + courtsCaseSubjectCONTENT: "Contenido", + courtsCaseSubjectGOVERNANCE: "Gobernanza / normas", + courtsCaseSubjectFINANCIAL: "Finanzas / recursos", + courtsCaseSubjectOTHER: "Otro", + courtsHiddenRespondent: "Oculto (solo visible para los roles implicados).", courtsThRole: "Rol", courtsRoleAccuser: "Acusación", courtsRoleDefence: "Defensa", @@ -1066,19 +1209,51 @@ module.exports = { courtsAssignJudgeBtn: "Elegir juez", trendingTitle: "Tendencias", exploreTrending: "Explora el contenido más popular en tu red.", + bookmarkButton: "MARCADORES", + transferButton: "TRANSFERENCIAS", + eventButton: "EVENTOS", + taskButton: "TAREAS", votesButton: "GOBIERNO", - shopProductButton: "TIENDA", + reportButton: "INFORMES", + feedButton: "FEED", + marketButton: "MERCADO", + imageButton: "IMÁGENES", + audioButton: "AUDIOS", + videoButton: "VIDEOS", + documentButton: "DOCUMENTOS", + torrentButton: "TORRENTS", + noTrendingFound: "No se encontraron contenidos populares.", + noContentMessage: "No hay contenido popular disponible, aún.", + trendingDescription: "Descripción", + trendingDate: "Fecha", + trendingLocation: "Ubicación", + trendingPrice: "Precio", + trendingUrl: "URL", + trendingCategory: "Categoría", + trendingStart: "Inicio", + trendingEnd: "Fin", + trendingPriority: "Prioridad", + trendingStatus: "Estado", + trendingFrom: "Desde", + trendingTo: "Hasta", + trendingConcept: "Concepto", + trendingAmount: "Cantidad", + trendingDeadline: "Plazo", + trendingItemStatus: "Estado del Artículo", + trendingTotalVotes: "Total de Votos", trendingTotalOpinions: "Total de Opiniones", - trendingNoContentMessage: "Todavía no hay tendencias.", + trendingNoContentMessage: "No hay tendencias disponibles, aún.", + trendingAuthor: "Por", + trendingCreatedAtLabel: "Creado el", trendingTotalCount: "Total de Contadores", tasksTitle: "Tareas", tasksDescription: "Descubre y gestiona tareas en tu red.", - taskSearchPlaceholder: "Buscar tareas...", taskTitleLabel: "Título", taskDescriptionLabel: "Descripción", taskStartTimeLabel: "Fecha de Inicio", taskEndTimeLabel: "Fecha de Fin", taskPriorityLabel: "Prioridad", + taskPrioritySelect: "Seleccionar prioridad", taskPriorityUrgent: "Urgente", taskPriorityHigh: "Alta", taskPriorityMedium: "Media", @@ -1088,13 +1263,13 @@ module.exports = { taskVisibilityLabel: "Visibilidad", taskPublic: "público", taskPrivate: "privado", + taskCreatedAt: "Creado el", + taskBy: "Por", taskStatus: "Estado", taskStatusOpen: "Abierta", taskStatusInProgress: "En Progreso", taskStatusClosed: "Cerrada", taskAssignedTo: "Asignada a", - taskAssignedChip: "Asignada", - taskUnassignedChip: "Sin asignar", taskAssignees: "Asignados", taskAssignButton: "Asignar a Mí", taskUnassignButton: "Desasignar", @@ -1107,6 +1282,7 @@ module.exports = { taskFilterInProgress: "EN-PROCESO", taskFilterClosed: "CERRADAS", taskFilterAssigned: "ASIGNADAS", + taskFilterArchived: "ARCHIVADAS", taskFilterUrgent: "URGENTES", taskFilterHigh: "ALTAS", taskFilterMedium: "MEDIAS", @@ -1119,21 +1295,23 @@ module.exports = { taskInProgressTitle: "Tareas en Progreso", taskClosedTitle: "Tareas Cerradas", taskAssignedTitle: "Tareas Asignadas", + taskArchivedTitle: "Tareas Archivadas", + taskPublicTitle: "Tareas Públicas", + taskPrivateTitle: "Tareas Privadas", notasks: "No hay tareas disponibles.", noLocation: "No se especificó ubicación", taskSetStatus: "Establecer estado", + eventTitle: "Eventos", eventDateLabel: "Fecha", - eventRecurrenceLabel: "Recurrencia", - eventRecurrenceUntil: "Repetir hasta", - eventRecurrenceHint: "Deja vacía la fecha de repetición para un evento único.", - eventUpcomingDates: "Próximas fechas", eventsTitle: "Eventos", eventsDescription: "Descubre y gestiona eventos en tu red.", - eventSearchPlaceholder: "Buscar eventos...", + eventDescription: "Descripción", eventPrice: "Precio", eventStatus: "Estado", + eventOrganizer: "Organizador", eventAllSectionTitle: "Eventos", eventMineSectionTitle: "Tus Eventos", + eventArchivedTitle: "Eventos Archivados", eventCreateSectionTitle: "Crear Evento", eventUpdateSectionTitle: "Actualizar Evento", eventDeleteButton: "Eliminar", @@ -1141,26 +1319,11 @@ module.exports = { eventAddToCalendar: "Añadir al Calendario", eventVisitCalendar: "Visitar Calendario", viewTask: "Ver Tarea", - viewEvent: "Ver evento", - viewPad: "Ver pad", - viewMap: "Ver mapa", - viewCalendar: "Ver calendario", viewReport: "Ver Reporte", viewTransfer: "Ver Transferencia", viewItem: "Ver Item", viewShop: "Ver Tienda", viewProject: "Ver Proyecto", - viewJob: "Ver Empleo", - viewPlace: "Ver Lugar", - generatePdf: "Generar PDF", - sharePm: "Compartir por PM", - galleryImages: "Imágenes (tamaño máx: 50MB cada una)", - galleryPhotos: "Imágenes", - galleryAddPhoto: "Añadir Imagen", - galleryRemovePhoto: "Quitar", - galleryVideo: "Vídeo (tamaño máx: 50MB)", - galleryAddVideo: "Añadir Vídeo", - galleryRemoveVideo: "Quitar vídeo", viewVotation: "Ver Votación", voteCastTitle: "Emitir Voto", voteResults: "Resultados", @@ -1168,15 +1331,29 @@ module.exports = { eventDescriptionLabel: "Descripción", eventDescriptionPlaceholder: "Introduce la descripción del evento...", eventUpdateButton: "Actualizar", + eventAttendeesLabel: "Asistentes", + eventAttendeesPlaceholder: "Introduce los asistentes, separados por comas...", eventTagsLabel: "Etiquetas", + eventTagsPlaceholder: "Introduce las etiquetas del evento, separadas por comas...", + eventTags: "Etiquetas", eventPriceLabel: "Precio", eventUrlLabel: "URL", eventAttendees: "Asistentes", + noAttendees: "Aún no hay asistentes", + eventCreatedAt: "Creado el", eventLocation: "Ubicación", eventLocationLabel: "Ubicación", + eventNoLocation: "No se especificó ubicación", + eventNoURL: "No se especificó URL", + eventBy: "Por", noevents: "No hay eventos disponibles.", eventDate: "Fecha", + eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "Asistir al Evento", + eventUnattendButton: "Dejar de asistir al Evento", + eventCreatedBy: "Creado por", + eventAttendeesCount: "Número de Asistentes", + eventCreatedByYou: "Tú creaste este evento", eventFilterAll: "TODOS", eventFilterMine: "MIOS", eventFilterToday: "HOY", @@ -1184,9 +1361,18 @@ module.exports = { eventFilterMonth: "ESTE MES", eventFilterYear: "ESTE AÑO", eventFilterArchived: "ARCHIVADOS", + eventTodayTitle: "Eventos de Hoy", + eventThisWeekTitle: "Eventos de Esta Semana", + eventThisMonthTitle: "Eventos de Este Mes", + eventThisYearTitle: "Eventos de Este Año", eventPrivacyLabel: "Visibilidad", eventPublic: "Público", eventPrivate: "Privado", + eventPublicTitle: "Eventos Públicos", + eventNoPrice: "Evento Gratuito", + eventNoImage: "No se ha subido ninguna imagen", + eventAttendConfirmation: "Ahora estás asistiendo a este evento", + eventUnattendConfirmation: "Ya no estás asistiendo a este evento", eventAttended: "Asisto", eventUnattended: "No asisto", eventStatusOpen: "Abierto", @@ -1197,14 +1383,9 @@ module.exports = { tagsTopSectionTitle: "Etiquetas Principales", tagsCloudSectionTitle: "Nube de Etiquetas", tagsFilterAll: "TODOS", - tagsFilterMine: "MÍAS", - tagsFilterRecent: "RECIENTES", - tagsMineSectionTitle: "Mis etiquetas", - tagsRecentSectionTitle: "Etiquetas recientes", tagsFilterTop: "TOP", tagsFilterCloud: "NUBE", tagsNoItems: "No hay etiquetas disponibles.", - tagsSearchPlaceholder: "Buscar etiquetas...", tagsTableHeaderTag: "ETIQUETA/ENLACE:", tagsTableHeaderCount: "CONTADOR:", transfersTitle: "Transferencias", @@ -1244,12 +1425,18 @@ module.exports = { transfersDeadline: "Plazo", transfersTags: "Etiquetas", transfersStatus: "Estado", + transfersStatusUnconfirmed: "NO CONFIRMADA", + transfersStatusClosed: "CERRADA", + transfersStatusDiscarded: "DESCARTADA", + transfersCreatedAt: "Creado el", transfersConfirmations: "CONFIRMACIONES", - transfersExportContract: "Generar Factura", + transfersContainingBlock: "Bloque contenedor", + transfersExportContract: "Crear contrato", transfersConfirmButton: "Confirmar Transferencia", transfersNoItems: "No se encontraron transferencias.", transfersMineSectionTitle: "Tus Transferencias", transfersUBISectionTitle: "Transferencias RBU", + transfersMarketSectionTitle: "Transferencias del Mercado", transfersTopSectionTitle: "Transferencias Principales", transfersPendingSectionTitle: "Transferencias Pendientes", transfersUnconfirmedSectionTitle: "Transferencias No Confirmadas", @@ -1257,13 +1444,21 @@ module.exports = { transfersDiscardedSectionTitle: "Transferencias Descartadas", transfersCreateSectionTitle: "Crear transferencia", transfersAllSectionTitle: "Transferencias", + transfersFilterFavs: "Favoritos", + transfersFavsSectionTitle: "Transferencias favoritas", + transfersSearchLabel: "Buscar", transfersSearchPlaceholder: "Buscar por concepto, tags, usuarios...", transfersMinAmountLabel: "Importe mín.", transfersMaxAmountLabel: "Importe máx.", + transfersSortLabel: "Ordenar por", transfersSortRecent: "Más recientes", transfersSortAmount: "Mayor importe", transfersSortDeadline: "Deadline más próximo", transfersSearchButton: "Buscar", + transfersFavoriteButton: "Favorito", + transfersUnfavoriteButton: "Quitar favorito", + transfersMessageUserButton: "Mensaje", + transfersExpiringSoonBadge: "CADUCA PRONTO", transfersExpiredBadge: "CADUCADA", transfersUpdatedAt: "Actualizado", transfersNoMatch: "No hay transferencias que coincidan con tu búsqueda.", @@ -1276,9 +1471,6 @@ module.exports = { voteClosedTitle: "Votaciones Cerradas", voteAllSectionTitle: "Gobierno", voteCreateButton: "Crear Votación", - voteErrorTitle: "Revisa el formulario", - voteErrorDeadlineMin: "La fecha límite debe ser de al menos {days} días desde ahora.", - voteErrorGeneric: "No se ha podido guardar la votación.", voteUpdateButton: "Actualizar", voteDeleteButton: "Eliminar", voteOptionYes: "SÍ", @@ -1308,6 +1500,16 @@ module.exports = { voteNoQuestion: "No se proporcionó pregunta", voteUnknownCreator: "Creador Desconocido", voteUnknownDate: "Fecha Desconocida", + errorVoteNotFound: "Votación no encontrada", + errorAlreadyVoted: "Ya has opnado.", + errorVoteClosedCannotEdit: "No puedes editar una votación cerrada", + errorVoteDeadlinePassed: "El plazo para esta votación ha pasado", + errorRetrievingVote: "Error al recuperar votación", + errorCreatingVote: "Error al crear votación", + errorVoteAlreadyVoted: "No se puede editar después de haber opinado", + errorDeletingOldVote: "Error al eliminar opinión anterior", + errorCreatingUpdatedVote: "Error al crear opinión actualizada", + errorCreatingTombstone: "Error al crear lápida", voteDetailSectionTitle: 'Detalles de la votación', voteCommentsLabel: 'Comentarios', voteCommentsForumButton: 'Abrir conversación', @@ -1317,6 +1519,7 @@ module.exports = { voteNewCommentButton: 'Publicar comentario', voteNewCommentLabel: 'Añade un comentario', cvTitle: "CV", + cvLabel: "Currículum Vitae (CV)", cvEditSectionTitle: "Editar CV", cvCreateSectionTitle: "Crear CV", cvDescription: "Gestiona y comparte tus habilidades profesionales e información.", @@ -1335,121 +1538,29 @@ module.exports = { cvLocationLabel: "Ubicación", cvStatusLabel: "Estado", cvPreferencesLabel: "Preferencias", + cvOasisContributorLabel: "Contribuyente de Oasis", cvPersonal: "Personal", cvOasis: "Contribuyente de Oasis (opcional)", - cvOasisContributorView: "Contribución", + cvOasisContributorView: "Contribución en Oasis", cvEducational: "Educativo (opcional)", cvEducationalView: "Educativo", cvProfessional: "Profesional (opcional)", cvProfessionalView: "Profesional", cvAvailability: "Disponibilidad (opcional)", + cvAvailabilityView: "Disponibilidad", cvUpdateButton: "Actualizar", cvCreateButton: "Crear CV", + cvContactLabel: "Contacto", cvCreatedAt: "Creado el", cvUpdatedAt: "Actualizado el", cvEditButton: "Actualizar", cvDeleteButton: "Eliminar", cvNoCV: "No se encontró ningún CV.", blogSubject: "Asunto", - blogMessage: "Mensaje", + blogMessage: "Cuerpo limitado a 8096 caracteres.", blogImage: "Subir contenido multimedia (max: 50MB)", - blogMedia: "Adjuntos (hasta 8 imágenes y un vídeo)", blogPublish: "Vista previa", - pollsTitle: "Encuestas", - dataTitle: "Coincidencias", - dataDescription: "Explora y visualiza las coincidencias de tu red.", - dataFilterAll: "TODO", - dataFilterMine: "MÍOS", - dataFilterRecent: "RECIENTES", - dataFilterTop: "TOP", - dataKindInhabitants: "HABITANTES", - dataKindJobs: "EMPLEOS", - dataKindProjects: "PROYECTOS", - dataKindEvents: "EVENTOS", - dataKindTribes: "TRIBUS", - dataKindMarket: "MERCADO", - dataKindHousing: "VIVIENDA", - dataKindIndustry: "INDUSTRIA", - dataKindTasks: "TAREAS", - dataKindReports: "INFORMES", - dataKindVotes: "VOTACIONES", - dataSearchPlaceholder: "Buscar en los datos cruzados...", - dataCohesionTitle: "Coeficiente de Cohesión (CC)", - dataCcShort: "CC", - dataCohesionHint: "Afinidad media entre todo lo que la red ha publicado.", - dataAffinity: "Afinidad", - dataCommonTerms: "Palabra clave", - dataStatPeople: "CVs", - dataStatConnected: "Conectados", - dataStatSkills: "Habilidades", - dataBestMatch: "Mejor match", - dataStatIsolated: "Aislados", - dataStatEntities: "Entidades comparadas", - dataStatTerms: "Términos distintos", - dataStatPairs: "Pares conectados", - dataMatchesTitle: "Coincidencias", - dataMatchesHint: "Sugerencias personalizadas del algoritmo.", - dataTermsTitle: "Palabras clave", - dataTermsHint: "Los términos que aparecen en más contenidos.", - dataConnections: "Coincidencias enlazadas", - dataTopicTitle: "Todo lo relacionado con", - dataTopicHint: "Lo que el algoritmo relaciona con este tema", - dataNoMatches: "No hay datos cruzados que mostrar.", - dataNoProfile: "Publica contenido o escribe tu CV para que el algoritmo aprenda qué te interesa.", - pollsDescription: "Módulo para preguntar a la red y contar las respuestas.", - pollFilterAll: "TODAS", - pollFilterMine: "MÍAS", - pollFilterRecent: "RECIENTES", - pollFilterTop: "TOP", - pollFilterVoted: "VOTADAS", - pollFilterOpen: "ABIERTAS", - pollFilterClosed: "CERRADAS", - pollCreateButton: "Crear Encuesta", - pollCreateTitle: "Crear Encuesta", - pollEditTitle: "Editar Encuesta", - pollQuestion: "Pregunta", - pollQuestionPlaceholder: "¿Qué quieres preguntar?", - pollOptions: "Opciones (una por línea)", - pollOutcome: "Resultado", - pollNoVotesYet: "Todavía sin votos", - pollTie: "Empate", - pollOptionsPlaceholder: "Plaza\nParque\nBar", - pollAnonymous: "Anónima", - pollAnonymousLabel: "Encuesta anónima", - pollMultiple: "Respuesta múltiple", - pollMultipleLabel: "Permitir varias respuestas", - pollDeadline: "Fecha límite", - pollTags: "Etiquetas", - pollPublishButton: "Publicar Encuesta", - pollUpdateButton: "Actualizar", - pollCloseButton: "Cerrar", - pollDeleteButton: "Eliminar", - pollVoteButton: "Votar", - pollChangeVote: "Cambiar Voto", - pollVoters: "Votantes", - pollComments: "Comentarios", - pollStatusLabel: "Estado", - pollStatusOpen: "ABIERTA", - pollStatusClosed: "CERRADA", - pollSearchPlaceholder: "Buscar encuestas...", - pollsNoItems: "No se han encontrado encuestas.", - viewPoll: "Ver Encuesta", - pollChatCreate: "Crear Encuesta", - pollInChat: "Encuesta", - blogTitle: "Blogs", - blogDescription: "Módulo para descubrir y gestionar blogs.", - blogFilterAll: "TODOS", - blogFilterMine: "MÍOS", - blogFilterRecent: "RECIENTES", - blogFilterFavorites: "FAVORITOS", - blogFilterTop: "TOP", - blogCreateButton: "Crear Blog", - blogSearchPlaceholder: "Buscar blogs...", - blogComments: "Comentarios", - blogAllowComments: "Permitir comentarios", - blogCommentsClosed: "El autor ha cerrado los comentarios de este blog.", - blogNoItems: "No se han encontrado blogs.", - viewBlog: "Ver Blog", + noPopularMessages: "No se han publicado mensajes populares, aún", forumTitle: "Foros", forumCategoryLabel: "Categoría", forumTitleLabel: "Título", @@ -1457,22 +1568,43 @@ module.exports = { forumCreateButton: "Crear foro", forumCreateSectionTitle: "Crear foro", forumDescription: "Habla abiertamente con habitantes de tu red.", - forumSearchPlaceholder: "Buscar foros...", forumFilterAll: "TODOS", forumFilterMine: "MÍAS", forumFilterRecent: "RECIENTES", forumFilterTop: "TOP", + forumMineSectionTitle: "Tus Foros", + forumRecentSectionTitle: "Foros Recientes", + forumAllSectionTitle: "Foros", forumDeleteButton: "Borrar", forumParticipants: "participantes", forumMessages: "mensajes", + forumLastMessage: "Último mensaje", forumMessageLabel: "Mensaje", forumMessagePlaceholder: "Escribe tu mensaje...", forumSendButton: "Enviar", + forumVisitForum: "Visitar Foro", noForums: "No hay foros disponibles, aún.", - forumVisitButton: "Visitar Foro", - forumTopicLabel: "Tema", + forumVisitButton: "Visitar foro", + forumCatGENERAL: "General", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", + forumCatPOLITICS: "Política", + forumCatTECH: "Tecnología", + forumCatSCIENCE: "Ciencia", + forumCatMUSIC: "Música", + forumCatART: "Arte", + forumCatGAMING: "Videojuegos", + forumCatBOOKS: "Libros", + forumCatFILMS: "Cine", + forumCatPHILOSOPHY: "Filosofía", + forumCatSOCIETY: "Sociedad", + forumCatPRIVACY: "Privacidad", + forumCatCYBERWARFARE: "Ciberguerra", + forumCatSURVIVALISM: "Supervivencia", imageTitle: "Imágenes", imageDescription: "Explora y gestiona contenido de imágenes en tu red.", + imagePluginTitle: "Título", + imagePluginDescription: "Descripción", imageMineSectionTitle: "Tus imágenes", imageCreateSectionTitle: "Subir imagen", imageUpdateSectionTitle: "Actualizar imagen", @@ -1481,12 +1613,14 @@ module.exports = { imageTopSectionTitle: "Top imágenes", imageFavoritesSectionTitle: "Favoritos", imageGallerySectionTitle: "Galería", + imageMemeSectionTitle: "Memes", imageFilterAll: "TODAS", imageFilterMine: "MÍAS", imageFilterRecent: "RECIENTES", imageFilterTop: "TOP", imageFilterFavorites: "FAVORITOS", imageFilterGallery: "GALERÍA", + imageFilterMeme: "MEMES", imageCreateButton: "Subir imagen", imageUpdateButton: "Actualizar", imageDeleteButton: "Eliminar", @@ -1499,6 +1633,7 @@ module.exports = { imageTitlePlaceholder: "Opcional", imageDescriptionLabel: "Descripción", imageDescriptionPlaceholder: "Opcional", + imageMemeLabel: "¿MEME?", imageNoFile: "No se ha proporcionado ningún archivo de imagen", noImages: "No hay imágenes disponibles.", imageSearchPlaceholder: "Buscar por título, etiquetas, descripción, autor...", @@ -1506,6 +1641,7 @@ module.exports = { imageSortOldest: "Más antiguas", imageSortTop: "Más votadas", imageSearchButton: "Buscar", + imageMessageAuthorButton: "Mensaje", imageUpdatedAt: "Actualizado", imageNoMatch: "Ninguna imagen coincide con tu búsqueda.", feedTitle: "Feed", @@ -1517,6 +1653,7 @@ module.exports = { createFeedButton: "Enviar Feed!", feedPlaceholder: "¿Qué está pasando? (máximo 280 caracteres)", TODAYButton: "HOY", + CREATEButton: "Crear Feed", totalOpinions: "Total de Opiniones", moreVoted: "Más Votado", noFeedsFound: "No se encontraron feeds.", @@ -1538,12 +1675,20 @@ module.exports = { concept: "Concepto", description: "Descripción", meme: "Meme", + activityContact: "Contacto", + activityBy: "Nombre", + activityPixelia: "Nuevo píxel añadido", + viewImage: "Ver imagen", + playAudio: "Reproducir audio", + playVideo: "Reproducir vídeo", typeRecent: "RECIENTE", - typeTop: "TOP", + errorActivity: "Error al recuperar la actividad", typePost: "BLOGS", + recentButton: "RECIENTE", typeTribe: "TRIBUS", typeLarp: "L.A.R.P.", typeInhabitants: "HABITANTES", + activityProfileUpdated: "actualizó su perfil", typeAbout: "HABITANTES", typeCurriculum: "CVS", typeImage: "IMÁGENES", @@ -1587,6 +1732,8 @@ module.exports = { typeCourtsSettlementAccepted: "Tribunales · Acuerdo aceptado", typeCourtsNomination: "Tribunales · Nominación", typeCourtsNominationVote: "Tribunales · Votación de nominación", + activitySupport: "Nueva alianza forjada", + activityJoin: "Nuevo PUB unido", question: "Pregunta", deadline: "Fecha límite", status: "Estado", @@ -1600,8 +1747,12 @@ module.exports = { date: "Fecha", category: "Categoría", attendees: "Asistentes", + activitySpread: "->", + visitLink: "Visitar enlace", + viewDocument: "Ver documento", location: "Ubicación", contentWarning: "Asunto", + personName: "Nombre del habitante", bankWalletConnected: "Monedero ECOin", bankUbiReceived: "UBI recibido", bankTx: "Tx", @@ -1610,6 +1761,7 @@ module.exports = { link: "Enlace", activityProjectFollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%", activityProjectUnfollow: "%OASIS% ahora está %ACTION% este proyecto %PROJECT%", + activityProjectPledged: "%OASIS% ha %ACTION% %AMOUNT% al proyecto %PROJECT%", following: "SIGUIENDO", unfollowing: "DEJANDO DE SEGUIR", pledged: "APORTADO", @@ -1655,17 +1807,26 @@ module.exports = { courtsVotesSlashTotal: "SÍ/TOTAL", courtsOpenVote: "Votación abierta", courtsAnswerTitle: "Respuesta", + courtsStanceADMIT: "Admite", + courtsStanceDENY: "Niega", + courtsStancePARTIAL: "Parcial", + courtsStanceCOUNTERCLAIM: "Reconvención", + courtsStanceNEUTRAL: "Neutral", courtsVerdictResult: "Resultado", courtsVerdictOrders: "Órdenes", courtsSettlementText: "Acuerdo", courtsSettlementAccepted: "Aceptado", + courtsSettlementPending: "Pendiente", courtsJudge: "Juez", courtsThSupports: "Apoyos", courtsFilterOpenCase: 'Abrir caso', + courtsEvidenceFileLabel: 'Archivo de prueba (imagen, audio, vídeo o PDF)', + courtsCaseMediators: 'Mediadores', courtsCaseMediatorsPh: 'IDs de Oasis de mediadores, separados por comas', courtsMediatorsLabel: 'Mediadores', courtsThCase: 'Caso', courtsThCreatedAt: 'Fecha de inicio', + courtsThActions: 'Acciones', courtsPublicPrefLabel: 'Visibilidad tras la resolución', courtsPublicPrefYes: 'Acepto que este caso sea totalmente público', courtsPublicPrefNo: 'Prefiero mantener privados los detalles', @@ -1673,11 +1834,8 @@ module.exports = { courtsMethodMEDIATION: 'Mediación', courtsNoCases: 'No hay casos.', reportsTitle: "Informes", - reportsDescription: "Gestiona y sigue los reportes de tu red.", - reportsSearchPlaceholder: "Buscar reportes...", + reportsDescription: "Gestiona y realiza un seguimiento de los informes relacionados con problemas, errores, abusos y advertencias de contenido en tu red.", reportsFilterAll: "TODOS", - reportsFilterRecent: "RECIENTES", - reportsFilterTop: "TOP", reportsFilterMine: "MIOS", reportsFilterFeatures: "FUNCIONES", reportsFilterBugs: "ERRORES", @@ -1690,13 +1848,18 @@ module.exports = { reportsFilterInvalid: "INVALIDOS", reportsCreateButton: "Crear Informe", reportsTitleLabel: "Título", + reportsDescriptionLabel: "Descripción", + reportsDescriptionPlaceholder: "Por favor, proporciona una descripción detallada.", reportsCategory: "Categoría", + reportsCategoryLabel: "Categoría", reportsCategoryFeatures: "Funciones", reportsCategoryBugs: "Errores", reportsCategoryAbuse: "Abuso", - reportsCategoryContent: "Contenido", + reportsCategoryContent: "Problemas de Contenido", reportsUpdateButton: "Actualizar", reportsDeleteButton: "Eliminar", + reportsDateLabel: "Fecha", + reportsUploadFile: "Subir contenido multimedia (max: 50MB)", reportsMineSectionTitle: "Tus Informes", reportsFeaturesSectionTitle: "Solicitudes de Funciones", reportsBugsSectionTitle: "Errores", @@ -1704,6 +1867,11 @@ module.exports = { reportsContentSectionTitle: "Problemas de Contenido", reportsAllSectionTitle: "Informes", reportsNoItems: "No hay informes disponibles.", + reportsValidationTitle: "Por favor, ingresa un título válido.", + reportsValidationDescription: "La descripción no puede estar vacía.", + reportsValidationCategory: "Por favor, selecciona una categoría.", + reportsCreatedAt: "Creado el", + reportsCreatedBy: "Por", reportsSeverity: "Gravedad", reportsSeverityLow: "Baja", reportsSeverityMedium: "Media", @@ -1714,10 +1882,13 @@ module.exports = { reportsStatusUnderReview: "En Revisión", reportsStatusResolved: "Resuelto", reportsStatusInvalid: "Inválido", + reportsUpdateStatusButton: "Actualizar Estado", + reportsAnonymityOption: "Enviar de forma anónima", + reportsAnonymousAuthor: "Anónimo", reportsConfirmButton: "CONFIRMAR INFORME!", reportsConfirmations: "Confirmaciones", reportsConfirmedSectionTitle: "Informes Confirmados", - reportsCreateTaskButton: "Crear Tarea", + reportsCreateTaskButton: "CREAR TAREA", reportsOpenSectionTitle: "Informes Abiertos", reportsUnderReviewSectionTitle: "Informes en Revisión", reportsResolvedSectionTitle: "Informes Resueltos", @@ -1760,20 +1931,6 @@ module.exports = { reportsRequestedActionLabel: 'Acción solicitada', reportsRequestedActionPlaceholder: 'Eliminar, ocultar, marcar, advertir, etc.', tribesTitle: "Tribus", - tribeFilterAll: "TODAS", - tribeFilterRecent: "RECIENTES", - tribeFilterMine: "MÍAS", - tribeFilterMembership: "MEMBRESÍA", - tribeFilterSubtribes: "SUBTRIBUS", - tribeFilterTop: "TOP", - tribeFilterGallery: "GALERÍA", - tribeFeedFilterTOP: "TOP", - tribeFeedFilterMINE: "MÍOS", - tribeFeedFilterALL: "TODOS", - tribeFeedFilterRECENT: "RECIENTES", - courtsStanceDENY: "Negar", - courtsStanceADMIT: "Admitir", - courtsStancePARTIAL: "Admitir en parte", tribeAllSectionTitle: "Tribus", tribeMineSectionTitle: "Tus Tribus", tribeCreateSectionTitle: "Crear Tribu", @@ -1785,6 +1942,13 @@ module.exports = { tribeviewSubTribeButton: "Visitar Sub-Tribu", tribeRootLabel: "RAÍZ", tribeDescription: "Explora o crea tribus en tu red.", + tribeFilterAll: "TODOS", + tribeFilterMine: "MÍAS", + tribeFilterMembership: "MIEMBROS", + tribeFilterRecent: "RECIENTES", + tribeFilterTop: "TOP", + tribeFilterSubtribes: "SUB-TRIBUS", + tribeFilterGallery: "GALERÍA", tribeMainTribeLabel: "TRIBU PRINCIPAL", tribeCreateButton: "Crear Tribu", tribeUpdateButton: "Actualizar", @@ -1803,8 +1967,13 @@ module.exports = { tribeModeLabel: "MODO", tribeIsAnonymousLabel: "ESTADO", tribeMembersCount: "Miembros", + tribeInviteCodePlaceholder: "Introduce el código de invitación", + tribeJoinByCodeButton: "Unirse con código", + tribeJoinButton: "UNIRSE", tribeEnterInvite: "INTRODUCIR INVITACIÓN", tribeLeaveButton: "DEJAR", + tribeYes: "SÍ", + tribeNo: "NO", tribePublic: "PÚBLICA", tribePrivate: "PRIVADA", tribeGenerateInvite: "INVITACIÓN ÚNICA", @@ -1813,8 +1982,13 @@ module.exports = { tribeRemoveInvitation: "ELIMINAR INVITACIÓN", tribeCreatedAt: "Creado el", tribeAuthor: "Por", + tribeAuthorLabel: "AUTOR", tribeStrict: "Estricto", tribeOpen: "Abierta", + tribeFeedFilterRECENT: "RECIENTES", + tribeFeedFilterMINE: "MIOS", + tribeFeedFilterALL: "TODOS", + tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Redifusiones", tribeFeedRefeed: "Redifundir", tribeFeedMessagePlaceholder: "Escribe un feed…", @@ -1824,12 +1998,17 @@ module.exports = { tribeNotFound: "Tribu no encontrada!", createTribeTitle: "Crear Tribu", updateTribeTitle: "Actualizar Tribu", + tribeSectionOverview: "Resumen", tribeSectionInhabitants: "Habitantes", tribeSectionVotations: "VOTACIONES", tribeSectionEvents: "EVENTOS", + tribeSectionReports: "Reportes", tribeSectionTasks: "TAREAS", tribeSectionFeed: "FEED", tribeSectionForum: "FORO", + tribeSectionMarket: "Mercado", + tribeSectionJobs: "Empleos", + tribeSectionProjects: "Proyectos", tribeSectionMedia: "Multimedia", tribeSectionImages: "IMÁGENES", tribeSectionAudios: "AUDIOS", @@ -1867,6 +2046,11 @@ module.exports = { tribeTaskStatusClosed: "CERRAR", tribeTaskAssign: "ASIGNAR", tribeTaskUnassign: "DESASIGNAR", + tribeReportCreate: "Crear Reporte", + tribeReportsEmpty: "No hay reportes, aún.", + tribeReportTitle: "Título", + tribeReportDescription: "Descripción", + tribeReportCategory: "Categoría", tribeVotationCreate: "Crear Votación", tribeVotationsEmpty: "No hay votaciones, aún.", tribeVotationTitle: "Título", @@ -1884,6 +2068,27 @@ module.exports = { tribeForumCategory: "Categoría", tribeForumReply: "Responder", tribeForumReplies: "Respuestas", + tribeMarketCreate: "Crear Anuncio", + tribeMarketEmpty: "No hay anuncios, aún.", + tribeMarketTitle: "Título", + tribeMarketDescription: "Descripción", + tribeMarketPrice: "Precio", + tribeMarketImage: "Imagen", + tribeMarketCategory: "Categoría", + tribeJobCreate: "Crear Empleo", + tribeJobsEmpty: "No hay empleos, aún.", + tribeJobTitle: "Título", + tribeJobDescription: "Descripción", + tribeJobLocation: "Ubicación", + tribeJobSalary: "Salario", + tribeJobDeadline: "Fecha límite", + tribeProjectCreate: "Crear Proyecto", + tribeProjectsEmpty: "No hay proyectos, aún.", + tribeProjectTitle: "Título", + tribeProjectDescription: "Descripción", + tribeProjectGoal: "Objetivo", + tribeProjectFunded: "Financiado", + tribeProjectDeadline: "Fecha límite", tribeMediaUpload: "Subir Media", readDocument: "Leer Documento", tribeCreateImage: "Crear Imagen", @@ -1894,6 +2099,7 @@ module.exports = { tribeMediaEmpty: "No hay media, aún.", tribeMediaTitle: "Título", tribeMediaDescription: "Descripción", + tribeMediaType: "Tipo", tribeMediaTypeImage: "Imagen", tribeMediaTypeVideo: "Vídeo", tribeMediaTypeAudio: "Audio", @@ -1903,6 +2109,11 @@ module.exports = { tribeInviteCodeText: "Código de invitación: ", oasisVersionLabel: "Versión de Oasis", tribeInviteCodeHint: "Comparte este código con quien quieras invitar. Puede unirse desde /invites → Tribes.", + tribeGroupTribe: "Tribu", + tribeGroupOffice: "Oficina", + tribeGroupNetwork: "Red", + tribeGroupEconomy: "Economía", + tribeGroupMedia: "Multimedia", tribeStatusOpen: "ABIERTO", tribeStatusClosed: "CERRADO", tribeStatusInProgress: "EN PROGRESO", @@ -1912,17 +2123,33 @@ module.exports = { tribePriorityCritical: "CRÍTICA", tribeTaskFilterAll: "TODOS", tribeMediaFilterAll: "TODOS", + tribeReportCatBug: "ERROR", + tribeReportCatAbuse: "ABUSO", + tribeReportCatContent: "CONTENIDO", + tribeReportCatOther: "OTRO", tribeForumCatGeneral: "GENERAL", tribeForumCatProposal: "PROPUESTA", tribeForumCatQuestion: "PREGUNTA", tribeForumCatAnnouncement: "ANUNCIO", + tribeMarketCatGoods: "BIENES", + tribeMarketCatServices: "SERVICIOS", + tribeMarketCatFood: "ALIMENTACIÓN", + tribeMarketCatOther: "OTRO", tribeStatusLabel: "Estado", tribeSubTribes: "SUB-TRIBUS", tribeSubTribesCreate: "Crear Sub-Tribu", tribeSubTribesStrictDenied: "El modo estricto de la tribu no te permite generar nuevas sub-tribus. Ponte en contacto con el administrador.", + tribeSubTribesEmpty: "No se han creado sub-tribus, aún.", + tribeActivityJoined: "UNIDO", + tribeActivityLeft: "SALIDO", + tribeActivityFeed: "FEED", tribeActivityRefeed: "REFEED", + tribeGroupAnalytics: "Analíticas", + tribeGroupCreative: "Creativo", tribeSectionActivity: "ACTIVIDAD", tribeSectionTrending: "TENDENCIAS", + tribeSectionOpinions: "OPINIONES", + tribeSectionPixelia: "PIXELIA", tribeSectionTags: "ETIQUETAS", tribeSectionSearch: "BUSCAR", tribeActivityEmpty: "Sin actividad aún.", @@ -1934,15 +2161,25 @@ module.exports = { tribeTrendingPeriodWeek: "Esta Semana", tribeTrendingPeriodAll: "Todo", tribeTrendingEngagement: "interacción", + tribeOpinionsEmpty: "Sin opiniones aún.", + tribeOpinionsCast: "Votar", + tribeOpinionsRankings: "Clasificaciones", + tribeOpinionsAlreadyVoted: "Ya has votado", tribeTopCategory: "Más votado", + tribePixeliaTitle: "Pixelia", + tribePixeliaDescription: "Lienzo colaborativo de pixel art.", + tribePixeliaPaint: "Pintar", + tribePixeliaContributors: "contribuidores", + tribePixeliaTotalPixels: "píxeles pintados", tribeTagsEmpty: "No se encontraron etiquetas.", + tribeTagsCloud: "Nube de Etiquetas", + tribeTagsContentWith: "Contenido con etiqueta", tribeSearchPlaceholder: "Buscar contenido de la tribu...", tribeSearchEmpty: "Sin resultados.", tribeSearchResults: "Resultados", tribeSearchMinChars: "Introduce al menos 2 caracteres para buscar.", agendaTitle: "Agenda", agendaDescription: "Aquí puedes encontrar todos tus elementos asignados.", - agendaSearchPlaceholder: "Buscar en la agenda...", agendaFilterAll: "TODOS", agendaFilterToday: "HOY", agendaFilterUpcoming: "PRÓXIMO", @@ -1956,14 +2193,23 @@ module.exports = { agendaFilterReports: "INFORMES", agendaFilterTransfers: "TRANSFERENCIAS", agendaFilterJobs: "TRABAJOS", - agendaFilterIndustry: "INDUSTRIA", - agendaFilterHousing: "VIVIENDA", agendaFilterProjects: "PROYECTOS", agendaFilterCalendars: "CALENDARIOS", agendaNoItems: "No se encontraron asignaciones.", agendaDiscardButton: "Descartar", agendaRestoreButton: "Restaurar", + agendaAuthor: "Por", + agendaCreatedAt: "Creado el", + agendaTitleLabel: "Título", agendaMembersCount: "Miembros", + agendaDescriptionLabel: "Descripción", + agendaStatus: "Estado", + agendaVisibility: "Visibilidad", + agendaEventDate: "Fecha del Evento", + agendaEventLocation: "Ubicación", + agendaEventPrice: "Precio", + agendaEventUrl: "URL", + agendaTaskStart: "Hora de Inicio", agendaLocationLabel: "Ubicación", agendaYes: "SÍ", agendaNo: "NO", @@ -1973,6 +2219,11 @@ module.exports = { agendareportCategory: "Categoría", agendareportSeverity: "Gravedad", agendareportStatus: "Estado", + agendareportDescription: "Descripción", + agendaTaskEnd: "Hora de Fin", + agendaTaskPriority: "Prioridad", + agendaTransferFrom: "De", + agendaTransferTo: "A", agendaTransferConcept: "Concepto", agendaTransferAmount: "Cantidad", agendaTransferDeadline: "Plazo", @@ -1982,7 +2233,47 @@ module.exports = { voteNow: "Vota ahora", alreadyVoted: "Ya has opinado.", noOpinionsFound: "No se encontraron opiniones.", + RECENTButton: "RECIENTES", TOPButton: "TOP", + interestingButton: "INTERESANTE", + necessaryButton: "NECESARIO", + funnyButton: "DIVERTIDO", + disgustingButton: "ASQUEROSO", + sensibleButton: "SENSIBLE", + propagandaButton: "PROPAGANDA", + adultOnlyButton: "SOLO ADULTOS", + boringButton: "ABURRIDO", + confusingButton: "CONFUSO", + usefulButton: "UTIL", + informativeButton: "INFORMATIVO", + wellResearchedButton: "BIEN INVESTIGADO", + accurateButton: "CONCISO", + needsSourcesButton: "NECESITA FUENTES", + wrongButton: "INCORRECTO", + lowQualityButton: "BAJA CALIDAD", + creativeButton: "CREATIVO", + insightfulButton: "PERSPECTIVO", + actionableButton: "ACCIONABLE", + inspiringButton: "INSPIRADOR", + loveButton: "AMOR", + clearButton: "CLARO", + upliftingButton: "EMPODERANTE", + unnecessaryButton: "INNECESARIO", + rejectedButton: "RECHAZADO", + misleadingButton: "ENGAÑOSO", + offTopicButton: "FUERA DE TEMA", + duplicateButton: "DUPLICADO", + clickbaitButton: "CEBO DE CLICS", + spamButton: "SPAM", + trollButton: "TROLL", + nsfwButton: "NSFW", + violentButton: "VIOLENTO", + toxicButton: "TOXICO", + harassmentButton: "ACOSO", + hateButton: "ODIO", + scamButton: "ESTAFA", + triggeringButton: "PROVOCADOR", + opinionsCreatedAt: "Creado en", opinionsTotalCount: "Total de opiniones", voteInteresting: "Interesante", voteNecessary: "Necesario", @@ -2019,39 +2310,27 @@ module.exports = { voteCreative: "Creativo", voteSpam: "Spam", voteAdultOnly: "Solo adultos", + publishBlog: "Publicar Blog", privateMessage: "MP", pmSendTitle: "Mensajes Privados", pmSend: "Enviar!", - pmCancel: "Cancelar", - pmReset: "Restablecer", - pmSharedKeyHint: "Comparte esta clave con el destinatario por otro canal. Es necesaria para descifrar.", - pmDescription: "Usa este formulario para enviar un mensaje/fichero cifrado a otros habitantes.", - pmComposeTitle: "Enviar un mensaje", + pmDescription: "Usa este formulario para enviar un mensaje cifrado a otros habitantes.", pmRecipients: "Destinatarios", pmRecipientsHint: "Introduce los IDs de Oasis separados por comas", - pmLimitsHint: "Hasta 7 destinatarios por mensaje.", - pmTextPlaceholder: "Cuerpo limitado a 7000 caracteres.", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "Asunto", pmSubjectHint: "Introduce el asunto del mensaje", pmText: "Mensaje", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Descifrar", - fileShareTitle: "Compartir un fichero", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "Fichero", - fileShareDownload: "Descargar", - fileShareMutualError: "Solo puedes compartir ficheros con habitantes que te apoyen mutuamente.", - fileShareNoFile: "No se ha seleccionado ningún fichero.", - fileShareTooLarge: "El fichero supera el tamaño permitido.", - fileShareFailed: "No se pudo preparar el fichero.", - fileShareSendError: "No se pudo enviar el fichero.", - fileShareUnavailable: "Este fichero no está disponible ahora mismo. Inténtalo más tarde.", pmCrypterBadKey: "¡La clave compartida es incorrecta!", pmCrypterTooLong: "El mensaje es demasiado largo para cifrarlo con el Crypter. Acórtalo e inténtalo de nuevo.", pmCrypterCipherLabel: "Mensaje recifrado", pmCrypterCharsLabel: "caracteres", pmInvalidRecipients: "Oasis ID no válido (debería ser como @…=.ed25519).", pmEncryptionLabel: "Cifrado", + pmFile: "Archivo adjunto", private: "Privados", privateDescription: "Tus mensajes cifrados.", privateInbox: "BUZÓN", @@ -2061,33 +2340,27 @@ module.exports = { noPrivateMessages: "No hay mensajes privados.", pmReply: "Responder", pmReplies: "respuestas", + pmNew: "nuevas", + pmMarkRead: "Marcar como leido", inReplyTo: "EN RESPUESTA A", pmPreview: "Previsualizar", pmPreviewTitle: "Vista previa", performed: "→", pmFromLabel: "De:", pmToLabel: "Para:", + pmInvalidMessage: "Mensaje no válido", pmNoSubject: "(sin asunto)", pmSubjectLabel: "Asunto:", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "La casa gobernante del L.A.R.P ha cambiado.", - politicalBotParliamentTitle: "Ha comenzado un nuevo ciclo de gobierno en el Parlamento.", - politicalBotTribeTitle: "Ha comenzado un nuevo ciclo de gobierno en una de tus Tribus.", - politicalBotLarpIntro: "Ahora gobierna {house} durante este ciclo: {cycle}", - politicalBotParliamentIntro: "El gobierno actual es {gov}", - politicalBotParliamentIntroLeader: "El gobierno actual es {gov} ejercida por {leader}", - politicalBotTribeIntro: "El gobierno actual en la Tribu {tribe} es {gov}", - politicalBotTribeIntroLeader: "El gobierno actual en la Tribu {tribe} es {gov} ejercida por {leader}", - politicalBotLeaderLabel: "Líder", + pmBodyLabel: "Cuerpo", + pmBotJobs: "42-EmpleosBOT", + pmBotProjects: "42-ProyectosBOT", + pmBotMarket: "42-MercadoBOT", inboxJobSubscribedTitle: "Nueva suscripción a tu oferta de trabajo", pmInhabitantWithId: "Habitante con ID OASIS:", pmHasSubscribedToYourJobOffer: "se ha suscrito a tu oferta de trabajo", + inboxProjectCreatedTitle: "Nuevo proyecto creado", + pmHasCreatedAProject: "ha creado un proyecto", inboxMarketItemSoldTitle: "Artículo vendido", - inboxShopSoldTitle: "Producto vendido", pmYourItem: "Tu artículo", pmHasBeenSoldTo: "se ha vendido a", pmFor: "por", @@ -2095,6 +2368,7 @@ module.exports = { pmHasPledged: "ha aportado", pmToYourProject: "a tu proyecto", blockchain: 'Explorador', + blockchainTitle: 'Explorador', blockchainDescription: 'Explora y visualiza los bloques de la cadena.', blockchainNoBlocks: 'No se encontraron bloques en la cadena.', blockchainStatsTitle: 'Estadísticas', @@ -2106,30 +2380,40 @@ module.exports = { blockchainBlockCarbon: 'Huella de carbono', blockchainBlockType: 'Tipo', blockchainBlockTimestamp: 'Marca de tiempo', + blockchainBlockContent: 'Bloque', + blockchainBlockURL: 'URL:', + blockchainContent: 'Bloque', + blockchainContentPreview: 'Vista previa del contenido del bloque', blockchainLatestDatagram: 'Último Datagrama', - blockchainDatagram: "Datagrama", - blockchainDetails: "Ver Detalles del Bloque", - blockchainViewBlockexplorer: "Vista en Blockexplorer", + blockchainDatagram: 'Datagrama', + blockchainDetails: 'Ver detalles del bloque', + blockchainViewBlockexplorer: "Ver en el blockexplorer", + blockchainBlockInfo: 'Información del Bloque', + blockchainBlockDetails: 'Detalles del bloque seleccionado', blockchainBack: 'Volver al Blockexplorer', banking: 'Banking', bankingTitle: 'Banking', - bankingDescription: "La economía de ECOin: balance, RBU, tasas y valor de industria.", + bankingDescription: 'Explora el valor actual de ECOin y la asignación de RBU correspondiente, distribuida semanalmente en función de la participación y la confianza.', bankOverview: 'Resumen', bankEpochs: 'Épocas', bankRules: 'Reglas', pending: 'Pendientes', closed: 'Cerradas', + bankBack: 'Volver a Banking', bankViewTx: 'Ver Tx', bankClaimNow: 'Reclamar', bankClaimUBI: '¡Reclamar RBU!', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: 'No hay asignaciones RBU pendientes para esta época.', bankEpoch: 'Época', bankPool: 'Fondo (esta época)', bankWeightsSum: 'Suma de pesos', + bankAllocations: 'Asignaciones', bankNoAllocations: 'No se encontraron asignaciones.', bankNoEpochs: 'No se encontraron épocas.', bankEpochAllocations: 'Asignaciones de la época', @@ -2143,11 +2427,10 @@ module.exports = { bankEpochId: 'ID de época', bankRuleHash: 'Hash del snapshot de reglas', bankViewEpoch: 'Ver época', - bankYourIndustryBalance: "Tu parte de la industria", - bankYourUbiMonth: "Tu RBU (este mes)", - bankYourFundsMonth: "Tus fondos (este mes)", - bankIndustryBalance: "Producción industrial (estimada)", bankUserBalance: 'Tu saldo', + ecoWalletNotConfigured: 'Cartera de ECOin no configurada', + editWallet: 'Editar cartera', + addWallet: 'Añadir cartera', bankAddresses: 'Direcciones', bankNoAddresses: 'No se encontraron direcciones.', bankUser: 'ID de Oasis', @@ -2172,10 +2455,18 @@ module.exports = { search: 'Buscar!', bankLocal: 'Local', bankFromOasis: 'Oasis', + bankMyAddress: 'Tu dirección', + bankRemoveMyAddress: 'Eliminar mi dirección', + bankNotRemovableOasis: 'Las direcciones no se pueden eliminar localmente', + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "SIN FONDOS!", bankUbiAvailableOk: "¡DISPONIBLE!", - bankUbiAvailability: "RBU (red)", + bankUbiAvailability: "Disponibilidad de UBI", bankAlreadyClaimedThisMonth: "Ya reclamado este mes", bankUbiThisMonth: "UBI (este mes)", bankUbiLastClaimed: "UBI (última reclamada)", @@ -2189,6 +2480,13 @@ module.exports = { shopsTitle: "Tiendas", shopDescription: "Descubre y gestiona tiendas en la red.", shopTitle: "Tienda", + shopFilterAll: "TODAS", + shopFilterMine: "MÍAS", + shopFilterRecent: "RECIENTES", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUCTOS", + shopFilterPrices: "PRECIOS", + shopFilterFavorites: "FAVORITAS", shopUpload: "Crear Tienda", shopAllSectionTitle: "Tiendas", shopMineSectionTitle: "Mis Tiendas", @@ -2205,10 +2503,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "Publicar en Clearnet", + shopClearnetUnpublish: "Quitar de Clearnet", + shopClearnetUrlLabel: "URL en Clearnet", + shopClearnetWarning: "Publicar en Clearnet hace que esta tienda sea indexable por buscadores y rastreadores externos.", shopAddFavorite: "Añadir Favorito", shopRemoveFavorite: "Quitar Favorito", shopOpen: "ABIERTA", shopClosed: "CERRADA", + shopOpenShop: "Abrir Tienda", + shopCloseShop: "Cerrar Tienda", shopMakePrivate: "HACER PRIVADA (E2E)", shopMakePublic: "HACER PÚBLICA", shopProducts: "Productos", @@ -2236,6 +2540,8 @@ module.exports = { shopOrderPrice: "Precio", shopOrderBuyer: "Comprador", shopBackToShop: "Volver a la Tienda", + shopShareUrl: "URL para compartir", + shopVisitShop: "VISITAR TIENDA", marketVisitItem: "VER ARTÍCULO", shopStatus: "ESTADO", shopCreatedAt: "CREADO", @@ -2244,10 +2550,12 @@ module.exports = { shopLocation: "Ubicación", shopTags: "Etiquetas", shopVisibility: "Visibilidad", + shopImage: "Imagen", shopShortDescription: "Descripcion Corta", shopShortDescriptionPlaceholder: "Descripcion breve para las tarjetas (max 160 caracteres)", shopTitlePlaceholder: "Nombre de tu tienda", shopDescriptionPlaceholder: "Descripcion detallada de tu tienda", + shopUrlPlaceholder: "https://url-de-tu-tienda.com", shopLocationPlaceholder: "Ciudad, Pais", shopTagsPlaceholder: "etiqueta1, etiqueta2, etiqueta3", mapTitlePlaceholder: "Titulo del mapa", @@ -2264,6 +2572,7 @@ module.exports = { bankUnitMinutes: "minutos", bankUnitDays: "días", bankExchangeNoData: 'No hay datos disponibles', + bankExchangeIndex: 'Valor de ECOin (1h)', bankInflation: 'Inflación de ECOin (anual)', bankInflationMonthly: 'Inflación de ECOin (mensual)', bankExchangeChartTitle: 'Progresión del valor de ECOin a lo largo del tiempo', @@ -2277,17 +2586,38 @@ module.exports = { bankingSyncStatusOutdated: 'Desactualizado', statsTitle: 'Estadísticas', statistics: "Estadísticas", + statsInhabitant: "Estadísticas del Habitante", statsDescription: "Descubre estadísticas sobre tu red.", ALLButton: "TODO", MINEButton: "MÍAS", TOMBSTONEButton: "ELIMINADOS", + statsYou: "Tú", + statsUserId: "ID de Oasis", statsCreatedAt: "Creado el", statsYourContent: "Contenido", statsYourOpinions: "Opiniones", + statsYourTombstone: "Eliminados", + statsNetwork: "Red", + statsTotalInhabitants: "Habitantes", + statsDiscoveredTribes: "Tribus (Públicas)", + statsPrivateDiscoveredTribes: "Tribus (Privadas)", statsNetworkContent: "Contenido", + statsYourMarket: "Mercado", + statsYourJob: "Empleos", + statsYourProject: "Proyectos", + statsYourTransfer: "Transferencias", + statsYourForum: "Foros", statsNetworkOpinions: "Opiniones", + statsDiscoveredMarket: "Mercado", + statsDiscoveredJob: "Empleos", + statsDiscoveredProject: "Proyectos", + statsBankingTitle: "Banca", statsEcoWalletLabel: "Billetera ECOIN", statsEcoWalletNotConfigured: "¡No configurada!", + statsTotalEcoAddresses: "Direcciones totales", + statsDiscoveredTransfer: "Transferencias", + statsDiscoveredForum: "Foros", + statsNetworkTombstone: "Eliminados", statsBookmark: "Marcadores", statsEvent: "Eventos", statsTask: "Tareas", @@ -2309,12 +2639,16 @@ module.exports = { statsAiExchange: "IA", statsPUBs: 'PUBs', statsPost: "Publicaciones", + statsOasisID: "ID de Oasis", statsSize: "Total (tamaño)", statsBlockchainSize: "Blockchain (tamaño)", statsBlobsSize: "Blobs (tamaño)", statsActivity7d: "Actividad (últimos 7 días)", statsActivity7dTotal: "Total 7 días", statsActivity30dTotal: "Total 30 días", + statsKarmaScore: "Puntuación KARMA", + statsPublic: "Público", + statsPrivate: "Privado", day: "Día", messages: "Mensajes", statsProject: "Proyectos", @@ -2326,14 +2660,30 @@ module.exports = { statsProjectsCancelled: "Cancelados", statsProjectsGoalTotal: "Meta total", statsProjectsPledgedTotal: "Aportación total", + statsProjectsSuccessRate: "Tasa de éxito", + statsProjectsAvgProgress: "Progreso medio", + statsProjectsMedianProgress: "Progreso mediano", + statsProjectsActiveFundingAvg: "Financiación activa media", + statsJobsTitle: "Empleos", + statsJobsTotal: "Total de empleos", + statsJobsOpen: "Abiertos", + statsJobsClosed: "Cerrados", + statsJobsOpenVacants: "Vacantes abiertas", + statsJobsSubscribersTotal: "Suscriptores totales", + statsJobsAvgSalary: "Salario medio", + statsJobsMedianSalary: "Salario mediano", statsMarketTitle: "Mercado", statsMarketTotal: "Artículos totales", statsMarketForSale: "En venta", statsMarketReserved: "Reservados", statsMarketClosed: "Cerrados", statsMarketSold: "Vendidos", + statsMarketRevenue: "Ingresos", + statsMarketAvgSoldPrice: "Precio medio vendido", statsUsersTitle: "Habitantes", user: "Habitante", + statsTombstoneTitle: "Eliminados", + statsNetworkTombstones: "Eliminados en la red", statsTombstoneRatio: "Ratio de eliminados (%)", bankingUserEngagementScore: "Puntuación de KARMA", statsParliamentCandidature: "Candidaturas de parlamento", @@ -2361,21 +2711,30 @@ module.exports = { aiConfiguration: "Configurar prompt", aiPromptUsed: "Indicación", aiClearHistory: "Borrar historial de chat", + aiSharePrompt: "Añadir esta respuesta al entrenamiento colectivo?", + aiShareYes: "Sí", + aiShareNo: "No", + aiSharedLabel: "Añadida al entrenamiento", + aiRejectedLabel: "No añadida al entrenamiento", aiServerError: "La IA no ha podido responder. Inténtalo de nuevo.", aiInputPlaceholder: "¿Qué es Oasis?", typeAiExchange: "IA", aiApproveTrain: "Añadir al entrenamiento colectivo", aiRejectTrain: "No entrenar", + aiTrainPending: "Pendiente de aprobación", aiTrainApproved: "Aprobado para entrenamiento", aiTrainRejected: "Rechazado para entrenamiento", aiSnippetsUsed: "Fragmentos usados", aiSnippetsLearned: "Fragmentos aprendidos", statsAITraining: "Entrenamiento de IA", + aiApproveCustomTrain: "Entrenar con esta respuesta personalizada", aiCustomAnswerPlaceholder: "Escribe tu respuesta personalizada…", + statsAIExchanges: "Intercambio de Modelos", marketMineSectionTitle: "Tus artículos", marketCreateSectionTitle: "Crear artículo", marketUpdateSectionTitle: "Actualizar", marketAllSectionTitle: "Mercado", + marketRecentSectionTitle: "Mercado reciente", marketTitle: "Mercado", marketDescription: "Un mercado para intercambiar bienes o servicios en tu red.", marketFilterAll: "TODOS", @@ -2405,11 +2764,14 @@ module.exports = { marketItemDeadline: "Fecha límite", marketItemIncludesShipping: "¿Incluye envío?", marketItemHighestBid: "Puja más alta", + marketItemHighestBidder: "Mejor postor", marketItemStock: "Existencias", marketOutOfStock: "Sin stock", + marketItemBidTime: "Fecha de la puja", marketActionsUpdate: "Actualizar", marketUpdateButton: "Actualizar", marketActionsDelete: "Eliminar", + marketActionsSold: "Marcar como vendido", marketActionsChangeStatus: "CAMBIAR ESTADO", marketActionsBuy: "¡COMPRAR!", marketAuctionBids: "Pujas actuales", @@ -2418,82 +2780,19 @@ module.exports = { marketNoItems: "Aún no hay artículos disponibles.", marketYourBid: "Tu puja", marketCreateFormImageLabel: "Subir contenido multimedia (max: 50MB)", + marketSearchLabel: "Buscar", marketSearchPlaceholder: "Buscar por título o etiquetas", marketMinPriceLabel: "Precio mínimo", marketMaxPriceLabel: "Precio máximo", + marketSortLabel: "Ordenar por", marketSortRecent: "Más recientes", marketSortPrice: "Precio", marketSortDeadline: "Fecha límite", marketSearchButton: "Buscar", marketAuctionEndsIn: "Termina", marketAuctionEnded: "Terminó", + marketMyBidBadge: "Has pujado", marketNoItemsMatch: "No hay artículos que coincidan con tu búsqueda.", - housingTitle: "Vivienda", - housingDescriptionText: "Descubre y gestiona lugares en tu red.", - modulesHousingLabel: "Vivienda", - modulesHousingDescription: "Módulo para descubrir y gestionar lugares.", - typeHousing: "VIVIENDA", - housingFilterAll: "TODO", - housingFilterMine: "MÍOS", - housingFilterRequested: "SOLICITADOS", - housingFilterSale: "VENTA", - housingFilterRent: "ALQUILER", - housingFilterCouchsurfing: "COUCHSURFING", - housingFilterOpen: "DISPONIBLES", - housingFilterClosed: "NO DISPONIBLES", - housingFilterRecent: "RECIENTES", - housingFilterTop: "TOP", - housingType: "Oferta", - housingTypeSALE: "En venta", - housingTypeRENT: "En alquiler", - housingTypeCOUCHSURFING: "Couchsurfing", - housingProperty: "Inmueble", - housingPropertyAPARTMENT: "Piso", - housingPropertyHOUSE: "Casa", - housingPropertyROOM: "Habitación", - housingPropertyLAND: "Terreno", - housingPropertyOTHER: "Otro", - housingTitleLabel: "Título", - housingTitlePlaceholder: "Nombre del lugar", - housingDescription: "Descripción", - housingDescriptionPlaceholder: "Describe el lugar, el barrio, qué incluye: baños, jardín, camas…", - housingRules: "Normas", - housingRulesPlaceholder: "Mascotas, fumar, horas de silencio, estancia mínima…", - housingPlace: "Lugar", - housingPlacePlaceholder: "País, ciudad, dirección", - housingPrice: "Precio", - housingPriceLabel: "Precio (ECO)", - housingSizeLabel: "Superficie (m²)", - housingFree: "GRATIS", - housingPerMonth: "/mes", - housingRooms: "Habitaciones", - housingSize: "Superficie", - housingCapacity: "Huéspedes", - housingAvailableFrom: "Disponible desde", - housingAvailableTo: "Disponible hasta", - housingTags: "Etiquetas", - spreadEditWarning: "Este contenido perderá sus réplicas al editarlo", - housingRequests: "Solicitudes", - housingRequestButton: "Solicitar", - housingCancelButton: "Cancelar solicitud", - housingRequestedBadge: "SOLICITADO", - housingStatus: "Estado", - housingStatusOPEN: "DISPONIBLE", - housingStatusCLOSED: "NO DISPONIBLE", - housingSetOpen: "Marcar disponible", - housingSetClosed: "Marcar no disponible", - housingCreateButton: "Crear lugar", - housingUpdateButton: "Actualizar", - housingDeleteButton: "Borrar", - housingSearchPlaceholder: "Buscar lugares...", - housingSearchButton: "Buscar", - housingMinPrice: "Precio mín.", - housingMaxPrice: "Precio máx.", - housingSortRecent: "Más recientes", - housingSortPrice: "Precio más bajo", - housingSortRequests: "Más solicitados", - housingSortRating: "Mejor valorados", - housingNoItems: "No se han encontrado lugares.", jobsTitle: "Laboral", jobsCandidatesTitle: "Candidatos sugeridos", jobsCandidatesDescription: "Habitantes cuyas competencias encajan con este empleo. Envíales un mensaje privado para invitarles.", @@ -2541,6 +2840,7 @@ module.exports = { jobLocationPresencial: "Presencial", jobLocationRemote: "Remoto", jobVacantsPlaceholder: "Número de vacantes", + jobSalaryPlaceholder: "Salario en ECO por 1 hora dedicada", jobImage: "Subir contenido multimedia (max: 50MB)", jobTasks: "Tareas", jobType: "Tipo de Trabajo", @@ -2550,6 +2850,7 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Trabajos Mejor Pagados", createJobButton: "Publicar Trabajo", + viewDetailsButton: "Ver Detalles", noJobsFound: "No se encontraron ofertas de trabajo.", jobAuthor: "Por", jobTypeFreelance: "Autónomo", @@ -2561,6 +2862,10 @@ module.exports = { jobsHoursOffered: "Horas ofrecidas", jobsHoursRequested: "Horas pedidas a cambio", jobsExchangeSkill: "Habilidad solicitada a cambio", + jobsHoursOfferedPlaceholder: "p. ej. 4", + jobsHoursRequestedPlaceholder: "p. ej. 4", + jobsExchangeSkillPlaceholder: "p. ej. carpintería, diseño", + jobExchangeHint: "Para trabajos de intercambio de horas, rellena los campos siguientes en vez del salario.", jobTimePartial: "Parcial", jobTimeComplete: "Completo", jobsDeleteButton: "ELIMINAR", @@ -2568,201 +2873,31 @@ module.exports = { jobsFilterApplied: "CANDIDATURA", jobsAppliedTitle: "Mis candidaturas", jobsAppliedBadge: "Inscrito", + jobsFilterFavs: "Favoritos", + jobsFavsTitle: "Favoritos", + jobsFilterNeeds: "Necesita ayuda", + jobsNeedsTitle: "Necesita ayuda", + jobsSearchLabel: "Buscar", jobsSearchPlaceholder: "Buscar por título, tags, descripción...", jobsMinSalaryLabel: "Salario mín.", jobsMaxSalaryLabel: "Salario máx.", + jobsSortLabel: "Ordenar por", jobsSortRecent: "Más recientes", jobsSortSalary: "Mayor salario", jobsSortSubscribers: "Más candidatos", jobsSearchButton: "Buscar", + jobsFavoriteButton: "Favorito", + jobsUnfavoriteButton: "Quitar favorito", + jobsMessageAuthorButton: "MP", jobsApplicants: "Candidatos", jobsUpdatedAt: "Actualizado", jobSetOpen: "Marcar abierto", + jobNewBadge: "NUEVO", jobsTagsLabel: "Etiquetas", + jobsTagsPlaceholder: "etiqueta1, etiqueta2, etiqueta3", noJobsMatch: "No hay ofertas que coincidan con tu búsqueda.", - industrySearchPlaceholder: "Buscar fábricas…", - industryAllSectors: "Todos los sectores", - industryVisitButton: "Visitar Industria", - industryAdvanceTo: "Cambiar a", - industryAmount: "Cantidad", - industryApproveBuild: "Aprobar", - industryBlueprint: "Blueprint", - industryBlueprints: "Blueprints", - industryBuild: "Build", - industryBuildNotes: "Notas", - industryBuildStart: "Fecha de inicio", - industryVoteUpdateButton: "Votar actualización", - industryVoteDeleteButton: "Votar borrado", - industryRevokeVote: "Revocar", - industryTimeLeft: "Tiempo restante", - industryBuildEnd: "Fecha de fin", - industryBuilds: "Builds", - industryBuildTitle: "Título", - industryContribute: "Contribuir", - industryContributeButton: "Contribuir", - industryContributions: "Contribuciones", - industryCreateBlueprintButton: "Crear blueprint", - industryDistribute: "Distribuir", - industryDistributeButton: "Distribuir por cuotas", - industryDistribution: "Distribución", - industryEcoAmount: "Cantidad (ECO)", - industryEditBlueprint: "Editar blueprint", - industryFacility: "Fábrica", - industryKindLabel: "Tipo", - industryKind_labor: "Mano de obra", - industryKind_material: "Material", - industryKind_eco: "Fondos", - industryLaborHours: "Mano de obra (horas)", - industryHours: "Horas", - industryMaterialItem: "Material", - industryMaterials: "Materiales", - industryEstMaterials: "Total materiales", - industryEstLabor: "Total mano de obra", - industryEstTotal: "Precio estimado", - industryBuildingPrice: "Precio de construcción", - industryFinalPrice: "Precio final", - industryBlueprintDescPlaceholder: "Especificaciones, dimensiones, peso, documentación…", - industryMaterialValue: "Valor (ECO)", - industryMember: "Miembro", - industryNewBlueprint: "Nuevo blueprint", - industryNewBuild: "Proponer un build", - industryEditBuild: "Editar producción", - industryNeedBlueprint: "Crea primero un plano: cada producción fabrica uno.", - industryNoBlueprints: "Aún no hay blueprints.", - industryNoBuilds: "Aún no hay builds.", - industryNote: "Nota", - industryOutput: "Salida", - industryOutputKind: "Tipo de producto", - industryOutputValue: "Valor de salida (ECO, p. ej. de la venta)", - industryPostJob: "Crear Trabajo", - industryPot: "Bote", - industryProposeBuildButton: "Proponer build", - industryProposer: "Proponente", - industryAuthor: "Autor", - industrySellOnMarket: "Enviar al Mercado", - industrySendToProjects: "Crear Proyecto", - industrySkills: "Habilidades", - industryTreasury: "Tesorería", - industryKind_physical: "Físico", - industryKind_digital: "Digital", - industryBuildStatus_PROPOSED: "PROPUESTO", - industryBuildStatus_REJECTED: "RECHAZADO", - industryBuildStatus_APPROVED: "APROBADO", - industryBuildStatus_STOCKING: "ABASTECIENDO", - industryBuildStatus_IN_PRODUCTION: "EN PRODUCCIÓN", - industryBuildStatus_COMPLETED: "COMPLETADO", - industryBuildStatus_FAILED: "FALLIDO", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "Un empleo encaja con tu currículum", - jobsBotMatchIntro: "Un empleo encaja con tu currículum.", - jobsBotMatchJob: "Empleo", - jobsBotMatchHint: "Recibes esto porque tu currículum está gestionado por IA. Puedes desactivarlo en tu CV.", - cvAiManaged: "Gestionado por IA", - switchOn: "ACTIVADO", - switchOff: "DESACTIVADO", - cvAiManagedHint: "Si está activo, JobsBot te avisa por mensaje privado cuando un empleo encaja con tu currículum.", - cvMatchThreshold: "Avisarme a partir de este nivel de coincidencia", - housingBotRequestedTitle: "Alguien ha solicitado uno de tus lugares.", - housingBotCancelledTitle: "Se ha cancelado una solicitud sobre uno de tus lugares.", - housingBotYouRequestedTitle: "Has solicitado un lugar.", - housingBotYouCancelledTitle: "Has cancelado una solicitud.", - housingBotUnavailableTitle: "Un lugar que solicitaste ya no está disponible.", - housingBotUnavailableSentTitle: "Has avisado a quienes solicitaron este lugar.", - industryBotAdmittedTitle: "Has sido admitido en una fábrica.", - industryBotApplicationTitle: "Nueva solicitud de ingreso en tu fábrica.", - industryBotInvitedTitle: "Has sido invitado a una fábrica.", - industryBotDissolvedTitle: "Una fábrica a la que perteneces ha sido disuelta.", - industryBotBuildApprovedTitle: "Un build ha sido aprobado.", - industryBotDistributedTitle: "Se ha distribuido la salida de un build.", - industryFilterRules: "REGLAS", - industryRulesTitle: "Reglas", - industryRulesIntro: "Industry permite a los habitantes poseer colectivamente los medios de producción: una Fábrica es un taller de la red que nadie posee de forma privada.", - industryRulesSteward: "El fundador es el mayordomo — un custodio, no un dueño: no puede vender, privatizar ni disolver la Fábrica en solitario.", - industryRulesMembership: "La política de membresía define cómo se une la gente: Abierta (entras al instante), Por voto (te admite el voto de los miembros) o Solo invitación (un miembro debe invitarte antes).", - industryRulesQuorum: "El quórum es el número mínimo de miembros que deben votar para que una decisión cuente, para que una Fábrica pequeña no la decida una sola persona.", - industryRulesMajority: "La mayoría es la fracción de votos necesaria para aprobar (0,5 = mitad, 1 = unanimidad); una decisión pasa cuando los SÍ alcanzan al menos max(quórum, miembros × mayoría).", - industryRulesDecisions: "Los miembros votan para admitir nuevos, aprobar builds y disolver la Fábrica; el mayordomo no puede saltarse estas decisiones colectivas.", - industryRulesBlueprints: "Los Blueprints son recetas libres (COPYLEFT) — las entradas (materiales, horas de trabajo, skills) y la salida (un bien físico o digital) — y cualquiera puede forkearlas.", - industryRulesBuilds: "Un Build es una orden de producción: PROPUESTO → APROBADO (por voto) → ABASTECIENDO → EN PRODUCCIÓN → COMPLETADO, y solo acepta contribuciones una vez aprobado.", - industryRulesContributions: "Los miembros aportan trabajo (horas), materiales (valor declarado en ECO) o ECO a un build; cada aporte gana karma.", - industryRulesLaborRate: "La tarifa de trabajo es el valor en ECO de una hora de trabajo en esta Fábrica; convierte horas en karma para comparar esfuerzo y capital de forma justa: karma = horas × tarifa + valor material + ECO.", - industryRulesShares: "Tu cuota en un build es tu karma ÷ karma total, y determina cuánto del valor de salida recibes.", - industryRulesDistribution: "Cuando una producción se completa, el steward reparte el fondo (los fondos de la producción más el valor de venta) entre quienes contribuyeron, en proporción a su participación.", - industryRulesTreasury: "Las aportaciones en ECO las custodia el mayordomo como tesorería del build hasta el reparto; todos los movimientos quedan registrados en la red y las disputas pueden ir a Courts.", - industryJobs: "Empleos", - industryReportButton: "Reportar", - industryCreateTask: "Crear Tarea", - industryOpenDispute: "Abrir disputa", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "Generar invitación", - industryPauseVotes: "Votos de estado", - industryTitle: "Industria", - industryDescription: "Módulo para gestionar los medios de producción de forma colectiva.", - typeIndustryBuild: "BUILD", - typeIndustryBlueprint: "BLUEPRINT", - typeIndustryAllocation: "DISTRIBUCIÓN", - typeIndustry: "INDUSTRIA", - modulesIndustryLabel: "Industria", - modulesIndustryDescription: "Módulo para gestionar los medios de producción de forma colectiva.", - industryFilterAll: "TODAS", - industryFilterMember: "MIEMBRO", - industryFilterBlueprints: "PLANOS", - industryFilterBuilds: "PRODUCCIONES", - industryFilterMine: "MÍAS", - industryFilterActive: "EN MARCHA", - industryFilterPaused: "PAUSADAS", - industryFilterDissolved: "DISUELTAS", - industryAllTitle: "Industria", - industryMemberTitle: "Fábricas en las que estoy", - industryMineTitle: "Mis fábricas", - industryActiveTitle: "En marcha", - industryPausedTitle: "Fábricas pausadas", - industryDissolvedTitle: "Fábricas disueltas", - industryNoFacilitiesFound: "No se encontraron fábricas.", - industryCreateFacility: "Crear fábrica", - industryMembers: "Miembros", - industryMemberBadge: "MIEMBRO", - industryName: "Nombre", - industryNamePlaceholder: "Nombre de la fábrica", - industryDescriptionLabel: "Descripción", - industryDescriptionPlaceholder: "¿Qué produce esta fábrica?", - industrySector: "Sector", - industryMembershipPolicy: "Política de membresía", - industryQuorum: "Quórum", - industryMajority: "Mayoría", - industryLaborRate: "Tarifa de trabajo", - industryCreateButton: "Crear fábrica", - industryUpdateButton: "Actualizar", - industrySteward: "Mayordomo", - industryStatusActive: "EN MARCHA", - industryStatusPaused: "PAUSADA", - industryStatusDissolved: "DISUELTA", - industryStatusLabel: "Estado", - industryJoinButton: "Unirse", - industryApplyButton: "Solicitar ingreso", - industryLeaveButton: "Salir", - industryInviteButton: "Invitar", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "Necesitas una invitación para unirte.", - industryPauseButton: "Votar pausar", - industryResumeButton: "Votar reanudar", - industryDeleteButton: "Eliminar", - industryPendingApplicants: "Solicitudes pendientes", - industryVotesYes: "sí", - industryVotesNo: "no", - industryAlreadyVoted: "votado", - industryAdmit: "Admitir", - industryReject: "Rechazar", - industryDissolveTitle: "Disolver fábrica", - industryDissolveVote: "Votar disolución", - industryPolicy_open: "Abierta", - industryPolicy_vote: "Por voto", - industryPolicy_invite: "Solo invitación", projectsTitle: "Proyectos", projectsDescription: "Crea, financia y sigue proyectos impulsados por la comunidad en tu red.", - projectSearchPlaceholder: "Buscar proyectos...", projectCreateProject: "Crear Proyecto", projectCreateButton: "Crear Proyecto", projectUpdateButton: "ACTUALIZAR", @@ -2806,8 +2941,14 @@ module.exports = { projectPledgeTitle: "Apoya este proyecto", projectPledgePlaceholder: "Cantidad en ECO", projectBounties: "Recompensas", + projectBountiesInputLabel: "Recompensas (una por línea: Título|Cantidad [ECO]|Descripción)", + projectBountiesPlaceholder: "Arreglar error en UI|100|Enlace al problema\nEscribir documentación|250|Ejemplos de uso", projectNoBounties: "No se encontraron recompensas.", projectTitle: "Título", + projectAddBountyTitle: "Añadir nueva recompensa", + projectBountyTitle: "Título de la recompensa", + projectBountyAmount: "Cantidad (ECO)", + projectBountyDescription: "Descripción", projectMilestoneSelect: "Seleccionar hito", projectBountyCreateButton: "Crear recompensa", projectBountyStatus: "Estado de la recompensa", @@ -2822,15 +2963,19 @@ module.exports = { projectMilestoneTitle: "Título del hito", projectMilestoneTargetPercent: "Porcentaje (%)", projectMilestoneDueDate: "Fecha", + projectMilestoneCreateButton: "Crear hito", projectMilestoneStatus: "Estado del hito", projectMilestoneOpen: "abierto", projectMilestoneDone: "completado", projectMilestoneDue: "debido", + projectNoMilestones: "No se encontraron hitos.", projectMilestoneMarkDone: "Marcar como hecho", projectMilestoneTitlePlaceholder: "Ingresa el título del hito", projectMilestoneDescriptionPlaceholder: "Ingresa la descripción de este hito", projectMilestoneDescription: "Descripción del hito", + projectBudgetGoal: "Presupuesto (Objetivo)", projectBudgetAssigned: "Asignado a recompensas", + projectBudgetRemaining: "Restante", projectBudgetOver: "⚠ Presupuesto excedido: lo asignado supera el objetivo", projectFollowers: "Seguidores", projectFollowersTitle: "Seguidores", @@ -2843,6 +2988,7 @@ module.exports = { projectBackersTotalPledged: "Total aportado", projectBackersYourPledge: "Tu aportación", projectBackersNone: "Aún no hay aportaciones.", + projectNoRemainingBudget: "No queda presupuesto.", projectFilterBackers: "MECENAS", projectFilterApplied: "APORTANDO", projectAppliedTitle: "APORTANDO", @@ -2851,15 +2997,30 @@ module.exports = { projectBackerAmount: "Total aportado", projectBackerPledges: "Aportaciones", projectBackerProjects: "Proyectos", + projectPledgeAmount: "Cantidad", projectSelectMilestoneOrBounty: "Seleccionar Hito o Recompensa", projectPledgeButton: "Aportar", footerLicense: "GPLv3", + footerPackage: "Paquete", + footerVersion: "Versión", modulesModuleName: "Nombre", modulesModuleDescription: "Descripción", modulesModuleStatus: "Estado", modulesTotalModulesLabel: "Módulos Cargados", modulesEnabledModulesLabel: "Habilitados", modulesDisabledModulesLabel: "Deshabilitados", + modulesPopularLabel: "Popular", + modulesPopularDescription: "Módulo para recibir publicaciones que son tendencia, más vistas o más comentadas.", + modulesTopicsLabel: "Temas", + modulesTopicsDescription: "Módulo para recibir categorías de discusión basadas en intereses compartidos.", + modulesSummariesLabel: "Resúmenes", + modulesSummariesDescription: "Módulo para recibir resúmenes de discusiones o publicaciones largas.", + modulesLatestLabel: "Últimos", + modulesLatestDescription: "Módulo para recibir las publicaciones y discusiones más recientes.", + modulesThreadsLabel: "Hilos", + modulesThreadsDescription: "Módulo para recibir conversaciones agrupadas por tema o pregunta.", + modulesMultiverseLabel: "Multiverso", + modulesMultiverseDescription: "Módulo para recibir contenido de otros pares federados.", modulesInvitesLabel: "Invitaciones", modulesInvitesDescription: "Módulo para gestionar y aplicar códigos de invitación.", modulesWalletLabel: "Cartera", @@ -2904,12 +3065,8 @@ module.exports = { modulesOpinionsDescription: "Módulo para descubrir y votar opiniones.", modulesTransfersLabel: "Transferencias", modulesTransfersDescription: "Módulo para descubrir y gestionar contratos inteligentes (transferencias).", - modulesFediverseLabel: "Multiverso", - modulesBlogsLabel: "Blogs", - modulesBlogsDescription: "Módulo para descubrir y gestionar blogs.", - modulesPollsLabel: "Encuestas", - modulesPollsDescription: "Módulo para preguntar a la red y contar las respuestas.", - modulesFediverseDescription: "Gestiona tus otras cuentas del multiverso, incluyendo enviar y recibir contenido.", + modulesFediverseLabel: "Fediverse", + modulesFediverseDescription: "Gestiona tus otras cuentas del fediverso, incluyendo enviar y recibir contenido.", modulesFeedLabel: "Feed", modulesFeedDescription: "Módulo para descubrir y compartir textos breves (feeds).", modulesPixeliaLabel: "Pixelia", @@ -2941,33 +3098,37 @@ module.exports = { visibilityMakeHidden: "Hacer oculta", invitesInhabitantsTitle: "Habitantes", invitesInhabitantsFollow: "Seguir", - aiNavPlaceholder: "Describe qué necesitas...", + aiNavPlaceholder: "¿A dónde quieres ir?", aiNavDisabled: "La navegación con IA está desactivada.", - aiNavResultsTitle: "Sugerencias AINav", + aiNavResultsTitle: "Sugerencias de navegación", aiNavQueryLabel: "Consulta", + aiNavResultsDescription: "Elige la ruta que mejor se ajuste a lo que buscas.", + aiNavResultPath: "Ruta", + aiNavResultDescription: "Qué puedes hacer", aiNavResultMatch: "Coincidencia", aiNavResultsEmpty: "No hay rutas coincidentes. Prueba con /search.", + aiNavSearchInstead: "Buscar en su lugar", modulesAINavLabel: "AINav", modulesAINavDescription: "Módulo para realizar peticiones en lenguaje natural sobre el contenido de la red.", + directConnect: "Conexión Directa", directConnectDescription: "Conéctate directamente a un nodo introduciendo su dirección IP, puerto y clave pública. El nodo se añadirá como conexión seguida.", peerHost: "IP", peerPort: "Puerto (por defecto: 8008)", peerPublicKey: "Clave Pública (@...ed25519)", connectAndFollow: "Conectar", + deviceSourceLabel: "Dispositivo", modulesPresetTitle: "Configuraciones Comunes", - workflowsTitle: "Workflows", - workflowsDescription: "Un workflow fija el tema y qué módulos están activos.", - workflowsSet: "Aplicar workflow", - workflow_default: "Por defecto", - workflow_jobs: "Empleo", - workflow_ruling: "Gobierno", - workflow_politics: "Política", - workflow_social: "Social", - workflow_business: "Negocios", - workflow_night: "Noche", - workflow_mobile: "Móvil", + modulesPreset_minimal: "Mínimo", + modulesPreset_basic: "Básico", + modulesPreset_social: "Social", + modulesPreset_economy: "Economía", + modulesPreset_full: "Completo", statsCarbonFootprintTitle: "Huella de Carbono", + statsCarbonFootprintNetwork: "Huella de carbono de la red", + statsCarbonFootprintYours: "Tu huella de carbono", statsCarbonTombstone: "Huella del tombstoning", + feedSuccessMsg: "¡Feed publicado correctamente!", + dominantOpinionLabel: "Opinión predominante", uploadMedia: "Subir contenido multimedia (max: 50MB)", invitesUnfollow: "Dejar de seguir", invitesFollow: "Seguir", @@ -2975,11 +3136,7 @@ module.exports = { invitesNoUnfollowed: "No hay pubs no federados.", reportsWhyInappropriateLabel: "¿Por qué es inapropiado?", blockchainContentDeleted: "Este contenido ha sido eliminado", - visitContent: "Visitar Contenido", - favoriteAdd: "Fijar en Favoritos", - pmContentTooltip: "Enviar un Mensaje Privado", - favoriteRemove: "Quitar de Favoritos", - reportContent: "Reportar Contenido", + visitContent: "Visitar contenido", bankEcoinHours: "Equivalencia ECOin en tiempo", mapsLabel: "Mapas", mapTitle: "Mapas", @@ -3007,13 +3164,14 @@ module.exports = { mapDescriptionLabel: "Descripción", mapDescriptionPlaceholder: "Describe el mapa o ubicación...", mapTypeLabel: "Tipo de mapa", - mapInviteMode: "Invitación", - mapTypeSingle: "ÚNICO", - mapTypeOpen: "ABIERTO", - mapTypeClosed: "CERRADO", + mapTypeSingle: "ÚNICO (solo ubicación inicial)", + mapTypeOpen: "ABIERTO (cualquiera añade marcadores)", + mapTypeClosed: "CERRADO (solo el creador añade marcadores)", mapTagsLabel: "Etiquetas", mapTagsPlaceholder: "Introduce etiquetas separadas por comas", mapUrlLabel: "URL del mapa", + mapPickCoordLabel: "O selecciona una ubicación en la cuadrícula:", + mapMarkersLabel: "marcadores", mapMarkersTitle: "Marcadores", mapMarkerDefault: "Marcador", mapMarkerLatLabel: "Latitud del marcador", @@ -3027,6 +3185,8 @@ module.exports = { mapApplyZoom: "Aplicar Zoom", mapSearchPlaceholder: "Buscar descripción, etiquetas, autor...", mapSearchButton: "Buscar", + mapClickToCreate: "Haz click en el mapa para seleccionar ubicación", + mapClickToAddMarker: "Haz click en el mapa para añadir un marcador", mapUpdatedAt: "Actualizado", mapNoMatch: "Ningún mapa coincide con tu búsqueda.", noMaps: "No hay mapas disponibles.", @@ -3040,9 +3200,14 @@ module.exports = { padTitle: "Pad", modulesPadsLabel: "Pads", modulesPadsDescription: "Módulo para gestionar editores de texto colaborativos.", + padFilterAll: "TODOS", + padFilterMine: "MÍO", + padFilterRecent: "RECIENTE", + padFilterOpen: "ABIERTO", + padFilterClosed: "CERRADO", padCreate: "Crear Pad", - padUpdate: "Actualizar", - padDelete: "Eliminar", + padUpdate: "Actualizar Pad", + padDelete: "Eliminar Pad", padTitleLabel: "Título", padTitlePlaceholder: "Escribe el título del pad...", padStatusLabel: "Estado", @@ -3053,7 +3218,14 @@ module.exports = { padTagsLabel: "Etiquetas", padTagsPlaceholder: "etiqueta1, etiqueta2, ...", padMembersLabel: "Miembros", + padVisitPad: "Visitar Pad", + padShareUrl: "Compartir URL", padCreated: "Creado", + padAuthor: "Autor", + padGenerateCode: "Generar Código", + padInviteCodeLabel: "Código de Invitación", + padInviteCodePlaceholder: "Introduce el código de invitación...", + padValidateInvite: "Validar", padStartEditing: "¡EMPEZAR A EDITAR!", padEditorPlaceholder: "Empieza a escribir...", padSubmitEntry: "Enviar", @@ -3066,11 +3238,12 @@ module.exports = { padClosedSectionTitle: "Pads Cerrados", padCreateSectionTitle: "Crear Nuevo Pad", padUpdateSectionTitle: "Actualizar Pad", + padInviteGenerated: "Código de Invitación Generado", typePad: "PADS", padNew: "NUEVO", padAddFavorite: "Añadir a Favoritos", padRemoveFavorite: "Quitar de Favoritos", - padClose: "Cerrar", + padClose: "Cerrar Pad", padBackToEditor: "Volver al editor", padSearchPlaceholder: "Buscar pads...", @@ -3079,6 +3252,12 @@ module.exports = { modulesCalendarsLabel: "Calendarios", modulesCalendarsDescription: "Módulo para descubrir y gestionar calendarios.", typeCalendar: "CALENDARIOS", + calendarFilterAll: "TODOS", + calendarFilterMine: "MÍOs", + calendarFilterOpen: "ABIERTOS", + calendarFilterClosed: "CERRADOS", + calendarFilterRecent: "RECIENTES", + calendarFilterFavorites: "FAVORITOS", calendarCreate: "Crear Calendario", calendarUpdate: "Actualizar", calendarDelete: "Eliminar", @@ -3091,17 +3270,24 @@ module.exports = { calendarTagsLabel: "Etiquetas", calendarTagsPlaceholder: "etiqueta1, etiqueta2...", calendarParticipantsLabel: "Participantes", + calendarParticipantsCount: "Participantes", + calendarVisitCalendar: "Ver Calendario", calendarCreated: "Creado", + calendarAuthor: "Autor", calendarJoin: "Unirse al Calendario", calendarGenerateInvite: "Generar invitación", + calendarInviteCodePlaceholder: "Introduce código...", + calendarValidateInvite: "Validar código", + calendarJoined: "Unido", + calendarAddDate: "Añadir Fecha", + calendarAddNote: "Añadir Nota", calendarDateLabel: "Fecha", calendarDatePlaceholder: "Describe esta fecha...", - calendarNoteLabel: "Notas", + calendarNoteLabel: "Nota", calendarNotePlaceholder: "Añadir una nota...", calendarFirstDateLabel: "Fecha", calendarFirstNoteLabel: "Notas", calendarIntervalLabel: "Interval", - calendarIntervalNone: "Sin repetición", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3112,6 +3298,8 @@ module.exports = { calendarsDescription: "Descubre y gestiona calendarios en tu red.", calendarMonthPrev: "\u2190 Anterior", calendarMonthNext: "Siguiente \u2192", + calendarMonthLabel: "Fechas", + calendarsShareUrl: "URL para compartir", calendarAllSectionTitle: "Calendarios", calendarRecentSectionTitle: "Calendarios Recientes", calendarFavoritesSectionTitle: "Favoritos", @@ -3125,6 +3313,7 @@ module.exports = { calendarRemoveFavorite: "Quitar de Favoritos", calendarSearchPlaceholder: "Buscar calendarios...", calendarAddEntry: "Añadir Entrada", + calendarLeave: "Salir del Calendario", statsCalendar: "Calendarios", statsCalendarDate: "Fechas de Calendario", statsCalendarNote: "Notas de Calendario", @@ -3133,6 +3322,8 @@ module.exports = { modulesChatsDescription: "Módulo para descubrir y gestionar chats cifrados.", typeChat: "CHATS", typeChatMessage: "MENSAJE DE CHAT", + chatLabel: "CHATS", + chatMessageLabel: "MENSAJES DE CHAT", chatsTitle: "Chats", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3142,50 +3333,52 @@ module.exports = { chatDescription: "Descripción", chatCategory: "Categoría", chatStatus: "ESTADO", + chatFilterAll: "TODOS", + chatFilterMine: "MÍO", + chatFilterRecent: "RECIENTE", + chatFilterFavorites: "FAVORITOS", + chatFilterOpen: "ABIERTO", + chatFilterClosed: "CERRADO", chatCreate: "Crear Chat", - chatUpdate: "Actualizar", - chatDelete: "Borrar", + chatUpdate: "Actualizar Chat", + chatDelete: "Eliminar Chat", chatClose: "Cerrar Chat", chatVisitChat: "VER CHAT", chatUntitled: "Chat sin título", chatNoItems: "No se encontraron chats.", chatParticipants: "Participantes", - chatMessagesLabel: "Mensajes", - chatThreadsLabel: "Hilos", + chatStartChatting: "¡EMPEZAR A CHATEAR!", + chatGenerateCode: "Generar Código", + chatShareUrl: "Compartir URL", chatCreatedAt: "CREADO", chatSearchPlaceholder: "Buscar chats...", chatStatusOpen: "ABIERTO", chatStatusInviteOnly: "SOLO INVITADOS", chatStatusClosed: "CERRADO", chatSendMessage: "Enviar", - chatJumpLatest: "Último Mensaje", - chatPickChat: "Elige un chat para abrirlo", - chatNoneYet: "No existe ningún chat disponible, todavía.", - activitySearchPlaceholder: "Buscar en la actividad...", - trendingSearchPlaceholder: "Buscar en tendencias...", - opinionsSearchPlaceholder: "Buscar en opiniones...", - votesSearchPlaceholder: "Buscar en votaciones...", - welcomeStepUxTitle: "Elige tu interfaz", - welcomeStepUxText: "Decide cómo se ve Oasis. Podrás cambiarlo después en Ajustes.", - welcomeStepUxAction: "Usar esta vista", - chatRateLimitTitle: "Demasiados mensajes", - chatRateLimitMessage: "Has alcanzado el límite de mensajes que esta sala acepta en una hora. Inténtalo más tarde.", chatMessagePlaceholder: "Escribe tu mensaje...", chatNoMessages: "Sin mensajes aún.", + chatLeave: "Salir del Chat", + chatInviteCodeLabel: "Introduce el código de invitación", + chatJoinByInvite: "Unirse", chatTitlePlaceholder: "Título del chat", chatDescriptionPlaceholder: "Descripción del chat", chatTagsPlaceholder: "etiqueta1, etiqueta2", chatImageLabel: "Selecciona un archivo de imagen (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "Código de Invitación", chatAuthor: "Autor", + chatCreated: "Creado", chatAddFavorite: "Añadir a Favoritos", chatRemoveFavorite: "Quitar de Favoritos", chatPM: "MP", chatStatusLabel: "Estado", chatCategoryLabel: "Categoría", + chatParticipantsLabel: "Participantes", gamesTitle: "Juegos", gamesDescription: "Descubre y juega algunos juegos.", gamesFilterAll: "TODOS", gamesPlayButton: "¡JUGAR!", + gamesBackToGames: "Volver a Juegos", modulesGamesLabel: "Juegos", modulesGamesDescription: "Módulo para descubrir y jugar algunos juegos.", modulesGraphosLabel: "Graphos", @@ -3202,6 +3395,8 @@ module.exports = { gamesArkanoidDesc: "Rompe todos los ladrillos con tu paleta y pelota. Un clásico desafío arcade.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Ping-pong clásico contra una IA. El primero en llegar a 5 puntos gana.", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "¡Corre contra el tiempo! Esquiva el tráfico y llega a la meta antes de que se acabe el tiempo.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Pilota tu nave por un campo de asteroides mortal. Dispárales antes de que te alcancen.", gamesRockPaperScissorsTitle: "Piedra Papel Tijeras", @@ -3222,6 +3417,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3270,6 +3467,7 @@ module.exports = { torrentSortOldest: "Más antiguos", torrentSortTop: "Más votados", torrentNoMatch: "No se encontraron torrents coincidentes.", + torrentMessageAuthorButton: "Enviar Mensaje al Autor", torrentUpdatedAt: "Actualizado", statsTorrent: "Torrents", typeTorrent: "TORRENTS", @@ -3277,18 +3475,10 @@ module.exports = { modulesTorrentsDescription: "Módulo para descubrir y gestionar torrents.", favoritesFilterTorrents: "TORRENTS", favoritesFilterMarket: "MERCADO", - favoritesFilterEvents: "EVENTOS", - favoritesFilterForum: "FORO", - favoritesFilterHousing: "VIVIENDA", - favoritesFilterJobs: "EMPLEOS", - favoritesFilterProjects: "PROYECTOS", - favoritesFilterReports: "INFORMES", - favoritesFilterTasks: "TAREAS", - favoritesFilterTransfers: "TRANSFERENCIAS", - favoritesFilterVotes: "VOTACIONES", favoritesFilterShopProducts: "ARTÍCULOS DE TIENDA", tribeSectionTorrents: "TORRENTS", tribeCreateTorrent: "Subir Torrent", + tribeMediaTypeTorrent: "Torrent", settingsWishTitle: "Deseo", settingsWishDesc: "Configura el deseo de tu Avatar. Determina cómo accedes a todo el contenido y tu experiencia en la red Oasis.", settingsWishWhole: "Multiverse", @@ -3299,10 +3489,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "El envío es una barrera de UX. La recepción es un filtro de atención (el cifrado ya está replicado).", + pmBlockedNonMutual: "Solo puedes enviar mensajes privados a habitantes con apoyo mutuo.", inhabitantsPendingFollowsTitle: "Solicitudes de apoyo pendientes", inhabitantsPendingAccept: "Aceptar", inhabitantsPendingReject: "Rechazar", + bxEncrypted: "CIFRADO", + bxEncryptedHexLabel: "Texto cifrado (vista previa)", tribeSectionGovernance: "GOBIERNO", + tribeSubStatusPublic: "PÚBLICA", + tribeSubStatusPrivate: "PRIVADA", + tribeSubInheritedPrivate: "PRIVADA (heredada de la tribu principal)", tribePadInviteRequired: "No tienes acceso al pad. Pide una invitación para acceder al contenido.", tribeChatInviteRequired: "No tienes acceso al chat. Pide una invitación para acceder al contenido.", tribeGovernanceDesc: "Gobierno interno de esta tribu.", @@ -3342,31 +3538,44 @@ module.exports = { tribeGovNoHistorical: "Aún no hay ningún registro", logsTitle: "Bitácora", logsDescription: "Registra tu experiencia en la red.", + logsReadMore: "Leer más", logsViewTitle: "Bitácora", logsColumnType: "Tipo", logsView: "Ver", + logsManualPrompt: "Escribe tu bitácora", logsUpdateButton: "Actualizar", - logsEditTitle: "Actualizar registro", + logsEditTitle: "Editar entrada", + logsCreateDescription: "Registra tu experiencia...", logsCreateTitle: "Crear entrada", logsWriteButton: "Escribir", logsTextPlaceholder: "Describe tus experiencias...", + logsTextField: "Texto", + logsLabelPlaceholder: "Etiqueta...", + logsLabelField: "Escribe tu bitácora (etiqueta)", + logsAiDisabledWarn: "Activa el módulo de IA en /modules para usar entradas escritas por IA.", + logsAiContextValue: "Día de blockchain", + logsAiContext: "Contexto", + logsAiModStatus: "AImod", + logsModeApply: "¡Aplicar!", logsModeAIWritten: "AI-Assistant", logsModeManual: "Manual", logsModeAI: "IA", + logsColumnDelete: "Eliminar", + logsColumnEdit: "Editar", logsColumnLog: "Bitácora", logsColumnDate: "Fecha", logsDelete: "Eliminar", - logsEdit: "Actualizar", + logsEdit: "Editar", logsFilterAlways: "SIEMPRE", logsFilterYear: "ÚLTIMO AÑO", logsFilterMonth: "ÚLTIMO MES", logsFilterWeek: "ÚLTIMA SEMANA", logsFilterToday: "HOY", - logsComments: "Comentarios", - logsAuthorEntries: "Entradas", + logsFilterRecent: "RECIENTES", + logsFilterAll: "TODOS", logsCreate: "Crear entrada", logsExport: "Exportar bitácora", - logsExportOne: "Exportar registro", + logsExportOne: "Exportar", logsViewDetails: "Ver detalles", logsGenerateButton: "Generar texto", logsSearchText: "Buscar en bitácoras...", @@ -3376,25 +3585,31 @@ module.exports = { modulesLogsLabel: "Bitácora", modulesLogsDescription: "Módulo para registrar (vía asistente de IA) tus experiencias.", statsLogsTitle: "Bitácora", + statsLogsEntries: "Entradas", typeLog: "BITÁCORA", + blockchainCycle: "Ciclo", larpTitle: "L.A.R.P.", larpDescription: "Una capa de juego de rol en vivo para la experimentación colaborativa.", - larpNoHousesMatch: "Ninguna casa coincide con tu búsqueda.", - larpSearchPlaceholder: "Buscar casas...", larpAllHouses: "Casas:", larpFilterRuling: "RIGE", larpFilterHouses: "CASAS", larpFilterRules: "FAQ", larpRulesTitle: "FAQ del L.A.R.P.", + larpRulesEntryTitle: "Casa de inicio", larpRulesEntryText: "Todo habitante comienza en ACADEMIA por defecto. Desde ACADEMIA, elige una casa en el panel \"Unirse a una casa\": esto abre su test de entrada. Responde las 10 preguntas y envía. Si superas el umbral (7/10), serás admitido automáticamente en esa casa; si no, serás rechazado y permanecerás en ACADEMIA. En cualquier caso el sistema registra tu OasisID y el ciclo del intento e impide otro intento hasta que pase el enfriamiento de 30 ciclos.", + larpRulesLeaveText: "Los miembros de cualquier casa pueden volver a ACADEMIA en cualquier momento desde la página de su casa; al hacerlo se restablece su pertenencia, pero no se anula el enfriamiento del test.", + larpRulesGovernanceTitle: "Muro de gobierno", larpRulesGovernanceText: "Durante su ciclo, la casa gobernante luce la insignia \"Rige\" y es la única cuyo Muro se muestra públicamente bajo la pestaña RIGE.", - larpRulesWallText: "Cada casa tiene un Muro donde escriben sus miembros. El Muro de una casa es privado para la casa, salvo el de la casa gobernante y el de la ACADEMIA, que puede leer cualquiera.", + larpRulesSignTitle: "Emblema L.A.R.P.", larpRulesSignText: "Los habitantes pueden activarlo (en /profile/edit > Sensores) para mostrar la imagen de su casa actual como un sello en su perfil, en su tarjeta de autor y de habitante. El sello enlaza a la página de la casa.", larpPostsTitle: "Muro", larpPostsEmpty: "Aún no hay publicaciones.", + larpPostLabel: "Nueva publicación", larpPostPlaceholder: "¿Qué tiene que decir esta casa?", + larpPostPublic: "Pública (visible para cualquiera, si no solo miembros)", larpPostSubmit: "Publicar", + larpPostMembersOnly: "Solo miembros", larpCycleLabel: "Ciclo:", bankRulesArchTaxFormula: "ARCH Tax(usuario) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", bankRulesArchTaxNote: "El impuesto ARCH refleja el coste a largo plazo de hacer crecer y mantener el archivo de la red: cuanto más antiguos sean tus datos en la red, mayor será tu parte del coste de mantenimiento.", @@ -3432,9 +3647,12 @@ module.exports = { bankTaxesLookupNote: "Introduce un ID de bloque para buscarlo en tu feed local.", bankTaxesUserNoteChipsClick: "Haz clic en cualquier chip para inspeccionar su bloque en el blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntry: "Código de invitación", larpRulesInviteEntryText: "Los miembros de las casas pueden generar códigos de invitación que dan acceso directo a la casa.", larpRulesOneHouseText: "Solo puedes pertenecer a una casa a la vez. Si no perteneces a ninguna, estás en ACADEMIA. Cada página de casa (no ACADEMIA) muestra un botón \"Salir de la Casa\" a sus miembros que restablece la pertenencia a ACADEMIA. Salir NO anula el enfriamiento del test.", - larpRulesTestEntryText: "En el test WILL cada opción pondera una o más casas; al enviarlo, el sistema suma las puntuaciones y te admite automáticamente en la casa con más puntos.", + larpRulesOneHouseTitle: "Una casa cada vez", + larpRulesTestEntry: "Test WILL", + larpRulesTestEntryText: "Cada opción pondera una o varias casas; al enviar, el sistema suma los puntos y te admite automáticamente en la casa con mayor puntuación.", larpWillTestHint: "Una vez completado, se te asignará la casa que más se ajuste a tus respuestas. Tus respuestas individuales se procesan localmente y nunca se almacenan — solo se publica en tu feed la asignación de casa resultante.", larpWillTestTitle: "Test WILL", settingsLanDesc: "Anuncia periódicamente este peer a otras instancias de Oasis en la misma red local. Desactivar para detener las difusiones UDP.", @@ -3453,30 +3671,36 @@ module.exports = { aiExchangeMarkHelpful: "+1 útil", larpCyclesUntilRuling: "Próximo ciclo de gobierno", larpVisitTribe: "Visitar tribu", - larpStartJourney: "Comenzar el viaje", - larpGoverning: "Gobierna:", - larpStartHere: "Empieza aquí:", + larpGoverning: "Gobernando:", larpMyHouse: "Mi Casa:", larpMembersCount: "Miembros", + larpMembersTitle: "Miembros", + larpNoMembers: "Aún no hay miembros.", larpRolesLabel: "Roles", larpFunctionLabel: "Función", larpMonthLabel: "Ciclo de gobierno", larpLeaveToAcademia: "Volver a ACADEMIA", - larpAcademiaJoinTitle: "Casas L.A.R.P.", + larpAcademiaJoinTitle: "Unirse a una casa", + larpAcademiaJoinHint: "Realiza el test psicológico de perfil para que se te asigne la casa que mejor encaja contigo, o canjea un código de invitación compartido por algún miembro de una casa.", + larpTakeTestButton: "Hacer el test de perfil", larpInviteRedeem: "Unirse a la Casa", larpInviteCreate: "Generar código de invitación", larpInvitePlaceholder: "Código de invitación", larpInviteBannerTitle: "Nuevo código de invitación", larpInviteBannerHint: "Comparte este código con alguien que esté en ACADEMIA. Expira en 30 ciclos o tras un uso.", + larpTestAssignedHouse: "Casa asignada", larpTestRankingTitle: "Puntuación por casa", invitesHousesTitle: "L.A.R.P.", invitesHouseInviteCodePlaceholder: "Código de invitación de casa", invitesHouseJoinButton: "Unirse a la Casa", ecoTaxLabel: "ECO Tax", + ecoinTaxLabel: "ECOin Tax", bankTaxes: "Impuestos", bankOverviewYourTaxes: "Tus impuestos", + bankTaxesNetworkTitle: "Impuestos de la red", bankTaxesEcoTaxTitle: "ECO Tax", bankTaxesArchTaxTitle: "ARCH Tax", + bankTaxesUserTitle: "Tus impuestos", bankTaxesUserAmount: "Tu ECO tax (precio a devolver)", bankTaxesArchTaxUserAmount: "Tu ARCH tax (precio a devolver)", bankTaxesArchTaxFirstBlock: "Edad de tu primer bloque", @@ -3527,13 +3751,16 @@ module.exports = { bankRulesEcoTaxRate: "Tasa", bankRulesArchTaxRate: "Tasa", bankRulesCarbonFactor: "Factor de carbono", + statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "Aún no hay tombstones en la red.", blockchainBlockNotAvailable: "Este bloque no está disponible en tu feed local. Puede pertenecer a un peer del que aún no replicas.", blockchainBackToBlockexplorer: "Volver al blockexplorer", audioFilterBcs: "BCS", audioTranscodeButton: "TRANSCODE", + audioTranscodeTitle: "Transcode BCS", audioTranscodeDetailTitle: "Transcodificar", audioTranscodeDetailDescription: "Decodifica la carga útil incrustada y el mapa de composición original de la blockchain.", + audioTranscodeStegoTitle: "Incrustado en la forma de onda", audioTranscodeStegoOasisId: "By", audioTranscodeStegoTimestamp: "Generado el", audioTranscodeStegoMessage: "TEXT", @@ -3541,7 +3768,9 @@ module.exports = { audioTranscodeStegoUnknown: "—", audioTranscodeStegoNotFound: "No se ha podido decodificar ninguna carga esteganográfica en este audio.", audioTranscodeCompositionEmpty: "Este audio no incluye una composición de blockchain guardada.", + audioBackToAudio: "Volver al audio", audioBackToBcs: "Volver a BCS", + audioAuthorLabel: "Autor", melodyCompositionTitle: "Mapa de composición de la blockchain", melodyFilterMine: "MINE", melodyByLabel: "Por", @@ -3555,6 +3784,7 @@ module.exports = { melodyUploadBcsNameNote: "El audio se publicará con el nombre auto-generado", larpLastAttemptTitle: "Tu último intento", larpLastAttemptHouse: "Casa", + larpLastAttemptResult: "Resultado", larpLastAttemptWhen: "Cuándo", larpLastAttemptCooldown: "Próximo intento en", larpTestTitle: "Test de entrada", @@ -3563,14 +3793,27 @@ module.exports = { larpTestCooldownActive: "Próximo test disponible en", larpTestCooldownDays: "ciclos", larpTestResultTitle: "Resultado del test", + larpTestPassed: "Test superado — ¡bienvenida!", + larpTestFailed: "Test no superado", larpTestScore: "Puntuación", larpTestNextAttempt: "Puedes intentar otro test en 30 ciclos.", larpGoToHouse: "Ir a tu casa", larpBackToAcademia: "Volver a ACADEMIA", + larpBackToHouses: "◀ Casas", larpBadgeYou: "Tú", larpBadgeRuling: "Rige", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Módulo para la capa de juego de rol en vivo de Oasis (las 9 Casas).", + melodyUploadTitlePlaceholder: "Título de la composición", + melodyUploadDescPlaceholder: "Descripción opcional", + bankTaxesUserNote: "Tu impuesto ECO se deduce del excedente que la red genera sobre tu UBI; el valor base del UBI es un mínimo inamovible. El impuesto nunca se deduce del importe fijo que corresponde a cada habitante como UBI. Los fondos deducidos alimentan el algoritmo de redistribución de la riqueza y se canalizan a otros habitantes a través de sus reclamaciones de UBI. Aquellos otros habitantes que reciban más UBI para sus proyectos probablemente tengan alguna tarea dedicada a ayudarte a reducir tu impuesto ECO. O pueden dedicarse a reducir el impuesto ECO directamente, y su contribución continua puede ser requerida por consenso de la red.", + bankTaxesArchTaxNote: "El impuesto ARCH refleja el coste de hacer crecer y mantener el archivo de la red. Se calcula a partir del tiempo transcurrido entre tu primer bloque publicado y el bloque más nuevo de la red — cuanto más viva tu historia en el archivo, mayor será tu parte del coste de mantenimiento. Al igual que el impuesto ECO, el impuesto ARCH se deduce del excedente que la red añade sobre tu UBI; el UBI base nunca se reduce por él.", + bankTaxesExample: "Ejemplo", + bankTaxesSummaryNote: "Total agregado de todos los tipos de impuestos que aplica la red. Actualmente solo el impuesto ECO (huella de carbono) está activado; futuros tipos de impuestos aparecerán como filas adicionales y contribuirán al total.", + audioTranscodeDescription: "Interpretación del audio: la composición original de la blockchain que lo produjo, además del OasisID, marca temporal y mensaje oculto incrustado en la forma de onda mediante esteganografía.", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + gameScoreLabel: "PUNTUACIONES", larpProfileQ1: "Cuando te enfrentas a un problema complejo, ¿qué haces primero?", larpProfileQ1O1: "Hablar con otras personas para buscar consenso", larpProfileQ1O2: "Construir un prototipo para probarlo", diff --git a/src/client/assets/translations/oasis_eu.js b/src/client/assets/translations/oasis_eu.js index 30b7521..f67b698 100644 --- a/src/client/assets/translations/oasis_eu.js +++ b/src/client/assets/translations/oasis_eu.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { eu: { + bbAdd: "Gehitu", + bbQuickActions: "Ekintza azkarrak", + bbPickTitle: "Ainguratu modulu bat", + bbPickDesc: "Aukeratu modulu bat beheko barrako + hutsunerako.", + bbPickNone: "Bat ere ez", + bbManage: "Editatu", + bbYourBar: "Zure barra", + bbRemove: "Kendu", + bbFull: "Gehienekora iritsi da", + bbDone: "Eginda", + activityChangeFilter: "Aldatu iragazkia", + emptyConnectHint: "Konektatu pub batera sarearekin sinkronizatzeko.", + emptyConnectCta: "Konektatu pub batera", + menuCommunity: "Komunitatea", + activityGroupCommunity: "Komunitatea eta gobernantza", + activityGroupOrg: "Antolakuntza", + activityGroupComm: "Komunikazioa", + activityGroupEconomy: "Ekonomia", + activityGroupMedia: "Edukia eta multimedia", + activityGroupOther: "Bestelakoak", + languageName: "Basque", shopOrderStatusPaid: "Ordainketa jasota", shopOrderStatusReceived: "Jasota", shopOrderMarkPaid: "Markatu ordainketa jasota", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "Oraindik ez duzu erosketarik.", shopOrderPmSeller: "PM Seller", shopOrderSeller: "Saltzailea", + shopOrderPaymentConcept: "Ordainketa", shopOrderInvoice: "Faktura", shopOrderInvoiceConcept: "Faktura", shopOrderStatusPending: "Zain", shopOrderStatusAccepted: "Onartua", shopOrderStatusRejected: "Baztertua", shopOrderStatusShipped: "Bidalia", + shopOrderStatusDelivered: "Entregatua", shopOrderAccept: "Onartu", shopOrderReject: "Baztertu", shopOrderMarkShipped: "Markatu bidalia", + shopOrderMarkDelivered: "Markatu entregatua", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "Kontratua", shopOrderContractConcept: "Dendako eskaera", shopOrdersPending: "Eskaera zain", all: "Guztiak", @@ -50,13 +75,14 @@ module.exports = { viewJson: "Ikusi JSON", matchScore: "Bat-egite puntuazioa", preferencesLabel: "Hobespenak", - inhabitantProfileTitle: "Biztanlea", + inhabitantProfileTitle: "Biztanlearen profila", contentWarningLabel: "Eduki abisua", invalidPost: "Eduki baliogabea", invalidPostHint: "Mezu honek testu baliogabea/hutsa du.", spreadBy: "Honek", spreadChron: "Hedapena", spreaded: "Hedatua", + spreadMore: "gehiago", spreadContentUnavailable: "Edukia oraindik ez dago eskuragarri (erreplikazioa zain)", filteredByTag: "Etiketaz iragazia", filteredByTagTitle: "Etiketaz iragazia", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "Larritasunik ez", calendarDeleteDate: "Ezabatu data", calendarIntervalUntil: "Arte", + bookmarkCategory: "Kategoria", bookmarkLastVisit: "Azken bisita", profileClearnetBookmarksLabel: "Laster-markak", + forumFilterHot: "Beroa", invitesPort: "Ataka", currentlyUnrecheable: "ERROREA!", peerHostValidation: "IPv4 baliozkoa (adib. 192.168.1.100) edo ostalari-izena (adib. pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "Urteko gehienezko estimazioa", statsCarbonNetwork: "Sarearen guztira", statsCarbonOfEstMax: "estimatutako gehienezko ahalmenaren", - statsCarbonYearProgress: "urtea igarota", - statsNetworkTitle: "Sarea", - statsStorageTitle: "Biltegiratzea", - statsEcoTaxTitle: "ECO tasa", - statsAccountTitle: "Kontua", - statsAveragesTitle: "Batez bestekoak", statsCarbonOfNetwork: "sarearen guztizkoaren", statsCarbonUser: "Zure aztarna", statsContentCountColumn: "Kopurua", @@ -154,6 +176,10 @@ module.exports = { statsTopTagsTitle: "Etiketa nagusiak", statsTopTypesTitle: "Eduki mota nagusiak", statsTotalMsgs: "Mezuak guztira", + feedViewDetails: "Ikusi xehetasunak", + eventFileLabel: "Erantsi irudia", + taskFileLabel: "Erantsi irudia", + courtsRespondentRequired: "Demandatuaren IDa beharrezkoa da", extended: "Multibertsoa", extendedDescription: [ "Norbait laguntzen duzunean, laguntzen dituzun erabiltzaileen bidalketak deskargatu ditzakezu. Mezu horiek hemen azalduko dira, dataren arabera antolatuta.", @@ -197,22 +223,29 @@ module.exports = { graphosShowing: "Erakusten", graphosYou: "Zu", graphosTotalNodes: "Nodo guztiak", + manualMode: "Eskuzko modua", mentions: "Aipamenak", mentionsDescription: [ - strong("Zu @aipatzen zaituzten mezu guztiak"), - ".", + strong("@Aipatzen zaituzten bidalketak."), + ", gaurkotasunaren arabera antolatuta.", ], + nextPage: "Hurrengoa", + previousPage: "Aurrekoa", noMentions: "Ez duzu aipamenik jaso, oraindik.", - mentionsSearchPlaceholder: "Bilatu aipamenak...", privateReminders: "OROIGARRIAK", inboxFiltersLabel: "Iragazkiak:", inboxToggleToMutuals: "Mutuora aldatu", inboxToggleToWhole: "Guztiora aldatu", + privateFrom: "Nork", + privateTo: "Nori", privateDate: "Data", peers: "Parekoak", searchDescription: "Deskribapena", + imageSearch: "Irudiak bilatu", searchFromLabel: "Hasiera", searchToLabel: "Bukaera", + searchMinOpinionsLabel: "Gutxi. iritziak", + searchInhabitantLabel: "Biztanle (Oasis ID)", searchQueryLabel: "Bilaketa", searchPerPageLabel: "Emaitzak orriko", settings: "Ezarpenak", @@ -225,39 +258,68 @@ module.exports = { melodyDescription: 'Erreproduzitu zure blockchain-aren melodia — bloke bakoitza nota bat bihurtzen da.', melodyEmpty: 'Oraindik notarik ez — zure blockchain-ak ez du oraindik blokerik sortu.', melodyCompositionTitle: 'Konposizioa', + melodyCompositionHelp: 'Zure blockchain-eko bloke bakoitza musika-nota bat bihurtzen da, motaren eta tamainaren arabera.', + melodyStatsTitle: 'Motaren araberako banaketa', + melodyInhabitantLabel: 'Biztanlea', melodyTotalBlocks: 'Notak', + melodyType: 'Mota', + melodyCount: 'Blokeak', melodyFilterMine: 'NIREA', melodyFilterAll: 'GUZTIAK', + melodyFilterShare: 'Igo melodia', + melodyShareTitle: 'Partekatu zure melodia', + melodyShareHelp: 'Argitaratu zure uneko melodiaren irudia, besteek entzun eta birmoldatu ahal izateko.', + melodyShareTitleLabel: 'Izenburua (aukerakoa)', + melodyShareTitlePlaceholder: 'Blockchain mamu bat', + melodyShareSubmit: 'Argitaratu melodia', + melodyNoShared: 'Oraindik ez dago blockchain melodiarik.', melodyRegenerate: 'Birsortu', melodyDownload: "Deskargatu BCS", + profileActivityTitle: 'Jarduera', + profileActivityMore: 'Ikusi jarduera guztia', profileContentSectionTitle: 'Avatarraren Edukia', profileContentHelp: 'Aukeratu zein modulu agertuko diren zure abatarrean.', profileSensorsHelp: 'Zure abatarrean agertzen diren aukerako metrikak.', profileClearnetHelp: 'Oasis kanpotik atzitu daitezkeen moduluak.', profileHubAll: 'Guztiak', + profileHubTitle: 'Eduki Publikoa', + footerPulseTitle: 'Pultsua', + footerPulsePeers: 'Kideak', + footerPulseInbox: 'Sarrera-ontzia', + footerPulseSync: 'Azken sinkr.', + footerPulseEcoValue: 'ECO/h', + footerPulseLastActivity: 'Azken jarduera', footerLicenseLabel: 'Lizentzia', profileSwitchToOasis: 'Itzuli Oasisera', - profileVisibilityFediverse: "Multibertsoa", + profileSwitchToClearnet: 'Murgildu Clearneten', + profileVisibilityFediverse: "Fediverse", + profileSwitchToFediverse: "Murgildu fediversoan", + coordLabel: 'Koordenatuak (Adb. A3)', + coordPlaceholder: 'Sartu koordenatuak', contributorsTitle: "Ekarpenak", colorLabel: 'Hautatu kolorea', paintButton: 'Koloreztatu!', + invalidCoordinate: 'Koordenatu okerrak', + goToMuralButton: "Ikusi Murala", totalPixels: 'Pixelak guztira', + pixeliaBy: "egilea", modules: "Moduluak", modulesViewTitle: "Moduluak", modulesViewDescription: "Konfiguratu zure ingurunea moduluak gaitu edo desgaituz.", inbox: "Sarrera-ontzia", multiverse: "Multibertsoa", - fediverse: "Multibertsoa", - fediverseDescription: "Kudeatu zure beste multibertso kontuak, edukia bidaltzea eta jasotzea barne.", + fediverse: "Fediverse", + fediverseDescription: "Kudeatu zure beste fediverso kontuak, edukia bidaltzea eta jasotzea barne.", fediverseStatus: "Egoera", - fediverseDisconnected: "Multibertsoaa deskonektatuta. Egiaztatu %LINK% edo konexioaren egoera.", - fediverseSettingsTitle: "Multibertsoa", + fediverseDisconnected: "Fediversoa deskonektatuta. Egiaztatu %LINK% edo konexioaren egoera.", + fediverseSettingsTitle: "Fediverse", fediverseInstanceLabel: "Helbidea", fediverseTokenLabel: "Sarbide token-a", fediverseTokenHelp: "Ezarri zure sarbide token-a. Zure Mastodon kontua Oasis barrutik kudeatzeko erabiliko da.", fediverseConnect: "Konektatu", fediverseConnectedAs: "Honela konektatuta", fediverseDisconnect: "Deskonektatu", + fediverseEmpty: "Zure %LINK% beste fediverso kontu batzuk konektatzen dituzunean, hemendik kudeatu ahal izango dituzu.", fediverseEmptyLink: "ezarpenetatik", fediverseComposePlaceholder: "Zer duzu buruan?", fediverseReplyPlaceholder: "Idatzi zure erantzuna…", @@ -266,15 +328,21 @@ module.exports = { fediverseReply: "Erantzun", fediverseBoosted: "bultzatu du", fediverseNoPosts: "Zure denbora-lerroa hutsik dago.", + fediverseThread: "Haria", fediverseTimeline: "Denbora-lerroak", + fediverseManageTimeline: "Kudeatu denbora-lerroa", fediverseFollowers: "Jarraitzaileak", fediverseFollowing: "Jarraitzen", fediversePosts: "Argitalpenak", fediverseJoined: "Elkartua", + fediverseOverview: "Ikuspegi orokorra", fediverseRefresh: "Freskatu", fediverseWrite: "Idatzi", fediversePreview: "Aurrebista", + fediverseEdit: "Editatu", + fediverseOpenFeed: "Ikusi jarioa", fediverseManage: "Kudeatu", + fediverseStatusNotConnected: "Konektatu gabe", fediverseError: "Ezin izan da Mastodon-ekin konektatu.", fediverseErrInstance: "Instantzia URL baliogabea.", fediverseErrAuth: "Token baliogabea edo iraungia.", @@ -285,6 +353,17 @@ module.exports = { fediverseErrPost: "Ezin izan da argitaratu.", fediverseErrMedia: "Ezin izan da fitxategia igo.", fediverseErrEmpty: "Idatzi zerbait edo erantsi fitxategi bat.", + popularLabel: "Nabarmenak", + topicsLabel: "Gaiak", + latestLabel: "Azkena", + summariesLabel: "Laburpenak", + threadsLabel: "Hariak", + multiverseLabel: "Multibertsoa", + inboxLabel: "Sarrera-ontzia", + invitesLabel: "Gonbidapenak", + walletLabel: "Zorroa", + legacyLabel: "Gakoak", + cipherLabel: "Zifratzailea", bookmarksLabel: "Markagailuak", videosLabel: "Bideoak", torrentsLabel: "Torrentak", @@ -295,14 +374,18 @@ module.exports = { inhabitantsLabel: "Bizilagunak", trendingLabel: "Pil-pilean", eventsLabel: "Gertakariak", + tasksLabel: "Atazak", apply: "Aplikatu", menuPersonal: "Pertsonala", + menuContent: "Edukia", + menuBlogs: "Blogak", menuGovernance: "Gobernantza", menuOffice: "Bulegoa", - menuNetwork: "Komunitatea", + menuMultiverse: "Multibertsoa", + menuNetwork: "Sarea", menuCreative: "Sortzailea", menuEconomy: "Ekonomia", - menuMedia: "Liburutegia", + menuMedia: "Multimedia", menuTools: "Tresnak", comment: "Iruzkindu", subtopic: "Azpi-gaia", @@ -312,6 +395,13 @@ module.exports = { follow: "Lagundu", block: "Blokeatu", unblock: "Desblokeatu", + newerPosts: "Bidalketa berrienak", + olderPosts: "Bidalketa zaharrenak", + feedRangeEmpty: "Hautatuako tartea hutsik dago jario honentzat. Saiatu ", + seeFullFeed: "jario osoa ikusiz", + feedEmpty: "Oasis sareak ez du kontu honen bidalketarik ikusi inoiz.", + beginningOfFeed: "Jarioaren hasiera da hau", + noNewerPosts: "Ez da bidalketa berriagorik jaso oraindik.", relationshipNotFollowing: "Ez zaitu laguntzen", relationshipTheyFollow: "Laguntzen zaitu", relationshipMutuals: "Elkar laguntzen", @@ -321,20 +411,25 @@ module.exports = { relationshipBlockedBy: "Blokeatzen zaitu", relationshipMutualBlock: "Elkar blokeatzen", relationshipNone: "Ez duzu laguntzen", + relationshipConflict: "Gatazka", relationshipBlockingPost: "Blokeatutako bidalketa", viewLikes: "Ikusi zabalpenak", spreadedDescription: "Bizilagunak zabaldutako bidalketen zerrenda.", + totalspreads: "Hedapenak guztira", + attachFiles: "Erantsi fitxategiak", preview: "Aurrebista", publish: "Idatzi", contentWarningPlaceholder: "Gehitu gaia bidalketari (hautazkoa)", - publishWarningPlaceholder: "Gorputza 7000 karakteretara mugatua.", - publishTooLong: "Zure argitalpena luzeegia da. Laburtu ezazu, mesedez.", + privateWarningPlaceholder: "Gehitu bizilagunak mezu pribatua bidaltzeko (hautazkoa)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "GOGORATU: Blockchain teknologia dela eta, bidalketa argitaratu ezkero, ezin izango da aldatu ezta ezabatu.", ], commentWarning: [ "GOGORATU: Blockchain teknologia dela eta, bidalketa argitaratu ezkero, ezin izango da aldatu ezta ezabatu.", ], + commentPublic: "publikoa", + commentPrivate: "pribatua", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -354,6 +449,7 @@ module.exports = { "beharko zenuke.", ], publishCustom: "Idatzi bidalketa aurreratua", + subtopicLabel: "Bidalketa honen azpi-gaia sortu", messagePreview: "Aurreikusi bidalketa", mentionsMatching: "Bat datozen aipamenak", mentionsName: "Izena", @@ -376,93 +472,21 @@ module.exports = { ssbLogStream: "Blockchain", ssbLogStreamDescription: "Konfiguratu blockchain fluxuetarako mezu-muga.", saveSettings: "Ezarpenak gorde", + exportTitle: "Esportatu datuak", exportDescription: "Ezarri pasahitza (gutxienez 32 karaktere) zure gakoa zifratzeko", exportDataTitle: "Babeskopia", exportDataDescription: "Deskargatu zure datuak (gako sekretua izan ezik!)", exportDataButton: "Deskargatu datu-basea", + pubWallet: "PUB Wallet", + pubWalletDescription: "Ezarri PUB wallet-aren URLa. Hau PUB transakzioetarako erabiliko da (RBU barne).", + pubWalletConfiguration: "Ezarpenak gorde", + importTitle: "Inportatu datuak", importDescription: "Inportatu zifratutako sekretua (gako pribatua) zure abatarra gaitzeko", + importAttach: "Gehitu zifratutako fitxategia (.enc)", + passwordLengthInfo: "Pasahitzak 32 karaketereko luzera izan behar du.", passwordImport: "Idatzi pasahitza zure karpeta nagusian gordeko diren datuak deszifratzeko (izena: secret)", exportPasswordPlaceholder: "Erabili minuskulak, maiuskulak, zenbakiak eta sinboloak.", - fileInfo: "Zure gako sekretu zifratua zure gailura deskargatuko da (izena: oasis.enc)", - developer: "Garapena", - devTitle: "Garapena", - welcomeBannerText: "Ongi etorri OASISera. Jarraitu urrats azkar hauek zure avatarra konfiguratzeko.", - aiSuggestionBanner: "Iradokizuna:", - aiSuggestionsEnable: "AAren iradokizunak", - welcomeBannerAction: "Hasi!", - welcomeTitle: "Ongi etorri", - welcomeStepGreetingAction: "Bidali", - welcomeJoinPubButton: "Batu pubera", - welcomeStepLarpAction: "Batu \"Akademiara\"", - welcomeStepLarpText: "Batu gure zuzeneko rol jokora.", - welcomeStepLarpTitle: "Batu L.A.R.P.era", - welcomeStepGreetingTitle: "Bidali \"Kaixo mundua!\" bat", - welcomeStepGreetingText: "Bidali mezu bat beste biztanleek aurki zaitzaten.", - welcomeJoinPub: "Batu:", - welcomeDescription: "Hasi aurretik komeni diren gauza batzuk.", - welcomeProgress: "Osatuta", - welcomeStepDone: "eginda", - welcomeStepPending: "egiteke", - welcomeSkip: "Orain ez", - welcomeStepLanguageTitle: "Aukeratu zure hizkuntza", - welcomeStepLanguageText: "Oasisek hainbat hizkuntza hitz egiten ditu. Nahi duzunean alda dezakezu.", - welcomeStepProfileTitle: "Editatu zure profila", - welcomeStepProfileText: "Aukeratu izena, gehitu deskribapena eta jarri irudi bat beste biztanleek ezagut zaitzaten.", - welcomeStepProfileAction: "Gorde profila", - welcomeStepFederationTitle: "Batu sare nagusira", - welcomeStepFederationText: "Konektatu gure pubera beste biztanleak ezagutu eta erreplikatzen hasteko.", - welcomeStepFederationAction: "Joan gonbidapenetara", - welcomeStepBackupTitle: "Egin zure IDaren babeskopia", - welcomeStepBackupText: "Zure identitatea gailu honetako gako-fitxategi bat da.", - welcomeStepBackupWarning: "GALTZEN BADUZU, INORK EZIN DIZU BERRESKURATU.", - welcomeStepBackupAction: "Babeskopia!", - welcomeCompleteTitle: "Dena prest.", - welcomeCompleteText: "Zure nodoa prest dago. Hemendik aurrera sarea jarraitzen duzun jendearekin hazten da.", - welcomeFindPeople: "Bilatu biztanleak", - devFilterFiles: "FITXATEGIAK", - devFilterSearch: "BILATU", - devFilterModules: "MODULUAK", - devFilterTranslations: "ITZULPENAK", - devFilterStyles: "ESTILOAK", - devFilterTests: "TESTAK", - devVersion: "Bertsioa", - devNodeVersion: "Node", - devStatsTests: "Test multzoak", - devParentFolder: "Goiko karpeta", - devOpenFolder: "ireki", - devDescription: "Arakatu nodo honetan exekutatzen ari den kodea.", - devRoot: "erroa", - devSearchPlaceholder: "Kodean bilatzeko testua", - devAnyExtension: "Edozein fitxategi", - devStatsFiles: "Fitxategiak", - devStatsLines: "Lerroak", - devStatsModules: "Moduluak", - devStatsLanguages: "Hizkuntzak", - devNotViewable: "ez ikusgai", - devEmptyFolder: "Karpeta hau hutsik dago.", - devSize: "Tamaina", - devShowing: "Erakusten", - devLine: "lerroa", - devReportBug: "Akatsa Jakinarazi", - devCreateTask: "Ataza Sortu", - devRaw: "RAW", - devReportContext: "Oasis kodea irakurtzean aurkitua", - devTaskPrefix: "Berrikusi", - devPrevPage: "Aurreko lerroak", - devNextPage: "Hurrengo lerroak", - devMatches: "bat etortze", - devFilesScanned: "fitxategi aztertuta", - devNoResults: "Oraindik ez dago bat etortzerik.", - devColModule: "Modulua", - devColState: "Egoera", - devColModel: "Eredua", - devColView: "Ikuspegia", - devColRoutes: "Bideak", - devColTests: "Testak", - devOn: "aktibo", - devOff: "ez-aktibo", - modulesDevLabel: "Garapena", - modulesDevDescription: "Oasis kodea kudeatzeko modulua.", + fileInfo: "Zifratutako gako sekretua zure etxeko karpetan gordeko da. (izena: oasis.enc)", themeIntro: "Hautatu gaia.", setTheme: "Ezarri gaia", @@ -470,13 +494,16 @@ module.exports = { languageDescription: "Beste hizkuntza bat erabili nahi baduzu, hautatu hemen.", setLanguage: "Ezarri hizkuntza", + peerConnections: "Parekoak", peerConnectionsIntro: "Kudeatu beste parekoekin dituzun konexio guztiak.", peerConnectionsTitle: "Konexioak", online: "Linean", offline: "Lineaz kanpo", discovered: 'Aurkituak', unknown: 'Ezezagunak', + lanBroadcastLabel: "LAN igorpena", lanBroadcastActive: "Aktibo (UDP multicast LANean)", + lanBroadcastDisabled: "Desgaituta (pub modua)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -489,6 +516,8 @@ module.exports = { noConnections: "Konektatutako parekorik ez.", noDiscovered: "Ez duzu nodorik aurkitu.", noUnknownPeers: "Ez dago kide ezezagunik adiskidetza-grafoan.", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -498,6 +527,9 @@ module.exports = { peerImport: "Inportatu", peerImportTitle: "Peer-zerrenda inportatu", peerImportPlaceholder: "Itsatsi multiserver-helbide bat lerro bakoitzeko, edo igo .txt fitxategi bat…", + noSupportedConnections: "Lagundutako parekorik ez.", + noBlockedConnections: "Blokeatutako parekorik ez.", + noRecommendedConnections: "Gomendatutako parekorik ez.", connectionActionIntro: "", startNetworking: "Abiarazi sarea", @@ -511,19 +543,9 @@ module.exports = { homePageDescription: "Hautatu nahi duzun moduluaren orria hasiera gisa.", saveHomePage: "Gorde hasierako orria", invites: "Gonbidapenak", + invitesTitle: "Gonbidapenak", + invitesInvites: "Gonbidapenak", invitesDescription: "Kudeatu eta erabili gonbidapen kodeak zure sarean.", - invitesPubsHint: "Itsatsi pub baten gonbidapen-kodea harekin federatzeko eta bere biztanleak errepikatzen hasteko.", - invitesFederationsHint: "Federatuta zauden pubak: eguneratu, kendu iritsezinak edo esportatu zerrenda.", - invitesHousesHint: "Sartu etxe-kode bat LARPeko etxe batean sartzeko eta bere ekonomian parte hartzeko.", - invitesTribesHint: "Sartu tribu-kode bat kide egiteko eta bere eduki pribatura iristeko.", - invitesChatsHint: "Sartu txat-kode bat elkarrizketara eta bere harietara batzeko.", - invitesPadsHint: "Sartu pad-kode bat besteekin batera dokumentu partekatu batean idazteko.", - invitesCalendarsHint: "Sartu egutegi-kode bat datak eta oroigarriak partekatzeko.", - invitesEventsHint: "Sartu ekitaldi-kode bat ekitaldi pribatu batera batzeko.", - invitesForumsHint: "Sartu foro-kode bat foro pribatu batean irakurri eta erantzuteko.", - invitesMapsHint: "Sartu mapa-kode bat mapa partekatu batean lekuak ikusi eta gehitzeko.", - invitesShopsHint: "Sartu denda-kode bat denda batera eta bere katalogora batzeko.", - invitesInhabitantsHint: "Sartu biztanle-kode bat elkar jarraitzeko eta edukia trukatzeko.", invitesTribesTitle: "Tribuak", invitesTribeInviteCodePlaceholder: "Sartu tribuaren gonbidapen kodea", invitesTribeJoinButton: "Sartu Tribuan", @@ -554,6 +576,7 @@ module.exports = { invitesAlreadyFederated: "Dagoeneko pub honekin federatuta zaude.", invitesFederationsTitle: "Federazioak", invitesAcceptedInvites: "Federatuak", + invitesNoInvites: "Ez da gonbidapenik onartu, oraindik.", invitesUnfollow: "Utzi jarraitzea", invitesFollow: "Jarraitu", invitesUnfollowedInvites: "Ez federatuak", @@ -573,24 +596,39 @@ module.exports = { panicMode: "Izu Modua!", encryptData: "Idatzi pasahitza (gutxienez 32 karaketere) zure blockchain-a zifratzeko.", decryptData: "Sartu pasahitza zure blockchain-a deszifratzeko.", + panicModeDescription: "Zifratu/deszifratu edo EZABATU zure blockchain-a", removeDataDescription: "KONTUZ: Prozesu hau ezin da desegin.", + encryptPanicButton: "Zifratu blockchain-a", + decryptPanicButton: "Deszifratu blockchain-a", removePanicButton: "EZABATU ZURE DATU GUZTIAK!", searchTitle: "Bilatu", searchDescriptionLabel: "Bilatu edukia zure sarean.", + searchLanguagesLabel: "Hizkuntzak", + searchSkillsLabel: "Trebetasunak", searchPlaceholder:"Bilatu edukia...", + searchDateLabel:"Data", searchLocationLabel:"Kokapena", searchPriceLabel:"Prezioa", + searchUrlLabel:"URL", searchCategoryLabel:"Kategoria", + searchStartLabel:"Hasiera ordua", + searchEndLabel:"Amaiera ordua", searchPriorityLabel:"Lehentasuna", searchStatusLabel:"Egoera", statusLabel:"Egoera", + totalVotesLabel:"Bozkak Guztira", noResultsFound:"Emaitzik ez.", votesOption:"Bozkatzeko Aukerak", voteYesLabel:"Bai", voteNoLabel:"Ez", allTypesLabel: "MUGAGABE", + ABSTENTIONLabel:"ABSTENTZIOA", YESLabel:"BAI", NOLabel:"EZ", + FOLLOW_MAJORITYLabel: "JARRAITU GEHIENGOA", + CONFUSEDLabel: "NAHASTUTA", + NOT_INTERESTEDLabel: "INTERESIK EZ", + StatusLabel:"Egoera", votesOptionYesLabel:"Bai", votesOptionNoLabel:"Ez", votesOptionAbstentionLabel:"Abstentzioa", @@ -598,6 +636,7 @@ module.exports = { votesCreatedByLabel:"Noiz", voteStatusOpen:"IREKITA", voteStatusClosed:"ITXITA", + imageSearchLabel: "Hitz hauek dituzten irudiak bilatu.", commentDescription: ({ parentUrl }) => [ " noiz komentatuta ", a({ href: parentUrl }, " haria"), @@ -608,20 +647,53 @@ module.exports = { a({ href: parentUrl }, " bidalketa bat"), ], subtopicTitle: ({ authorName }) => [`Subtopic on @${authorName}'s post`], + mysteryDescription: " bidalketa misteriotsua argitaratu du", oasisDescription: "OASIS Proiektuko Sarea", searchSubmit: "Bilatu...", + postLabel: "BIDALKETAK", + aboutLabel: "BIZILAGUNAK", + feedLabel: "JARIOAK", votesLabel: "GOBERNUA", + reportLabel: "TXOSTENAK", + imageLabel: "IRUDIAK", + videoLabel: "BIDEOAK", + audioLabel: "AUDIOAK", + documentLabel: "DOKUMENTUAK", + torrentLabel: "TORRENTAK", pdfFallbackLabel: "PDF Dokumentua", + eventLabel: "EKITALDIAK", + taskLabel: "ATAZAK", + transferLabel: "TRANSFERENTZIAK", + curriculumLabel: "KURRIKULUMA", + bookmarkLabel: "MARKAGAILUAK", + tribeLabel: "TRIBUAK", + marketLabel: "MERKATUA", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "ZORROTAK", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "Bidali", + subjectLabel: "Gaia", editProfile: "Editatu Abatarra", + profileQrAlt: "Profilaren QR kodea", + profileQrHint: "Eskaneatu Oasis ID hau kopiatzeko.", oasisIdLabel: "OasisID", - spreadContent: "Errepikatu", + spreadLabel: "Hedatu", + spreadHint: "Hedatu zure babesleei (zure feed bidez errepikatzen da).", welcomePmSubject: "Hello World!", - welcomePmBody: "Ongi etorri Oasis-era — sare sozial libre, parekideen arteko, federatu eta zifratua, gonbidapen bidezko arkitektura banatu batekin.\n\nHasteko leku interesgarri batzuk:\n\n- /profile — Editatu zure avatarra, izena, deskribapena eta zein modulu agertuko diren zure alde publikoan.\n- /invites — Gehitu pub bat sarearen gainerakoarekin federatzeko.\n- /peers — Ikusi nor dagoen konektatuta, eskaneatu LAN-a berriz, esportatu edo inportatu peer-zerrendak.\n- /inhabitants — Aurkitu eta bisitatu beste pertsonen avatarrak.\n- /tribes — Sortu edo elkartu muturretik muturrera zifratutako talde pribatuetan.\n- /inbox — Irakurri eta bidali mezu pribatuak.\n- /activity — Ikusi azken jarduera, zurea eta zure sarearena.\n- /publish — Idatzi post bat edo partekatu irudia, audioa, bideoa edo dokumentua.\n- /search — Bilatu sarearen edukia motaren arabera.\n\nKonektatzeko leku interesgarri batzuk:\n\n- /multiverse — Kudeatu zure beste multibertso kontuak, edukia bidaltzea eta jasotzea barne.\n\nMezu hau zugandik zuregana modu pribatuan bidali da; horregatik ez zen konexiorik behar jasotzeko.", + welcomePmBody: "Ongi etorri Oasis-era — sare sozial libre, parekideen arteko, federatu eta zifratua, gonbidapen bidezko arkitektura banatu batekin.\n\nHasteko leku interesgarri batzuk:\n\n- /profile — Editatu zure avatarra, izena, deskribapena eta zein modulu agertuko diren zure alde publikoan.\n- /invites — Gehitu pub bat sarearen gainerakoarekin federatzeko.\n- /peers — Ikusi nor dagoen konektatuta, eskaneatu LAN-a berriz, esportatu edo inportatu peer-zerrendak.\n- /inhabitants — Aurkitu eta bisitatu beste pertsonen avatarrak.\n- /tribes — Sortu edo elkartu muturretik muturrera zifratutako talde pribatuetan.\n- /inbox — Irakurri eta bidali mezu pribatuak.\n- /activity — Ikusi azken jarduera, zurea eta zure sarearena.\n- /publish — Idatzi post bat edo partekatu irudia, audioa, bideoa edo dokumentua.\n\nKonektatzeko leku interesgarri batzuk:\n\n- /fediverse — Kudeatu zure beste fediverso kontuak, edukia bidaltzea eta jasotzea barne.\n\nMezu hau zugandik zuregana modu pribatuan bidali da; horregatik ez zen konexiorik behar jasotzeko.", + profileVisibilityTitle: "Profil publikoaren ikusgaitasuna", + profileVisibilityHint: "Aukeratu zein eremu ikus ditzaketen beste biztanleek zure profilean. Lehenetsia: Karma soilik.", profileSensorsSectionTitle: "Sentsoreak", profileVisibilityActivity: "Jarduera-maila", + profileVisibilityDevice: "Gailua", profileDeviceLockedHint: "Sentsore hau beti dago aktibo: besteei zein gailutatik konektatzen zaren erakusten die eta ezin da desgaitu.", profileVisibilityKarma: "KARMA puntuazioa", profileVisibilityUbi: "UBI", @@ -629,6 +701,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "GPG Gakoa", + profileVisibilityClearnet: "Nire profila argitaratu", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Dendak", profileClearnetJobsLabel: "Lanak", @@ -682,6 +755,7 @@ module.exports = { " edo konexioaren egoera.", ], walletSentToLine: ({ destination, amount }) => `${amount} ECO bidali dira ${destination} helbidera`, + walletSettingsTitle: "Zorroa", walletSettingsDescription: "Integratu Oasis zure ECOin zorroarekin.", walletSettingsDocLink: "ECOin instalazio gida", walletStatusMessages: { @@ -703,19 +777,37 @@ module.exports = { cipherTitle: "Zifratzailea", cipherDescription: "Zifratu eta deszifratu testua simetrikoki (Pasahitza erabiliz).", randomPassword: "Ausazko pasahitza", + cipherEncryptTitle: "Zifratu testua", + cipherEncryptDescription: "Ezarri pasahitza (gutxienez 32 karaktereko luzera) zure testua zifratzeko", + cipherTextLabel: "Zifratuko den testua", cipherTextPlaceholder: "Idatzi zifratuko den testua...", cipherPasswordLabel: "Pasahitza", cipherPasswordDecryptLabel: "Pasahitza ezarri (gutxienez 32 karaktere) testua deszifratzeko", cipherPasswordPlaceholder: "Idatzi pasahitza...", cipherEncryptButton: "Zifratu", + cipherDecryptTitle: "Deszifratu testua", + cipherDecryptDescription: "Sartu zifratutako testua eta pasahitza, deszifratzeko.", cipherEncryptedMessageLabel: "Zifratutako testua", cipherDecryptedMessageLabel: "Deszifratutako Testua", + cipherPasswordUsedLabel: "Zifratzeko pasahitza (gorde!)", cipherEncryptedTextPlaceholder: "Sartu zifratutako testua...", + cipherIvLabel: "HB", + cipherIvPlaceholder: "Sartu hasieratzeko bektorea...", cipherDecryptButton: "Deszifratu", password: "Pasahitza", text: "Testua", encryptedText: "Zifratutako testua", iv: "Hasieratze Bektorea (HB)", + encryptTitle: "Zifratu zure testua", + encryptDescription: "Sartu zifratu nahi duzun testua eta eman pasahitz bat.", + encryptButton: "Zifratu", + decryptTitle: "Deszifratu zure testua", + decryptDescription: "Sartu zifratutako testua eta eman enkripatzeko erabili duzun pasahitz berbera.", + decryptButton: "Deszifratu", + passwordLengthError: "Pasahitzak gutxienez 32 karaketere izan behar ditu.", + missingFieldsError: "Ez da testurik, pasahitzik edo HB-rik sartu.", + encryptionError: "Errorea testua zifratzean.", + decryptionError: "Errorea testua deszifratzean.", bookmarkTitle: "Laster-markak", bookmarkDescription: "Zure sareko laster-markak aurkitu eta kudeatu.", bookmarkAllSectionTitle: "Laster-markak", @@ -741,6 +833,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "Aukerakoa", bookmarkTagsLabel: "Tagak", bookmarkTagsPlaceholder: "Sartu tagak komaz banatuta", + bookmarkCategoryLabel: "Kategoria", + bookmarkCategoryPlaceholder: "Aukerakoa", bookmarkLastVisitLabel: "Azken bisita", bookmarkSearchPlaceholder: "Bilatu URLa, tagak, kategoria, egilea...", bookmarkSortRecent: "Berrienak", @@ -755,6 +849,8 @@ module.exports = { noLastVisit: "Azken bisitarik ez", videoTitle: "Bideoak", videoDescription: "Arakatu eta kudeatu zure sareko bideo-edukia.", + videoPluginTitle: "Izenburua", + videoPluginDescription: "Deskribapena", videoMineSectionTitle: "Zure bideoak", videoCreateSectionTitle: "Bideoa igo", videoUpdateSectionTitle: "Bideoa eguneratu", @@ -786,6 +882,7 @@ module.exports = { videoSortOldest: "Zaharrenak", videoSortTop: "Bozkatuenak", videoSearchButton: "Bilatu", + videoMessageAuthorButton: "MP", videoUpdatedAt: "Eguneratua", videoNoMatch: "Ez dago zure bilaketarekin bat datorren bideorik.", documentTitle: "Dokumentuak", @@ -826,6 +923,8 @@ module.exports = { documentUpdatedAt: "Eguneratua", audioTitle: "Audioak", audioDescription: "Arakatu eta kudeatu zure sareko audio-edukia.", + audioPluginTitle: "Izenburua", + audioPluginDescription: "Deskribapena", audioMineSectionTitle: "Zure audioak", audioCreateSectionTitle: "Audioa igo", audioUpdateSectionTitle: "Audioa eguneratu", @@ -836,6 +935,7 @@ module.exports = { audioFilterMine: "NIREAK", audioFilterRecent: "AZKENAK", audioFilterTop: "TOP", + audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Audioa igo", audioUpdateButton: "Eguneratu", audioDeleteButton: "Ezabatu", @@ -862,7 +962,6 @@ module.exports = { audioFilterFavorites: "GOGOKOAK", favoritesTitle: "Gogokoak", favoritesDescription: "Zure gogoko guztiak leku berean.", - favoritesSearchPlaceholder: "Bilatu gogokoetan...", favoritesFilterAll: "GUZTIAK", favoritesFilterRecent: "AZKENAK", favoritesFilterAudios: "AUDIOAK", @@ -878,13 +977,18 @@ module.exports = { favoritesNoItems: "Oraindik ez dago gogokorik.", yourContacts: "Zure Kontaktuak", allInhabitants: "Bizilagunak", - allCVs: "CVak", + allCVs: "CV guztiak", discoverPeople: "Aurkitu bizilagunak zure sarean.", + allInhabitantsButton: "GUZTIAK", + contactsButton: "LAGUNTZEN", + CVsButton: "CV-ak", + matchSkills: "Aurkitu trebetasunak", + matchSkillsButton: "AURKITU TREBETASUNAK", + suggestedButton: "GOMENDATUTAKOAK", searchInhabitantsPlaceholder: "IRAGAZI bizilagunak IZENAREN ARABERA …", filterLocation: "IRAGAZI bizilagunak KOKAPENAREN ARABERA …", filterLanguage: "IRAGAZI bizilagunak BY HIZKUNTZAREN ARABERA …", filterSkills: "IRAGAZI bizilagunak BY TREBETASUNEN ARABERA …", - cvSkillsCount: "Trebetasunak", applyFilters: "Ezarri Iragazkiak", locationLabel: "Kokapena", languagesLabel: "Hizkuntzak", @@ -893,18 +997,17 @@ module.exports = { mutualFollowers: "Jarraitzaile amankomunean", suggestedFollowsYou: "Zu jarraitzen zaitu", latestInteractions: "Interakzioak Azkenak", + viewAvatar: "Ikusi Abatarra", + viewCV: "Ikusi CV-a", suggestedSectionTitle: "Gomendatuta", topkarmaSectionTitle: "Karma Onena", topecoSectionTitle: "Eko Onena", topactivitySectionTitle: "Top Jarduera", - "TOP INACTIVITYButton": "INAKTIBOENAK", - topinactivitySectionTitle: "Biztanle gutxien aktiboak", blockedSectionTitle: "Blokeatuta", gallerySectionTitle: "GALERIA", - topSectionTitle: "TOP", + blockedButton: "BLOKETATUTA", blockedLabel: "Blokeatutako Erabiltzailea", inhabitantviewDetails: "Ikusi Xehetasunak", - cvVisitButton: "Bisitatu CVa", keepReading: "Irakurtzen jarraitu...", oasisId: "ID-a", noInhabitantsFound: "Bizilagunik ez, oraindik.", @@ -917,9 +1020,8 @@ module.exports = { parliamentFilterCandidatures: "HAUTAGAITZAK", parliamentFilterProposals: "PROPOSAMENAK", parliamentFilterLaws: "LEGEAK", - parliamentFilterRevocations: "EZEZTAPENAK", parliamentFilterHistorical: "HISTORIKOA", - parliamentFilterLeaders: "BURUAK", + parliamentFilterLeaders: "LIDERAK", parliamentFilterRules: "ARAUAK", parliamentGovernmentCard: "Uneko gobernua", parliamentActorInPowerInhabitant: 'BIZTANLEA AGINTEAN', @@ -943,8 +1045,10 @@ module.exports = { parliamentPoliciesDeclined: "BAZTERTUTAKO LEGEAK", parliamentPoliciesDiscarded: "POLITIKA BAZTERTUAK", parliamentEfficiency: "% ERAGINKORTASUNA", + parliamentFilterRevocations: 'INDARGABETZEAK', parliamentRevocationFormTitle: 'INDARGABETU', parliamentRevocationLaw: 'Legea', + parliamentRevocationTitle: 'Izenburua', parliamentRevocationReasons: 'Arrazoiak', parliamentCurrentRevocationsTitle: 'Uneko Indargabetzeak', parliamentFutureRevocationsTitle: 'Etorkizuneko Indargabetzeak', @@ -956,12 +1060,23 @@ module.exports = { parliamentNoGovernments: "Oraindik ez dago gobernurik.", parliamentCandidatureFormTitle: "Kandidatura Proposatu", parliamentCandidatureIdPh: "Oasis ID (@...) edo tribuaren izena", + parliamentCandidatureSlogan: "Leloa (geh. 140 karaktere)", + parliamentCandidatureSloganPh: "Lelo labur bat", parliamentCandidatureMethod: "Metodoa", parliamentCandidatureProposeBtn: "Hautagaitza Proposatu", + parliamentThDate: "Proposamen data", + parliamentThSlogan: "Leloa", + parliamentThSince: "Profila noiztik", + parliamentThVotes: "Jasotako botoak", + parliamentThVoteAction: "Bozkatu", + parliamentTypeUser: "Biztanlea", + parliamentTypeTribe: "Tribu", parliamentVoteBtn: "Bozkatu", parliamentProposalFormTitle: "Legea Proposatu", parliamentProposalDescription: "Deskribapena (≤1000)", parliamentProposalPublish: "Proposamena Argitaratu", + parliamentFinalize: "Amaitu", + parliamentDeadline: "Epea", parliamentNoProposals: "Oraindik ez dago lege-proposamenik.", parliamentNoLaws: "Oraindik ez dago onartutako legerik.", parliamentMethodDEMOCRACY: "Demokrazia", @@ -979,21 +1094,29 @@ module.exports = { parliamentVotesSlashTotal: "Botoak/Guztira", parliamentVotesNeeded: "Beharrezko Botoak", parliamentFutureLawsTitle: "Etorkizuneko legeak", + parliamentNoFutureLaws: "Oraindik ez dago etorkizuneko legerik", parliamentVoteAction: "Bozkatu", + parliamentLeadersTitle: "Liderak", parliamentThLeader: "AVATAR", parliamentPopulation: "Biztanleria", + parliamentThType: "Mota", + parliamentThInPower: "Agintean", parliamentThPresented: "HAUTAGAITZAK", parliamentNoLeaders: "Oraindik ez dago liderrik.", parliamentCandidaturesListTitle: "Kandidaturen Zerrenda", parliamentTimeRemaining: "Geratzen den denbora", + parliamentNoLeader: "Oraindik ez dago irabazten ari den kandidaturarik.", parliamentElectionsStatusTitle: "Hurrengo Gobernua", parliamentProposalDeadlineLabel: "Epemuga", parliamentProposalTimeLeft: "Geratzen den denbora", + parliamentProposalOnTrack: "Onartzeko bidean", + parliamentProposalOffTrack: "Euskarri nahikorik ez", parliamentRulesTitle: "Legebiltzarrak nola funtzionatzen duen", parliamentRulesIntro: "Hauteskundeak 2 hilean behin ebazten dira; hautagaitzak etengabeak dira eta gobernua aukeratzean berrabiarazten dira.", parliamentRulesCandidates: "Edozein biztanlek bere burua, beste biztanle bat edo edozein tribu proposa dezake. Biztanle bakoitzak gehienez 3 hautagaitza proposa ditzake ziklo bakoitzean; ziklo bereko bikoiztuak baztertzen dira.", parliamentRulesElection: "Irabazlea ebazpen unean boto gehien dituen hautagaitza da.", parliamentRulesTies: "Berdinketa haustea: biztanlearen karma handiena; berdin jarraituz gero, profilaren antzinatasun handiena; oraindik berdin, proposamen goiztiarrena; ondoren, IDaren hurrenkera lexikografikoa.", + parliamentRulesFallback: "Inork bozkatzen ez badu, azken proposatutako hautagaitzak irabazten du. Hautagaitzarik ez badago, ausazko tribu bat hautatzen da.", parliamentRulesTerm: "Gobernua fitxak uneko gobernua eta estatistikak erakusten ditu.", parliamentRulesMethods: "Ereduak: Anarkia (gehiengo sinplea), Demokrazia (%50+1), Gehiengoa (%80), Gutxiengoa (%20), Karmatokrazia (karma handiena duten proposamenak), Diktadura (berehalako onarpena).", parliamentRulesAnarchy: "Anarkia modu lehenetsia da: ebazpenean ez bada kandidaturarik hautatzen, Anarkia ezartzen da. Anarkian, edozein biztanlek legeak proposa ditzake.", @@ -1017,8 +1140,12 @@ module.exports = { courtsCaseTitle: "Izenburua", courtsCaseRespondent: "Akusatua / Erantzulea", courtsCaseRespondentPh: "Oasis ID (@...) edo Tribu izena", + courtsCaseMediatorsAccuser: "Bitartekariak (akusazioa)", courtsCaseMediatorsPh: "Oasis IDak, komaz bereizita", courtsCaseMethod: "Ebazpen metodoa", + courtsCaseDescription: "Azalpena (gehienez 1000 karaktere)", + courtsCaseEvidenceTitle: "Kasuko frogak", + courtsCaseEvidenceHelp: "Erantsi zure kasua babesten duten irudiak, audioak, dokumentuak (PDF) edo bideoak.", courtsCaseSubmit: "Kasu bidali", courtsNominateJudge: "Epailea proposatu", courtsJudgeId: "Epailea", @@ -1033,6 +1160,11 @@ module.exports = { courtsAnswerTitle: "Erreklamazioari erantzun", courtsAnswerText: "Erantzun laburra", courtsAnswerSubmit: "Erantzuna bidali", + courtsStanceDENY: "Ukatu", + courtsStanceADMIT: "Onartu", + courtsStancePARTIAL: "Partziala", + courtsStanceCOUNTERCLAIM: "Kontraerreklamazioa", + courtsStanceNEUTRAL: "Neutrala", courtsVerdictTitle: "Ebazpena eman", courtsVerdictResult: "Emaitza", courtsVerdictOrders: "Aginduak", @@ -1041,6 +1173,7 @@ module.exports = { courtsMediationPropose: "Akordioa proposatu", courtsSettlementText: "Baldintzak", courtsSettlementAccepted: "Onartua", + courtsSettlementPending: "Zain", courtsSettlementProposeBtn: "Proposatu", courtsNominationsTitle: "Epailetzako izendapenak", courtsThJudge: "Epailea", @@ -1057,6 +1190,7 @@ module.exports = { courtsThDecisionBy: "Ebazteko epea", courtsThCase: "Kasua", courtsThCreatedAt: "Hasiera-data", + courtsThActions: "Ekintzak", courtsCaseMediatorsRespondentTitle: "Defentsako bitartekariak gehitu", courtsCaseMediatorsRespondent: "Bitartekariak (defentsa)", courtsMediatorsAccuserLabel: "Bitartekariak (akusazioa)", @@ -1097,6 +1231,22 @@ module.exports = { courtsRulesMisconduct: "Jazarpenak, manipulazioak edo faltsututako frogek berehalako ebazpen negatiboa ekar dezakete.", courtsRulesGlossary: "Kasua: gatazkaren erregistroa. Froga: aldarrikapenak babesten dituzten materialak. Epai/ebazpena: neurri edo konponketarekin datorren erabakia. Helegitea: ebazpena berrikusteko eskaera.", courtsFilterActions: "EKINTZAK", + courtsNoActions: "Ez duzu zain dauden ekintzarik zure rolarentzat.", + courtsCaseTitlePlaceholder: "Gatazkaren deskribapen laburra", + courtsCaseSeverity: "Larritasuna", + courtsCaseSeverityNone: "Larritasun etiketarik ez", + courtsCaseSeverityLOW: "Baxua", + courtsCaseSeverityMEDIUM: "Ertaina", + courtsCaseSeverityHIGH: "Handia", + courtsCaseSeverityCRITICAL: "Larria", + courtsCaseSubject: "Gaia", + courtsCaseSubjectNone: "Gairik gabeko etiketa", + courtsCaseSubjectBEHAVIOUR: "Jokabidea", + courtsCaseSubjectCONTENT: "Edukia", + courtsCaseSubjectGOVERNANCE: "Gobernantza / arauak", + courtsCaseSubjectFINANCIAL: "Finantzak / baliabideak", + courtsCaseSubjectOTHER: "Bestelakoa", + courtsHiddenRespondent: "Ezkutatua (rol inplikatuek bakarrik ikus dezakete).", courtsThRole: "Rola", courtsRoleAccuser: "Akusatzailea", courtsRoleDefence: "Defentsa", @@ -1107,19 +1257,51 @@ module.exports = { courtsAssignJudgeBtn: "Epailea aukeratu", trendingTitle: "Pil-pilean", exploreTrending: "Aurkitu pil-pileko edukia zure sarean.", + bookmarkButton: "MARKAGAILUAK", + transferButton: "TRANSFERENTZIAK", + eventButton: "EKITALDIAK", + taskButton: "ATAZAK", votesButton: "GOBERNUA", - shopProductButton: "DENDA", + reportButton: "TXOSTENAK", + feedButton: "JARIOA", + marketButton: "MERKATUA", + imageButton: "IRUDIAK", + audioButton: "AUDIOAK", + videoButton: "BIDEOAK", + documentButton: "DOKUMENTUAK", + torrentButton: "TORRENTAK", + noTrendingFound: "Pil-pileko edukirik ez.", + noContentMessage: "Pil-pileko edukirik ez, oraindik.", + trendingDescription: "Deskribapena", + trendingDate: "Data", + trendingLocation: "Kokapena", + trendingPrice: "Prezioa", + trendingUrl: "URL", + trendingCategory: "Kategoria", + trendingStart: "Hasi", + trendingEnd: "Amaitu", + trendingPriority: "Lehentasuna", + trendingStatus: "Egoera", + trendingFrom: "Nork", + trendingTo: "Nori", + trendingConcept: "Kontzeptua", + trendingAmount: "Kopurua", + trendingDeadline: "Epemuga", + trendingItemStatus: "Elementuaren Egoera", + trendingTotalVotes: "Bozkak Guztira", trendingTotalOpinions: "Iritzial Guztira", - trendingNoContentMessage: "Oraindik ez dago joerarik.", + trendingNoContentMessage: "Edukirik ez.", + trendingAuthor: "Nork", + trendingCreatedAtLabel: "Noiz", trendingTotalCount: "Kopurua Guztira", tasksTitle: "Atazak", tasksDescription: "Aurkitu eta kudeatu atazak", - taskSearchPlaceholder: "Bilatu zereginak...", taskTitleLabel: "Izenburua", taskDescriptionLabel: "Deskribapena", taskStartTimeLabel: "Hasiera Data", taskEndTimeLabel: "Amaiera Data", taskPriorityLabel: "Lehentasuna", + taskPrioritySelect: "Aukeratu lehentasuna", taskPriorityUrgent: "Larria", taskPriorityHigh: "Altua", taskPriorityMedium: "Ertaina", @@ -1129,13 +1311,13 @@ module.exports = { taskVisibilityLabel: "Ikusgaitasuna", taskPublic: "publikoa", taskPrivate: "pribatua", + taskCreatedAt: "Noiz", + taskBy: "Nork", taskStatus: "Egoera", taskStatusOpen: "Irekita", taskStatusInProgress: "Ekinean", taskStatusClosed: "Itxita", taskAssignedTo: "Honi esleituta", - taskAssignedChip: "Esleituta", - taskUnassignedChip: "Esleitu gabe", taskAssignees: "Hauei esleituta", taskAssignButton: "Esleitu neuri", taskUnassignButton: "Kendu esleipena", @@ -1148,6 +1330,7 @@ module.exports = { taskFilterInProgress: "EKINEAN", taskFilterClosed: "ITXITA", taskFilterAssigned: "ESLEITUTA", + taskFilterArchived: "ARTXIBATUTA", taskFilterUrgent: "LARRIA", taskFilterHigh: "ALTUA", taskFilterMedium: "ERTAINA", @@ -1160,21 +1343,23 @@ module.exports = { taskInProgressTitle: "Atazak Ekinean", taskClosedTitle: "Atazak itxita", taskAssignedTitle: "Atazak esleituta", + taskArchivedTitle: "Atazak artxibatuta", + taskPublicTitle: "Ataza publikoak", + taskPrivateTitle: "Ataza pribatuak", notasks: "Atazarik ez.", noLocation: "Ez da kokapenik zehaztu", taskSetStatus: "Egoera ezarri", + eventTitle: "Ekitaldiak", eventDateLabel: "Data", - eventRecurrenceLabel: "Errepikapena", - eventRecurrenceUntil: "Errepikatu arte", - eventRecurrenceHint: "Utzi hutsik amaiera data ekitaldi bakarrerako.", - eventUpcomingDates: "Hurrengo datak", eventsTitle: "Ekitaldiak", eventsDescription: "Aurkitu eta kudeatu ekitaldiak zure sarean.", - eventSearchPlaceholder: "Bilatu ekitaldiak...", + eventDescription: "Deskribapena", eventPrice: "Prezioa", eventStatus: "Egoera", + eventOrganizer: "Antolatzailea", eventAllSectionTitle: "Ekitaldiak", eventMineSectionTitle: "Zeure Ekitaldiak", + eventArchivedTitle: "Artxibatutako Ekitaldiak", eventCreateSectionTitle: "Sortu Ekitaldia", eventUpdateSectionTitle: "Eguneratu Ekitaldia", eventDeleteButton: "Ezabatu", @@ -1182,26 +1367,11 @@ module.exports = { eventAddToCalendar: "Egutegira gehitu", eventVisitCalendar: "Egutegia ikusi", viewTask: "Ikusi ataza", - viewEvent: "Ikusi ekitaldia", - viewPad: "Ikusi pad-a", - viewMap: "Ikusi mapa", - viewCalendar: "Ikusi egutegia", viewReport: "Ikusi txostena", viewTransfer: "Ikusi transferentzia", viewItem: "Ikusi elementua", viewShop: "Ikusi denda", viewProject: "Ikusi proiektua", - viewJob: "Ikusi Lanpostua", - viewPlace: "Ikusi Lekua", - generatePdf: "Sortu PDFa", - sharePm: "Partekatu PMz", - galleryImages: "Irudiak (geh. 50MB bakoitza)", - galleryPhotos: "Irudiak", - galleryAddPhoto: "Gehitu Irudia", - galleryRemovePhoto: "Kendu", - galleryVideo: "Bideoa (geh. 50MB)", - galleryAddVideo: "Gehitu Bideoa", - galleryRemoveVideo: "Kendu bideoa", viewVotation: "Ikusi bozketa", voteCastTitle: "Eman botoa", voteResults: "Emaitzak", @@ -1209,15 +1379,29 @@ module.exports = { eventDescriptionLabel: "Deskribapena", eventDescriptionPlaceholder: "Sartu ekitaldiaren deskribapena...", eventUpdateButton: "Eguneratu", + eventAttendeesLabel: "Partaideak", + eventAttendeesPlaceholder: "Sartu partaideak, erabili komak bereizteko.", eventTagsLabel: "Etiketak", + eventTagsPlaceholder: "Sartu ekitaldiaren etiketak, erabili komak bereizteko.", + eventTags: "Etiketak", eventPriceLabel: "Prezioa", eventUrlLabel: "URL", eventAttendees: "Partaideak", + noAttendees: "Partaiderik ez oraindik", + eventCreatedAt: "Noiz sortua", eventLocation: "Kokapena", eventLocationLabel: "Kokapena", + eventNoLocation: "Ez da zehaztu kokapenik", + eventNoURL: "Ez da zehaztu URL-rik", + eventBy: "Nork", noevents: "Ekitaldirik ez.", eventDate: "Data", + eventDateFormat: "YYYY/MM/DD HH:mm", eventAttendButton: "Joan Ekitaldira", + eventUnattendButton: "Joateari utzi", + eventCreatedBy: "Nork", + eventAttendeesCount: "Partaide kopurua", + eventCreatedByYou: "Zeuk sortu duzu ekitaldia", eventFilterAll: "GUZTIAK", eventFilterMine: "NEUREAK", eventFilterToday: "GAUR", @@ -1225,9 +1409,18 @@ module.exports = { eventFilterMonth: "HILEON", eventFilterYear: "AURTEN", eventFilterArchived: "ARTXIBATUTA", + eventTodayTitle: "Ekitaldiak gaur", + eventThisWeekTitle: "Ekitaldiak asteon", + eventThisMonthTitle: "Ekitaldiak hileon", + eventThisYearTitle: "Ekitaldiak aurten", eventPrivacyLabel: "Ikusgaitasuna", eventPublic: "Publikoa", eventPrivate: "Pribatua", + eventPublicTitle: "Ekitaldi Publikoak", + eventNoPrice: "Ekitaldia doan", + eventNoImage: "Ez da irudirik igo", + eventAttendConfirmation: "Ekitaldi honetara zoaz", + eventUnattendConfirmation: "Ez zara ekitaldi honetara joango", eventAttended: "Bertaratzen naiz", eventUnattended: "Ez naiz bertaratzen", eventStatusOpen: "Irekia", @@ -1238,14 +1431,9 @@ module.exports = { tagsTopSectionTitle: "Etiketa Gorenak", tagsCloudSectionTitle: "Etiketen hodeia", tagsFilterAll: "GUZTIAK", - tagsFilterMine: "NIREAK", - tagsFilterRecent: "BERRIAK", - tagsMineSectionTitle: "Nire etiketak", - tagsRecentSectionTitle: "Etiketa berriak", tagsFilterTop: "GORENAK", tagsFilterCloud: "HODEIA", tagsNoItems: "Etiketarik ez.", - tagsSearchPlaceholder: "Bilatu etiketak...", tagsTableHeaderTag: "ETIKETA/LOTURA:", tagsTableHeaderCount: "KONTAGAILUA:", transfersTitle: "Transferentziak", @@ -1285,12 +1473,18 @@ module.exports = { transfersDeadline: "Epemuga", transfersTags: "Etiketak", transfersStatus: "Egoera", + transfersStatusUnconfirmed: "BERRETSI GABE", + transfersStatusClosed: "ITXITA", + transfersStatusDiscarded: "BAZTERTUTA", + transfersCreatedAt: "Noiz", transfersConfirmations: "BERRESPENAK", - transfersExportContract: "Sortu Faktura", + transfersContainingBlock: "Bloke edukitzailea", + transfersExportContract: "Sortu kontratua", transfersConfirmButton: "Berretsi Transferentzia", transfersNoItems: "Transferentziarik ez.", transfersMineSectionTitle: "Zeure Transferentziak", transfersUBISectionTitle: "RBU Transferentziak", + transfersMarketSectionTitle: "Merkatuko Transferentziak", transfersTopSectionTitle: "Transferentzia Gorenak", transfersPendingSectionTitle: "Transferentziak Zain", transfersUnconfirmedSectionTitle: "Transferentziak Berretsi Gabe", @@ -1298,13 +1492,21 @@ module.exports = { transfersDiscardedSectionTitle: "Transferentziak Baztertuta", transfersCreateSectionTitle: "Sortu transferentzia", transfersAllSectionTitle: "Transferentziak", + transfersFilterFavs: "Gustukoak", + transfersFavsSectionTitle: "Gustuko transferentziak", + transfersSearchLabel: "Bilatu", transfersSearchPlaceholder: "Bilatu kontzeptua, etiketak, erabiltzaileak...", transfersMinAmountLabel: "Gutx. zenbatekoa", transfersMaxAmountLabel: "Geh. zenbatekoa", + transfersSortLabel: "Ordenatu", transfersSortRecent: "Berrienak", transfersSortAmount: "Zenbateko handiena", transfersSortDeadline: "Epe hurbilena", transfersSearchButton: "Bilatu", + transfersFavoriteButton: "Gustukoa", + transfersUnfavoriteButton: "Kendu gustukoa", + transfersMessageUserButton: "Mezua", + transfersExpiringSoonBadge: "LASTER", transfersExpiredBadge: "IRAUNGITA", transfersUpdatedAt: "Eguneratua", transfersNoMatch: "Ez dago bilaketarekin bat datorren transferentziarik.", @@ -1317,9 +1519,6 @@ module.exports = { voteClosedTitle: "Bozketak Itxita", voteAllSectionTitle: "Bozketak", voteCreateButton: "Sortu Bozketa", - voteErrorTitle: "Egiaztatu inprimakia", - voteErrorDeadlineMin: "Epemuga gutxienez {days} egunetara egon behar da.", - voteErrorGeneric: "Ezin izan da bozketa gorde.", voteUpdateButton: "Eguneratu", voteDeleteButton: "Ezabatu", voteOptionYes: "BAI", @@ -1349,6 +1548,16 @@ module.exports = { voteNoQuestion: "Galderarik ez", voteUnknownCreator: "Sortzaile Ezezaguna", voteUnknownDate: "Data Ezezaguna", + errorVoteNotFound: "Bozka ez da aurkitu", + errorAlreadyVoted: "Jadanik bozkatu duzu", + errorVoteClosedCannotEdit: "Ezin duzu itxitako bozketa bat editatu", + errorVoteDeadlinePassed: "Bozketa honen epemuga igaro da", + errorRetrievingVote: "Errorea bozka eskuratzerakoan", + errorCreatingVote: "Errorea bozka sortzerakoan", + errorVoteAlreadyVoted: "Ezin duzu bozka editatu jadanik bozkatu baduzu", + errorDeletingOldVote: "Errorea bozka zaharra ezabatzerakoan", + errorCreatingUpdatedVote: "Errorea eguneratutako bozka sortzean", + errorCreatingTombstone: "Errorea hilarria sortzerakoan", voteDetailSectionTitle: 'Bozketa xehetasunak', voteCommentsLabel: 'Iruzkinak', voteCommentsForumButton: 'Eztabaida ireki', @@ -1358,6 +1567,7 @@ module.exports = { voteNewCommentButton: 'Bidali iruzkina', voteNewCommentLabel: 'Gehitu iruzkina', cvTitle: "CV", + cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Editatu CV-a", cvCreateSectionTitle: "Sortu CV-a", cvDescription: "Kudeatu eta partekatu zure trebetasun profesionalak eta informazio gehiago.", @@ -1376,121 +1586,29 @@ module.exports = { cvLocationLabel: "Kokapena", cvStatusLabel: "Egoera", cvPreferencesLabel: "Lehentasunak", + cvOasisContributorLabel: "Oasis-eko Parte-Hartzailea", cvPersonal: "Pertsonala", cvOasis: "Oasis-eko Parte-Hartzailea (optional)", - cvOasisContributorView: "Ekarpena", + cvOasisContributorView: "Oasis-i Ekarpena", cvEducational: "Hezkuntza (hautazkoa)", cvEducationalView: "Hezkuntza", cvProfessional: "Profesionala (hautazkoa)", cvProfessionalView: "Profesionala", cvAvailability: "Eskuragarritasuna (hautazkoa)", + cvAvailabilityView: "Eskuragarritasuna", cvUpdateButton: "Eguneratu", cvCreateButton: "Sortu CV-a", + cvContactLabel: "Kontaktua", cvCreatedAt: "Noiz sortua", cvUpdatedAt: "Noiz eguneratua", cvEditButton: "Eguneratu", cvDeleteButton: "Ezabatu", cvNoCV: "CV-rik ez.", blogSubject: "Gaia", - blogMessage: "Mezua", + blogMessage: "Edukia 8096 karakteretara mugatuta.", blogImage: "Multimedia igo (max: 50MB)", - blogMedia: "Eranskinak (8 irudi eta bideo bat gehienez)", blogPublish: "Aurrebista", - pollsTitle: "Inkestak", - dataTitle: "Bat etortzeak", - dataDescription: "Arakatu eta ikusi zure sareko bat etortzeak.", - dataFilterAll: "DENAK", - dataFilterMine: "NIREAK", - dataFilterRecent: "BERRIENAK", - dataFilterTop: "TOP", - dataKindInhabitants: "BIZTANLEAK", - dataKindJobs: "LANPOSTUAK", - dataKindProjects: "PROIEKTUAK", - dataKindEvents: "EKITALDIAK", - dataKindTribes: "TRIBUAK", - dataKindMarket: "MERKATUA", - dataKindHousing: "ETXEBIZITZA", - dataKindIndustry: "INDUSTRIA", - dataKindTasks: "ZEREGINAK", - dataKindReports: "TXOSTENAK", - dataKindVotes: "BOZKETAK", - dataSearchPlaceholder: "Bilatu datu gurutzatuetan...", - dataCohesionTitle: "Kohesio Koefizientea (CC)", - dataCcShort: "CC", - dataCohesionHint: "Sareak argitaratutako guztiaren arteko batez besteko afinitatea.", - dataAffinity: "Afinitatea", - dataCommonTerms: "Gako-hitza", - dataStatPeople: "CVak", - dataStatConnected: "Konektatuta", - dataStatSkills: "Trebetasunak", - dataBestMatch: "Match onena", - dataStatIsolated: "Isolatuta", - dataStatEntities: "Konparatutako entitateak", - dataStatTerms: "Termino desberdinak", - dataStatPairs: "Konektatutako bikoteak", - dataMatchesTitle: "Bat etortzeak", - dataMatchesHint: "Algoritmoaren iradokizun pertsonalizatuak.", - dataTermsTitle: "Gako-hitzak", - dataTermsHint: "Eduki gehienetan agertzen diren terminoak.", - dataConnections: "Lotutako bat-etortzeak", - dataTopicTitle: "Honekin lotutako guztia:", - dataTopicHint: "Algoritmoak gai honekin lotzen duena", - dataNoMatches: "Ez dago datu gurutzaturik erakusteko.", - dataNoProfile: "Argitaratu edukia edo idatzi zure CVa, algoritmoak zure interesak ikas ditzan.", - pollsDescription: "Sareari galdetzeko eta erantzunak zenbatzeko modulua.", - pollFilterAll: "GUZTIAK", - pollFilterMine: "NIREAK", - pollFilterRecent: "BERRIENAK", - pollFilterTop: "TOP", - pollFilterVoted: "BOZKATUAK", - pollFilterOpen: "IREKIAK", - pollFilterClosed: "ITXIAK", - pollCreateButton: "Sortu Inkesta", - pollCreateTitle: "Sortu Inkesta", - pollEditTitle: "Editatu Inkesta", - pollQuestion: "Galdera", - pollQuestionPlaceholder: "Zer galdetu nahi duzu?", - pollOptions: "Aukerak (bat lerroko)", - pollOutcome: "Emaitza", - pollNoVotesYet: "Oraindik botorik ez", - pollTie: "Berdinketa", - pollOptionsPlaceholder: "Plaza\nParkea\nTaberna", - pollAnonymous: "Anonimoa", - pollAnonymousLabel: "Inkesta anonimoa", - pollMultiple: "Aukera anitza", - pollMultipleLabel: "Baimendu erantzun bat baino gehiago", - pollDeadline: "Epemuga", - pollTags: "Etiketak", - pollPublishButton: "Argitaratu Inkesta", - pollUpdateButton: "Eguneratu", - pollCloseButton: "Itxi", - pollDeleteButton: "Ezabatu", - pollVoteButton: "Bozkatu", - pollChangeVote: "Aldatu Botoa", - pollVoters: "Bozkatzaileak", - pollComments: "Iruzkinak", - pollStatusLabel: "Egoera", - pollStatusOpen: "IREKIA", - pollStatusClosed: "ITXIA", - pollSearchPlaceholder: "Bilatu inkestak...", - pollsNoItems: "Ez da inkestarik aurkitu.", - viewPoll: "Ikusi Inkesta", - pollChatCreate: "Sortu Inkesta", - pollInChat: "Inkesta", - blogTitle: "Blogak", - blogDescription: "Blogak aurkitzeko eta kudeatzeko modulua.", - blogFilterAll: "GUZTIAK", - blogFilterMine: "NIREAK", - blogFilterRecent: "BERRIENAK", - blogFilterFavorites: "GOGOKOAK", - blogFilterTop: "TOP", - blogCreateButton: "Sortu Bloga", - blogSearchPlaceholder: "Bilatu blogak...", - blogComments: "Iruzkinak", - blogAllowComments: "Baimendu iruzkinak", - blogCommentsClosed: "Egileak blog honen iruzkinak itxi ditu.", - blogNoItems: "Ez da blogik aurkitu.", - viewBlog: "Ikusi Bloga", + noPopularMessages: "Pil-pileko mezurike ez, oraindik", forumTitle: "Foroak", forumCategoryLabel: "Kategoria", forumTitleLabel: "Izenburua", @@ -1498,22 +1616,43 @@ module.exports = { forumCreateButton: "Sortu foroa", forumCreateSectionTitle: "Sortu foroa", forumDescription: "Hitz egin sareko beste erabiltzaileekin libreki.", - forumSearchPlaceholder: "Bilatu foroak...", forumFilterAll: "GUZTIAK", forumFilterMine: "NIREAK", forumFilterRecent: "BERRIENAK", forumFilterTop: "GORENAK", + forumMineSectionTitle: "Zure Foroak", + forumRecentSectionTitle: "Azken Foroak", + forumAllSectionTitle: "Foroak", forumDeleteButton: "Ezabatu", forumParticipants: "parte-hartzaileak", forumMessages: "mezuak", + forumLastMessage: "Azken mezua", forumMessageLabel: "Mezua", forumMessagePlaceholder: "Idatzi zure mezua...", forumSendButton: "Bidali", + forumVisitForum: "Bisitatu Foroaren", noForums: "Ez da fororik aurkitu.", - forumVisitButton: "Ikusi Foroa", - forumTopicLabel: "Gaia", + forumVisitButton: "Foroa bisitatu", + forumCatGENERAL: "Orokorra", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", + forumCatPOLITICS: "Politika", + forumCatTECH: "Teknologia", + forumCatSCIENCE: "Zientzia", + forumCatMUSIC: "Musika", + forumCatART: "Artea", + forumCatGAMING: "Bideojokoak", + forumCatBOOKS: "Liburuak", + forumCatFILMS: "Filmak", + forumCatPHILOSOPHY: "Filosofia", + forumCatSOCIETY: "Gizartea", + forumCatPRIVACY: "Pribatutasuna", + forumCatCYBERWARFARE: "Zibergerra", + forumCatSURVIVALISM: "Biziraupena", imageTitle: "Irudiak", imageDescription: "Arakatu eta kudeatu zure sareko irudi-edukia.", + imagePluginTitle: "Izenburua", + imagePluginDescription: "Deskribapena", imageMineSectionTitle: "Zure irudiak", imageCreateSectionTitle: "Irudia igo", imageUpdateSectionTitle: "Irudia eguneratu", @@ -1522,12 +1661,14 @@ module.exports = { imageTopSectionTitle: "Bozkatuenak", imageFavoritesSectionTitle: "Gogokoak", imageGallerySectionTitle: "Galeria", + imageMemeSectionTitle: "Memak", imageFilterAll: "GUZTIAK", imageFilterMine: "NIREAK", imageFilterRecent: "AZKENAK", imageFilterTop: "TOP", imageFilterFavorites: "GOGOKOAK", imageFilterGallery: "GALERIA", + imageFilterMeme: "MEMAK", imageCreateButton: "Irudia igo", imageUpdateButton: "Eguneratu", imageDeleteButton: "Ezabatu", @@ -1540,6 +1681,7 @@ module.exports = { imageTitlePlaceholder: "Aukerakoa", imageDescriptionLabel: "Deskribapena", imageDescriptionPlaceholder: "Aukerakoa", + imageMemeLabel: "¿MEME?", imageNoFile: "Ez da irudi-fitxategirik eman", noImages: "Ez dago irudirik erabilgarri.", imageSearchPlaceholder: "Bilatu izenburua, etiketak, deskribapena, egilea...", @@ -1547,6 +1689,7 @@ module.exports = { imageSortOldest: "Zaharrenak", imageSortTop: "Bozkatuenak", imageSearchButton: "Bilatu", + imageMessageAuthorButton: "Mezua", imageUpdatedAt: "Eguneratua", imageNoMatch: "Ez dago zure bilaketarekin bat datorren irudirik.", feedTitle: "Jarioa", @@ -1558,6 +1701,7 @@ module.exports = { createFeedButton: "Bidali Jarioa!", feedPlaceholder: "Zer berri? (max 280 characters)", TODAYButton: "GAUR", + CREATEButton: "Sortu Jarioa", totalOpinions: "Iritziak Guztira", moreVoted: "Gehien bozkatu", noFeedsFound: "Ez da jariorik aurkitu.", @@ -1579,12 +1723,20 @@ module.exports = { concept: "Kontzeptua", description: "Deskribapena", meme: "Meme", + activityContact: "Kontaktua", + activityBy: "Izena", + activityPixelia: "Pixel berria gehituta", + viewImage: "Ikusi irudia", + playAudio: "Audioa erreproduzitu", + playVideo: "Bideoa erreproduzitu", typeRecent: "AZKENA", - typeTop: "TOP", + errorActivity: "Errorea jarduera eskuratzean", typePost: "BLOGAK", + recentButton: "AZKENAK", typeTribe: "TRIBUAK", typeLarp: "L.A.R.P.", typeInhabitants: "BIZTANLEAK", + activityProfileUpdated: "bere profila eguneratu du", typeAbout: "BIZTANLEAK", typeCurriculum: "CVAK", typeImage: "IRUDIAK", @@ -1628,6 +1780,8 @@ module.exports = { typeCourtsSettlementAccepted: "Epaitegiak · Akordio onartua", typeCourtsNomination: "Epaitegiak · Izendapena", typeCourtsNominationVote: "Epaitegiak · Izendapen bozketak", + activitySupport: "Aliantza berria sortua", + activityJoin: "PUB berria batu da", question: "Galdera", deadline: "Epemuga", status: "Egoera", @@ -1641,8 +1795,12 @@ module.exports = { date: "Data", category: "Kategoria", attendees: "Parte-hartzaileak", + activitySpread: "->", + visitLink: "Esteka bisitatu", + viewDocument: "Dokumentua ikusi", location: "Kokalekua", contentWarning: "Gaia", + personName: "Biztanlearen izena", bankWalletConnected: "ECOin zorroa", bankUbiReceived: "Jasotako UBI", bankTx: "Tx", @@ -1651,6 +1809,7 @@ module.exports = { link: "Esteka", activityProjectFollow: "%OASIS% orain %ACTION% proiektu hau %PROJECT%", activityProjectUnfollow: "%OASIS% orain %ACTION% proiektu hau %PROJECT%", + activityProjectPledged: "%OASIS%-(e)k %ACTION% %AMOUNT% egin du %PROJECT% proiektuan", following: "JARRAITZEN", unfollowing: "EZ JARRAITZEN", pledged: "KONPROMISOA", @@ -1677,11 +1836,8 @@ module.exports = { parliamentLawVotes: "Botoak", createdAt: "Sortze-data", reportsTitle: "Txostenak", - reportsDescription: "Kudeatu eta jarraitu zure sareko txostenak.", - reportsSearchPlaceholder: "Bilatu txostenak...", + reportsDescription: "Kudeatu eta jarraitu arazo, akats, gehiegikeri eta eduki-abisuei buruzko txostena zure sarean.", reportsFilterAll: "GUZTIAK", - reportsFilterRecent: "BERRIAK", - reportsFilterTop: "TOP", reportsFilterMine: "NEUREAK", reportsFilterFeatures: "GAITASUNAK", reportsFilterBugs: "BUGAK", @@ -1694,13 +1850,18 @@ module.exports = { reportsFilterInvalid: "BALIOGABEKOA", reportsCreateButton: "Sortu Txostena", reportsTitleLabel: "Izenburua", + reportsDescriptionLabel: "Deskribapena", + reportsDescriptionPlaceholder: "Idatzi deskribapen zehatza, mesedez.", reportsCategory: "Kategoria", + reportsCategoryLabel: "Kategoria", reportsCategoryFeatures: "Gaitasunak", reportsCategoryBugs: "Bugak", reportsCategoryAbuse: "Gehiegikeria", - reportsCategoryContent: "Edukia", + reportsCategoryContent: "Arazoak Edukiarekin", reportsUpdateButton: "Eguneratu", reportsDeleteButton: "Ezabatu", + reportsDateLabel: "Data", + reportsUploadFile: "Multimedia igo (max: 50MB)", reportsMineSectionTitle: "Zeure Txostenak", reportsFeaturesSectionTitle: "Gaitasuna Eskatu", reportsBugsSectionTitle: "Bugak", @@ -1708,6 +1869,11 @@ module.exports = { reportsContentSectionTitle: "Arazoak Edukiarekin", reportsAllSectionTitle: "Txostenak", reportsNoItems: "Txostenik ez.", + reportsValidationTitle: "Sartu izenburu zuzena, mesedez.", + reportsValidationDescription: "Deskribapena ezin da hutsi egon.", + reportsValidationCategory: "Aukeratu kategoria, mesedez.", + reportsCreatedAt: "Noiz", + reportsCreatedBy: "Nork", reportsSeverity: "Larritasuna", reportsSeverityLow: "Baxua", reportsSeverityMedium: "Ertaina", @@ -1718,10 +1884,13 @@ module.exports = { reportsStatusUnderReview: "Berrikusten", reportsStatusResolved: "Zuzenduta", reportsStatusInvalid: "Balio gabekoa", + reportsUpdateStatusButton: "Eguneratu Egoera", + reportsAnonymityOption: "Bidali Anonimoki", + reportsAnonymousAuthor: "Anonimoa", reportsConfirmButton: "BERRETSI TXOSTENA!", reportsConfirmations: "Berrespenak", reportsConfirmedSectionTitle: "Berretsitako Txostenak", - reportsCreateTaskButton: "Sortu Ataza", + reportsCreateTaskButton: "SORTU ATAZA", reportsOpenSectionTitle: "Ireki Txostenak", reportsUnderReviewSectionTitle: "Txostenak Berrikuspenean", reportsResolvedSectionTitle: "Zuzendutako Txostenak", @@ -1764,20 +1933,6 @@ module.exports = { reportsRequestedActionLabel: 'Eskatutako ekintza', reportsRequestedActionPlaceholder: 'Kendu, ezkutatu, etiketatu, ohartarazi, etab.', tribesTitle: "Tribuak", - tribeFilterAll: "GUZTIAK", - tribeFilterRecent: "BERRIAK", - tribeFilterMine: "NIREAK", - tribeFilterMembership: "KIDETZA", - tribeFilterSubtribes: "AZPITRIBUAK", - tribeFilterTop: "TOP", - tribeFilterGallery: "GALERIA", - tribeFeedFilterTOP: "TOP", - tribeFeedFilterMINE: "NIREAK", - tribeFeedFilterALL: "GUZTIAK", - tribeFeedFilterRECENT: "BERRIAK", - courtsStanceDENY: "Ukatu", - courtsStanceADMIT: "Onartu", - courtsStancePARTIAL: "Neurri batean onartu", tribeAllSectionTitle: "Tribuak", tribeMineSectionTitle: "Zeure Tribuak", tribeCreateSectionTitle: "Sortu Tribua", @@ -1789,6 +1944,13 @@ module.exports = { tribeviewSubTribeButton: "Azpi-Tribua Bisitatu", tribeRootLabel: "ERROA", tribeDescription: "Aurkitu edo sortu tribuak zure sarean.", + tribeFilterAll: "GUZTIAK", + tribeFilterMine: "NEUREAK", + tribeFilterMembership: "BAZKIDETZA", + tribeFilterRecent: "BERRIAK", + tribeFilterTop: "TOP", + tribeFilterSubtribes: "AZPI-TRIBUAK", + tribeFilterGallery: "GALERIA", tribeMainTribeLabel: "TRIBU NAGUSIA", tribeCreateButton: "Sortu Tribua", tribeUpdateButton: "Egutneratu", @@ -1807,8 +1969,13 @@ module.exports = { tribeModeLabel: "Gonbidapen MODUA", tribeIsAnonymousLabel: "Publikoa?", tribeMembersCount: "Partaideak", + tribeInviteCodePlaceholder: "Sartu gonbidapen kodea", + tribeJoinByCodeButton: "Sartu kodearekin", + tribeJoinButton: "SARTU", tribeEnterInvite: "SARTU GONBIDAPENA", tribeLeaveButton: "ATERA", + tribeYes: "BAI", + tribeNo: "EZ", tribePublic: "PULIKOA", tribePrivate: "PRIBATUA", tribeGenerateInvite: "GONBIDAPEN BAKARRA", @@ -1817,8 +1984,13 @@ module.exports = { tribeRemoveInvitation: "KENDU GONBIDAPENA", tribeCreatedAt: "Noiz", tribeAuthor: "Nork", + tribeAuthorLabel: "EGILEA", tribeStrict: "Zorrotza", tribeOpen: "Irekita", + tribeFeedFilterRECENT: "BERRIAK", + tribeFeedFilterMINE: "NEUREAK", + tribeFeedFilterALL: "GUZTIAK", + tribeFeedFilterTOP: "GORENAK", tribeFeedRefeeds: "BERJARIOAK", tribeFeedRefeed: "Berjariotu", tribeFeedMessagePlaceholder: "Idatzi jarioa…", @@ -1828,12 +2000,17 @@ module.exports = { tribeNotFound: "Tribua ez da aurkitu!", createTribeTitle: "Sortu Tribua", updateTribeTitle: "Eguneratu Tribua", + tribeSectionOverview: "Ikuspegi orokorra", tribeSectionInhabitants: "Biztanleak", tribeSectionVotations: "BOZKATZEAK", tribeSectionEvents: "EKITALDIAK", + tribeSectionReports: "Txostenak", tribeSectionTasks: "ATAZAK", tribeSectionFeed: "JARIOA", tribeSectionForum: "FOROA", + tribeSectionMarket: "Merkatua", + tribeSectionJobs: "Lanak", + tribeSectionProjects: "Proiektuak", tribeSectionMedia: "Media", tribeSectionImages: "IRUDIAK", tribeSectionAudios: "AUDIOAK", @@ -1871,6 +2048,11 @@ module.exports = { tribeTaskStatusClosed: "ITXI", tribeTaskAssign: "ESLEITU", tribeTaskUnassign: "KENDU", + tribeReportCreate: "Sortu Txostena", + tribeReportsEmpty: "Txostenik ez, oraindik.", + tribeReportTitle: "Izenburua", + tribeReportDescription: "Deskribapena", + tribeReportCategory: "Kategoria", tribeVotationCreate: "Sortu Bozketa", tribeVotationsEmpty: "Bozketarik ez, oraindik.", tribeVotationTitle: "Izenburua", @@ -1888,6 +2070,27 @@ module.exports = { tribeForumCategory: "Kategoria", tribeForumReply: "Erantzun", tribeForumReplies: "Erantzunak", + tribeMarketCreate: "Sortu Iragarkia", + tribeMarketEmpty: "Iragarkirik ez, oraindik.", + tribeMarketTitle: "Izenburua", + tribeMarketDescription: "Deskribapena", + tribeMarketPrice: "Prezioa", + tribeMarketImage: "Irudia", + tribeMarketCategory: "Kategoria", + tribeJobCreate: "Sortu Lana", + tribeJobsEmpty: "Lanik ez, oraindik.", + tribeJobTitle: "Izenburua", + tribeJobDescription: "Deskribapena", + tribeJobLocation: "Kokalekua", + tribeJobSalary: "Soldata", + tribeJobDeadline: "Epemuga", + tribeProjectCreate: "Sortu Proiektua", + tribeProjectsEmpty: "Proiekturik ez, oraindik.", + tribeProjectTitle: "Izenburua", + tribeProjectDescription: "Deskribapena", + tribeProjectGoal: "Helburua", + tribeProjectFunded: "Finantzatua", + tribeProjectDeadline: "Epemuga", tribeMediaUpload: "Igo Media", readDocument: "Dokumentua Irakurri", tribeCreateImage: "Irudia Sortu", @@ -1898,6 +2101,7 @@ module.exports = { tribeMediaEmpty: "Mediarik ez, oraindik.", tribeMediaTitle: "Izenburua", tribeMediaDescription: "Deskribapena", + tribeMediaType: "Mota", tribeMediaTypeImage: "Irudia", tribeMediaTypeVideo: "Bideoa", tribeMediaTypeAudio: "Audioa", @@ -1907,6 +2111,11 @@ module.exports = { tribeInviteCodeText: "Gonbidapen kodea: ", oasisVersionLabel: "Oasis Bertsioa", tribeInviteCodeHint: "Partekatu kode hau gonbidatu nahi duzun pertsonarekin. /invites → Tribes bidez sar daiteke.", + tribeGroupTribe: "Tribua", + tribeGroupOffice: "Bulegoa", + tribeGroupNetwork: "Sarea", + tribeGroupEconomy: "Ekonomia", + tribeGroupMedia: "Media", tribeStatusOpen: "IREKITA", tribeStatusClosed: "ITXITA", tribeStatusInProgress: "ABIAN", @@ -1916,17 +2125,33 @@ module.exports = { tribePriorityCritical: "KRITIKOA", tribeTaskFilterAll: "GUZTIAK", tribeMediaFilterAll: "GUZTIAK", + tribeReportCatBug: "AKATSA", + tribeReportCatAbuse: "ABUSUA", + tribeReportCatContent: "EDUKIA", + tribeReportCatOther: "BESTEA", tribeForumCatGeneral: "OROKORRA", tribeForumCatProposal: "PROPOSAMENA", tribeForumCatQuestion: "GALDERA", tribeForumCatAnnouncement: "IRAGARPENA", + tribeMarketCatGoods: "ONDASUNAK", + tribeMarketCatServices: "ZERBITZUAK", + tribeMarketCatFood: "ELIKADURA", + tribeMarketCatOther: "BESTEA", tribeStatusLabel: "Egoera", tribeSubTribes: "AZPI-TRIBUAK", tribeSubTribesCreate: "Azpi-Tribua Sortu", tribeSubTribesStrictDenied: "Tribuaren modu zorrotzak ez dizu azpi-tribu berriak sortzen uzten. Jarri harremanetan administratzailearekin.", + tribeSubTribesEmpty: "Ez da azpi-triburik sortu, oraindik.", + tribeActivityJoined: "BATUTA", + tribeActivityLeft: "IRTEN", + tribeActivityFeed: "FEED", tribeActivityRefeed: "REFEED", + tribeGroupAnalytics: "Analitikak", + tribeGroupCreative: "Sormenez", tribeSectionActivity: "JARDUERA", tribeSectionTrending: "JOERAK", + tribeSectionOpinions: "IRITZIAK", + tribeSectionPixelia: "PIXELIA", tribeSectionTags: "ETIKETAK", tribeSectionSearch: "BILATU", tribeActivityEmpty: "Ez dago jarduerarik oraindik.", @@ -1938,15 +2163,25 @@ module.exports = { tribeTrendingPeriodWeek: "Aste Honetan", tribeTrendingPeriodAll: "Dena", tribeTrendingEngagement: "parte-hartzea", + tribeOpinionsEmpty: "Ez dago iritzirik oraindik.", + tribeOpinionsCast: "Bozkatu", + tribeOpinionsRankings: "Sailkapenak", + tribeOpinionsAlreadyVoted: "Dagoeneko bozkatu duzu", tribeTopCategory: "Gehien bozkatua", + tribePixeliaTitle: "Pixelia", + tribePixeliaDescription: "Pixel art oihal kolaboratiboa.", + tribePixeliaPaint: "Pintatu", + tribePixeliaContributors: "laguntzaileak", + tribePixeliaTotalPixels: "pixel margotuta", tribeTagsEmpty: "Ez da etiketarik aurkitu.", + tribeTagsCloud: "Etiketa Hodeia", + tribeTagsContentWith: "Etiketadun edukia", tribeSearchPlaceholder: "Bilatu tribuaren edukian...", tribeSearchEmpty: "Emaitzarik ez.", tribeSearchResults: "Emaitzak", tribeSearchMinChars: "Idatzi gutxienez 2 karaktere bilatzeko.", agendaTitle: "Agenda", agendaDescription: "Eskeituta dauzkazun elementu guztiak aurki ditzakezu hemen.", - agendaSearchPlaceholder: "Bilatu agendan...", agendaFilterAll: "GUZTIAK", agendaFilterToday: "GAUR", agendaFilterUpcoming: "DATOZENAK", @@ -1960,15 +2195,24 @@ module.exports = { agendaFilterReports: "TXOSTENAK", agendaFilterTransfers: "TRANSFERENTZIAK", agendaFilterJobs: "LANPOSTUAK", - agendaFilterIndustry: "INDUSTRIA", - agendaFilterHousing: "ETXEBIZITZA", agendaFilterProjects: "PROIEKTUAK", agendaFilterCalendars: "EGUTEGIAK", agendaInviteModeLabel: "Egoera", agendaNoItems: "Esleipenik ez.", agendaDiscardButton: "Baztertu", agendaRestoreButton: "Berrezarri", + agendaAuthor: "Nork", + agendaCreatedAt: "Noiz", + agendaTitleLabel: "Izenburua", agendaMembersCount: "Partaideak", + agendaDescriptionLabel: "Deskribapena", + agendaStatus: "Egoera", + agendaVisibility: "Ikusgaitasuna", + agendaEventDate: "Ekitaldiaren Data", + agendaEventLocation: "Kokapena", + agendaEventPrice: "Prezioa", + agendaEventUrl: "URL-a", + agendaTaskStart: "Hasiera Data", agendaLocationLabel: "Kokapena", agendaYes: "BAI", agendaNo: "EZ", @@ -1977,6 +2221,11 @@ module.exports = { agendareportCategory: "Kategoria", agendareportSeverity: "Larritasuna", agendareportStatus: "Egoera", + agendareportDescription: "Deskribapena", + agendaTaskEnd: "Amaiera Data", + agendaTaskPriority: "Lehentasuna", + agendaTransferFrom: "Nork", + agendaTransferTo: "Nori", agendaTransferConcept: "Kontzeptua", agendaTransferAmount: "Kopurua", agendaTransferDeadline: "Epemuga", @@ -1986,7 +2235,47 @@ module.exports = { voteNow: "Bozkatu orain", alreadyVoted: "Jada zure iritzia eman duzu.", noOpinionsFound: "Ez da iritzirik aurkitu.", + RECENTButton: "AZKENAK", TOPButton: "GORDEAK", + interestingButton: "INTERESGARRIA", + necessaryButton: "BEHARREZKOA", + funnyButton: "BARREGARRIA", + disgustingButton: "LOREZTUGARRIA", + sensibleButton: "SENTIKORRA", + propagandaButton: "PROPAGANDA", + adultOnlyButton: "ADULTO BAKARRIK", + boringButton: "NEKAGARRIA", + confusingButton: "ZAILA", + usefulButton: "ERABILI", + informativeButton: "INFORMATIBOA", + wellResearchedButton: "ONDO IKERTUA", + accurateButton: "EGOKITUA", + needsSourcesButton: "ITURRIK BEHAR DITU", + wrongButton: "OKERREKOA", + lowQualityButton: "BEHERA KALITATEA", + creativeButton: "KREABO", + insightfulButton: "ARRETAZKOA", + actionableButton: "ERABILI DAITEKE", + inspiringButton: "INSPIRATZAILEA", + loveButton: "MAITASUN", + clearButton: "ARGI", + upliftingButton: "EGOITZAILEA", + unnecessaryButton: "EZINBESTEKOA", + rejectedButton: "UKATUA", + misleadingButton: "ENGAINEZKOA", + offTopicButton: "GAITIK AT", + duplicateButton: "DUPLIKATU", + clickbaitButton: "KLIK-APURKA", + spamButton: "SPAM", + trollButton: "TROLL", + nsfwButton: "NSFW", + violentButton: "BORTITZA", + toxicButton: "TOXIKOA", + harassmentButton: "BAZTERKETA", + hateButton: "ARRETA", + scamButton: "ENGAINU", + triggeringButton: "HAUSKORRA", + opinionsCreatedAt: "Sortu zen", opinionsTotalCount: "Iritzi guztiak", voteInteresting: "Interesgarria", voteNecessary: "Beharrezkoa", @@ -2023,39 +2312,27 @@ module.exports = { voteCreative: "Sortzailea", voteSpam: "Spam", voteAdultOnly: "Ados bakarrik", + publishBlog: "Bloga argitaratu", privateMessage: "MP", pmSendTitle: "Mezu Pribatuak", pmSend: "Bidali!", - pmCancel: "Utzi", - pmReset: "Berrezarri", - pmSharedKeyHint: "Partekatu gako hau hartzailearekin beste bide batetik. Deszifratzeko beharrezkoa da.", - pmDescription: "Erabili formulario hau beste biztanleei mezu/fitxategi zifratu bat bidaltzeko.", - pmComposeTitle: "Bidali mezu bat", + pmDescription: "Erabili formulario hau beste biztanleei mezu zifratua bidaltzeko.", pmRecipients: "Hartzaileak", pmRecipientsHint: "Idatzi Oasis IDak, komaz bereizita", - pmLimitsHint: "Mezu bakoitzeko 7 hartzaile gehienez.", - pmTextPlaceholder: "Gorputza 7000 karakteretara mugatua.", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "Gaia", pmSubjectHint: "Idatzi mezuaren gaia", pmText: "Mezua", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Deszifratu", - fileShareTitle: "Partekatu fitxategi bat", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "Fitxategia", - fileShareDownload: "Deskargatu", - fileShareMutualError: "Elkarrekiko laguntza duten biztanleekin soilik parteka ditzakezu fitxategiak.", - fileShareNoFile: "Ez da fitxategirik hautatu.", - fileShareTooLarge: "Fitxategiak baimendutako tamaina gainditzen du.", - fileShareFailed: "Ezin izan da fitxategia prestatu.", - fileShareSendError: "Ezin izan da fitxategia bidali.", - fileShareUnavailable: "Fitxategi hau ez dago erabilgarri une honetan. Saiatu geroago.", pmCrypterBadKey: "Partekatutako gakoa ez da zuzena!", pmCrypterTooLong: "Mezua luzeegia da Crypter-arekin zifratzeko. Laburtu eta saiatu berriro.", pmCrypterCipherLabel: "Berriz zifratutako mezua", pmCrypterCharsLabel: "karaktere", pmInvalidRecipients: "Oasis ID baliogabea (@…=.ed25519 itxurakoa izan behar du).", pmEncryptionLabel: "Zifratzea", + pmFile: "Eranskina", private: "Pribatuak", privateDescription: "Zure mezu zifratuak.", privateInbox: "SARRERA-ONTZIA", @@ -2065,39 +2342,34 @@ module.exports = { noPrivateMessages: "Ez dago mezu pribaturik.", pmReply: "Erantzun", pmReplies: "erantzunak", + pmNew: "berriak", + pmMarkRead: "Irakurritako gisa markatu", inReplyTo: "HONI ERANTZUNEZ", pmPreview: "Aurrebista", pmPreviewTitle: "Mezuaren aurrebista", performed: "→", pmFromLabel: "Nork:", pmToLabel: "Nori:", + pmInvalidMessage: "Mezu baliogabea", pmNoSubject: "(gaia gabe)", pmSubjectLabel: "Gaia:", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "L.A.R.P-eko etxe gobernatzailea aldatu da.", - politicalBotParliamentTitle: "Gobernu-ziklo berri bat hasi da Parlamentuan.", - politicalBotTribeTitle: "Gobernu-ziklo berri bat hasi da zure Tribuetako batean.", - politicalBotLarpIntro: "Orain {house} ari da gobernatzen ziklo honetan: {cycle}", - politicalBotParliamentIntro: "Egungo gobernua {gov} da", - politicalBotParliamentIntroLeader: "Egungo gobernua {gov} da, {leader}-(e)k gauzatua", - politicalBotTribeIntro: "{tribe} Tribuko egungo gobernua {gov} da", - politicalBotTribeIntroLeader: "{tribe} Tribuko egungo gobernua {gov} da, {leader}-(e)k gauzatua", - politicalBotLeaderLabel: "Liderra", + pmBodyLabel: "Gorputza", + pmBotJobs: "42-JobsBOT", + pmBotProjects: "42-ProjectsBOT", + pmBotMarket: "42-MarketBOT", inboxJobSubscribedTitle: "Lan-eskaintzara harpidetza berria", pmInhabitantWithId: "OASIS ID duen biztanlea:", pmHasSubscribedToYourJobOffer: "zure lan-eskaintzara harpidetu da", + inboxProjectCreatedTitle: "Proiektu berria sortu da", + pmHasCreatedAProject: "proiektu bat sortu du", inboxMarketItemSoldTitle: "Artikulua salduta", - inboxShopSoldTitle: "Produktua salduta", pmYourItem: "Zure artikulua", pmHasBeenSoldTo: "honi saldu zaio", pmFor: "truke", inboxProjectPledgedTitle: "Ekarpen berria zure proiektuan", pmHasPledged: "ekarpena egin du", pmToYourProject: "zure proiektura", + blockchainTitle: 'BlockExplorer', blockchainDescription: 'Blokeak aztertu eta ikusgaitu blockchain-ean.', blockchainNoBlocks: 'Ez da blokeik aurkitu blockchain-ean.', blockchainStatsTitle: 'Estatistikak', @@ -2109,31 +2381,41 @@ module.exports = { blockchainBlockCarbon: 'Karbono-aztarna', blockchainBlockType: 'Mota', blockchainBlockTimestamp: 'Tzeitza', + blockchainBlockContent: 'Bloke', + blockchainBlockURL: 'URL:', + blockchainContent: 'Bloke', + blockchainContentPreview: 'Bloke edukia aurrebista', blockchainLatestDatagram: 'Azken Datagrama', - blockchainDatagram: "Datagrama", - blockchainDetails: "Ikusi Blokearen Xehetasunak", - blockchainViewBlockexplorer: "Ikusi Blockexplorer-en", + blockchainDatagram: 'Datagrama', + blockchainDetails: 'Ikusi blokearen xehetasunak', + blockchainViewBlockexplorer: "Ikusi blockexplorer-ean", + blockchainBlockInfo: 'Blokearen informazioa', + blockchainBlockDetails: 'Hautatutako blokearen xehetasunak', blockchainBack: 'Itzuli blokearen azterkira', blockchainContentDeleted: "Edukia ezabatu egin da", banking: 'Banking', bankingTitle: 'Banking', - bankingDescription: "ECOin ekonomia: saldoa, oinarrizko errenta, zergak eta industria balioa.", + bankingDescription: 'Aztertu ECOin-en egungo balioa eta dagokion RBU esleipena, astero banatzen dena parte-hartzearen eta konfiantzaren arabera.', bankOverview: 'Laburpena', bankEpochs: 'Epeak', bankRules: 'Arauak', pending: 'Zain', closed: 'Itxita', + bankBack: 'Itzuli Banking-era', bankViewTx: 'Tx ikusi', bankClaimNow: 'Esleitu', bankClaimUBI: 'RBU eskatu!', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: 'Ez dago RBU esleipen zain garai honetan.', bankEpoch: 'Epea', bankPool: 'Funtsa (epe honetan)', bankWeightsSum: 'Pisuen batura', + bankAllocations: 'Esleipenak', bankNoAllocations: 'Ez da esleipenik aurkitu.', bankNoEpochs: 'Ez da eperik aurkitu.', bankEpochAllocations: 'Epearen esleipenak', @@ -2147,11 +2429,10 @@ module.exports = { bankEpochId: 'Epearen IDa', bankRuleHash: 'Arauen snapshot hash-a', bankViewEpoch: 'Epea ikusi', - bankYourIndustryBalance: "Zure industria zatia", - bankYourUbiMonth: "Zure OBU (hilabete honetan)", - bankYourFundsMonth: "Zure funtsak (hilabete honetan)", - bankIndustryBalance: "Industria ekoizpena (estimatua)", bankUserBalance: 'Zure saldoa', + ecoWalletNotConfigured: 'ECOin poltsa ez dago konfiguratuta', + editWallet: 'Poltsa editatu', + addWallet: 'Poltsa gehitu', bankAddresses: 'Helbideak', bankNoAddresses: 'Ez da helbiderik aurkitu.', bankUser: 'Oasis ID', @@ -2176,11 +2457,19 @@ module.exports = { search: 'Bilatu!', bankLocal: 'Lokala', bankFromOasis: 'Oasis', + bankMyAddress: 'Zure helbidea', + bankRemoveMyAddress: 'Nire helbidea kendu', + bankNotRemovableOasis: 'Oasis helbideak ezin dira lokalki kendu', bankingUserEngagementScore: "KARMA puntuazioa", + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "FUNTSIK EZ!", bankUbiAvailableOk: "ESKURAGARRI!", - bankUbiAvailability: "OBU (sarea)", + bankUbiAvailability: "UBI eskuragarritasuna", bankAlreadyClaimedThisMonth: "Hilabete honetan jada aldarrikatu da", bankUbiThisMonth: "UBI (hilabete honetan)", bankUbiLastClaimed: "UBI (azken aldarrikapena)", @@ -2194,6 +2483,13 @@ module.exports = { shopsTitle: "Dendak", shopDescription: "Sareko dendak aurkitu eta kudeatu.", shopTitle: "Denda", + shopFilterAll: "GUZTIAK", + shopFilterMine: "NIREAK", + shopFilterRecent: "BERRIAK", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUKTUAK", + shopFilterPrices: "PREZIOAK", + shopFilterFavorites: "GOGOKOAK", shopUpload: "Denda sortu", shopAllSectionTitle: "Denda guztiak", shopMineSectionTitle: "Nire Dendak", @@ -2210,10 +2506,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "Clearnet-en argitaratu", + shopClearnetUnpublish: "Clearnet-etik kendu", + shopClearnetUrlLabel: "Clearnet URLa", + shopClearnetWarning: "Clearnet-en argitaratzeak denda hau kanpoko bilatzaileek eta crawlerrek indexagarri egiten du.", shopAddFavorite: "Gogoko gehitu", shopRemoveFavorite: "Gogoko kendu", shopOpen: "IREKIA", shopClosed: "ITXIA", + shopOpenShop: "Denda ireki", + shopCloseShop: "Denda itxi", shopMakePrivate: "PRIBATU BIHURTU (E2E)", shopMakePublic: "PUBLIKO BIHURTU", shopProducts: "Produktuak", @@ -2241,6 +2543,8 @@ module.exports = { shopOrderPrice: "Prezioa", shopOrderBuyer: "Erosle", shopBackToShop: "Dendara itzuli", + shopShareUrl: "Partekatzeko URLa", + shopVisitShop: "DENDA BISITATU", marketVisitItem: "ITEMA BISITATU", shopStatus: "EGOERA", shopCreatedAt: "SORTUA", @@ -2249,10 +2553,12 @@ module.exports = { shopLocation: "Kokapena", shopTags: "Etiketak", shopVisibility: "Ikusgarritasuna", + shopImage: "Irudia", shopShortDescription: "Deskribapen Laburra", shopShortDescriptionPlaceholder: "Deskribapen laburra txarteletarako (gehienez 160 karaktere)", shopTitlePlaceholder: "Zure dendaren izena", shopDescriptionPlaceholder: "Zure dendaren deskribapen zehatza", + shopUrlPlaceholder: "https://zure-denda-url.com", shopLocationPlaceholder: "Hiria, Herrialdea", shopTagsPlaceholder: "tag1, tag2, tag3", mapTitlePlaceholder: "Maparen titulua", @@ -2269,6 +2575,7 @@ module.exports = { bankUnitMinutes: "minutu", bankUnitDays: "egun", bankExchangeNoData: 'Ez dago daturik eskuragarri', + bankExchangeIndex: 'ECOin Balioa (1h)', bankInflation: 'ECOin Inflazioa (anual)', bankInflationMonthly: 'ECOin Inflazioa (mensual)', bankExchangeChartTitle: 'ECOin balioaren bilakaera denboran zehar', @@ -2282,17 +2589,38 @@ module.exports = { bankingSyncStatusOutdated: 'Desegonetik', statsTitle: 'Estatistikak', statistics: "Estatistikak", + statsInhabitant: "Bizilagunaren estatistikak", statsDescription: "Ezagutu zure sareari buruzko estatistikak.", ALLButton: "GUZTIAK", MINEButton: "NIREAK", TOMBSTONEButton: "EZABAKETAK", + statsYou: "Zu", + statsUserId: "Oasis ID", statsCreatedAt: "Sortze data", statsYourContent: "Edukia", statsYourOpinions: "Iritziak", + statsYourTombstone: "Ezabaketak", + statsNetwork: "Sarea", + statsTotalInhabitants: "Bizilagunak", + statsDiscoveredTribes: "Tribuak (Publikoak)", + statsPrivateDiscoveredTribes: "Tribuak (Pribatuak)", statsNetworkContent: "Edukia", + statsYourMarket: "Merkatua", + statsYourJob: "Lanak", + statsYourProject: "Proiektuak", + statsYourTransfer: "Transferentziak", + statsYourForum: "Foroak", statsNetworkOpinions: "Iritziak", + statsDiscoveredMarket: "Merkatua", + statsDiscoveredJob: "Lanak", + statsDiscoveredProject: "Proiektuak", + statsBankingTitle: "Bankua", statsEcoWalletLabel: "ECOIN zorroa", statsEcoWalletNotConfigured: "Konfiguratu gabe!", + statsTotalEcoAddresses: "Helbide kopurua", + statsDiscoveredTransfer: "Transferentziak", + statsDiscoveredForum: "Foroak", + statsNetworkTombstone: "Ezabaketak", statsBookmark: "Laster-markak", statsEvent: "Ekitaldiak", statsTask: "Zereginak", @@ -2314,12 +2642,16 @@ module.exports = { statsAiExchange: "IA", statsPUBs: 'PUBack', statsPost: "Mezuak", + statsOasisID: "Oasis ID", statsSize: "Guztira (tamaina)", statsBlockchainSize: "Blockchain (tamaina)", statsBlobsSize: "Blobak (tamaina)", statsActivity7d: "Jarduera (azken 7 egun)", statsActivity7dTotal: "7 eguneko guztira", statsActivity30dTotal: "30 eguneko guztira", + statsKarmaScore: "KARMA puntuazioa", + statsPublic: "Publikoa", + statsPrivate: "Pribatua", day: "Eguna", messages: "Mezuak", statsProject: "Proiektuak", @@ -2331,14 +2663,30 @@ module.exports = { statsProjectsCancelled: "Bertan behera", statsProjectsGoalTotal: "Helburu osoa", statsProjectsPledgedTotal: "Konprometitutako guztira", + statsProjectsSuccessRate: "Arrakasta tasa", + statsProjectsAvgProgress: "Batez besteko aurrerapena", + statsProjectsMedianProgress: "Aurrerapen mediana", + statsProjectsActiveFundingAvg: "Finantzaketa aktiboaren batez bestekoa", + statsJobsTitle: "Lanak", + statsJobsTotal: "Lan kopurua", + statsJobsOpen: "Irekita", + statsJobsClosed: "Itxita", + statsJobsOpenVacants: "Plaza irekiak", + statsJobsSubscribersTotal: "Harpidedun kopurua", + statsJobsAvgSalary: "Soldata ertaina", + statsJobsMedianSalary: "Soldata mediana", statsMarketTitle: "Merkatua", statsMarketTotal: "Gai kopurua", statsMarketForSale: "Salgai", statsMarketReserved: "Erreserbatuta", statsMarketClosed: "Itxita", statsMarketSold: "Salduta", + statsMarketRevenue: "Diru-sarrerak", + statsMarketAvgSoldPrice: "Salmenta prezio ertaina", statsUsersTitle: "Bizilagunak", user: "Bizilaguna", + statsTombstoneTitle: "Ezabaketak", + statsNetworkTombstones: "Sareko ezabaketak", statsTombstoneRatio: "Ezabaketa ratioa (%)", statsParliamentCandidature: "Parlamenturako hautagaitzak", statsParliamentTerm: "Parlamentuko agintaldiak", @@ -2365,21 +2713,30 @@ module.exports = { aiConfiguration: "Konfiguratu prompt-a", aiPromptUsed: "Prompt-a", aiClearHistory: "Txataren historia garbitu", + aiSharePrompt: "Erantzun hau entrenamendu kolektibora gehitu?", + aiShareYes: "Bai", + aiShareNo: "Ez", + aiSharedLabel: "Entrenamendura gehituta", + aiRejectedLabel: "Ez da gehitu entrenamendura", aiServerError: "Ezin izan da IAren erantzuna lortu. Saiatu berriro.", aiInputPlaceholder: "What is Oasis?", typeAiExchange: "IA", aiApproveTrain: "Prestakuntza kolektibora gehitu", aiRejectTrain: "Ez entrenatu", + aiTrainPending: "Onartzearen zain", aiTrainApproved: "Entrenamendurako onartua", aiTrainRejected: "Entrenamendurako baztertua", aiSnippetsUsed: "Erabilitako zatiak", aiSnippetsLearned: "Ikasitako fragementuak", statsAITraining: "IA prestakuntza", + aiApproveCustomTrain: "Erantzun pertsonalizatu hau erabiliz trebatu", aiCustomAnswerPlaceholder: "Idatzi zure erantzun pertsonalizatua…", + statsAIExchanges: "Ereduen trukea", marketMineSectionTitle: "Zure artikuluak", marketCreateSectionTitle: "Artikulua sortu", marketUpdateSectionTitle: "Eguneratu", marketAllSectionTitle: "Merkatua", + marketRecentSectionTitle: "Azken merkatua", marketTitle: "Merkatua", marketDescription: "Zure sarean ondasunak edo zerbitzuak trukatzeko merkatua.", marketFilterAll: "GUZTIAK", @@ -2409,11 +2766,14 @@ module.exports = { marketItemDeadline: "Epemuga", marketItemIncludesShipping: "Bidalketa barne?", marketItemHighestBid: "Puja altuena", + marketItemHighestBidder: "Pujatzaile onena", marketItemStock: "Stocka", marketOutOfStock: "Stockik gabe", + marketItemBidTime: "Pujaren data", marketActionsUpdate: "Eguneratu", marketUpdateButton: "Eguneratu", marketActionsDelete: "Ezabatu", + marketActionsSold: "Salduta gisa markatu", marketActionsChangeStatus: "EGOERA ALDATU", marketActionsBuy: "EROSI!", marketAuctionBids: "Oraingo pujаk", @@ -2422,82 +2782,19 @@ module.exports = { marketNoItems: "Oraindik ez dago artikulurik eskuragarri.", marketYourBid: "Zure puja", marketCreateFormImageLabel: "Multimedia igo (max: 50MB)", + marketSearchLabel: "Bilatu", marketSearchPlaceholder: "Izenburuan edo etiketetan bilatu", marketMinPriceLabel: "Gutxieneko prezioa", marketMaxPriceLabel: "Gehieneko prezioa", + marketSortLabel: "Ordenatu", marketSortRecent: "Berrienak", marketSortPrice: "Prezioa", marketSortDeadline: "Epemuga", marketSearchButton: "Bilatu", marketAuctionEndsIn: "Amaitzen da", marketAuctionEnded: "Amaituta", + marketMyBidBadge: "Puja egin duzu", marketNoItemsMatch: "Ez dago zure bilaketarekin bat datorren artikulurik.", - housingTitle: "Etxebizitza", - housingDescriptionText: "Aurkitu eta kudeatu lekuak zure sarean.", - modulesHousingLabel: "Etxebizitza", - modulesHousingDescription: "Lekuak aurkitzeko eta kudeatzeko modulua.", - typeHousing: "ETXEBIZITZA", - housingFilterAll: "DENAK", - housingFilterMine: "NIREAK", - housingFilterRequested: "ESKATUTAKOAK", - housingFilterSale: "SALMENTA", - housingFilterRent: "ALOKAIRUA", - housingFilterCouchsurfing: "COUCHSURFING", - housingFilterOpen: "ESKURAGARRI", - housingFilterClosed: "EZ ESKURAGARRI", - housingFilterRecent: "BERRIAK", - housingFilterTop: "TOP", - housingType: "Eskaintza", - housingTypeSALE: "Salgai", - housingTypeRENT: "Alokagai", - housingTypeCOUCHSURFING: "Couchsurfing", - housingProperty: "Higiezina", - housingPropertyAPARTMENT: "Pisua", - housingPropertyHOUSE: "Etxea", - housingPropertyROOM: "Gela", - housingPropertyLAND: "Lursaila", - housingPropertyOTHER: "Bestelakoa", - housingTitleLabel: "Izenburua", - housingTitlePlaceholder: "Lekuaren izena", - housingDescription: "Deskribapena", - housingDescriptionPlaceholder: "Deskribatu lekua, auzoa, zer dagoen barne: komunak, lorategia, oheak…", - housingRules: "Arauak", - housingRulesPlaceholder: "Animaliak, erretzea, isiltasun orduak, gutxieneko egonaldia…", - housingPlace: "Lekua", - housingPlacePlaceholder: "Herrialdea, hiria, helbidea", - housingPrice: "Prezioa", - housingPriceLabel: "Prezioa (ECO)", - housingSizeLabel: "Azalera (m²)", - housingFree: "DOAN", - housingPerMonth: "/hilabete", - housingRooms: "Gelak", - housingSize: "Azalera", - housingCapacity: "Gonbidatuak", - housingAvailableFrom: "Noiztik erabilgarri", - housingAvailableTo: "Noiz arte erabilgarri", - housingTags: "Etiketak", - spreadEditWarning: "Eduki honek spread-ak galduko ditu editatzean", - housingRequests: "Eskaerak", - housingRequestButton: "Eskatu", - housingCancelButton: "Ezeztatu eskaera", - housingRequestedBadge: "ESKATUTA", - housingStatus: "Egoera", - housingStatusOPEN: "ESKURAGARRI", - housingStatusCLOSED: "EZ ESKURAGARRI", - housingSetOpen: "Eskuragarri jarri", - housingSetClosed: "Ez eskuragarri jarri", - housingCreateButton: "Sortu lekua", - housingUpdateButton: "Eguneratu", - housingDeleteButton: "Ezabatu", - housingSearchPlaceholder: "Bilatu lekuak...", - housingSearchButton: "Bilatu", - housingMinPrice: "Gutxieneko prezioa", - housingMaxPrice: "Gehieneko prezioa", - housingSortRecent: "Berrienak", - housingSortPrice: "Prezio baxuena", - housingSortRequests: "Gehien eskatuak", - housingSortRating: "Ondoen baloratuak", - housingNoItems: "Ez da lekurik aurkitu.", jobsTitle: "Lanak", jobsCandidatesTitle: "Hautagai iradokitakoak", jobsCandidatesDescription: "Lan honetarako trebetasunak dituzten biztanleak. Bidali mezu pribatua gonbidatzeko.", @@ -2520,6 +2817,10 @@ module.exports = { jobsHoursOffered: "Eskainitako orduak", jobsHoursRequested: "Trukean eskatutako orduak", jobsExchangeSkill: "Trukean eskatutako gaitasuna", + jobsHoursOfferedPlaceholder: "ad. 4", + jobsHoursRequestedPlaceholder: "ad. 4", + jobsExchangeSkillPlaceholder: "ad. zurgintza, diseinua", + jobExchangeHint: "Ordu-truke lanetarako, bete beheko eremuak soldataren ordez.", jobsCV: "CV-ak", jobsCreateJob: "Argitaratu Lana", jobsRecentTitle: "Lana Azkenak", @@ -2556,6 +2857,7 @@ module.exports = { jobLocationPresencial: "Aurrez aurrekoa", jobLocationRemote: "Urrunekoa", jobVacantsPlaceholder: "Postu kopurua", + jobSalaryPlaceholder: "Ordubeteko soldata ECO", jobImage: "Multimedia igo (max: 50MB)", jobTasks: "Eginkizunak", jobType: "Lanen Motak", @@ -2563,6 +2865,7 @@ module.exports = { jobSubscribers: "Harpidetzaileak", noSubscribers: "Ez da harpidetzailerik", createJobButton: "Argitaratu Lana", + viewDetailsButton: "Ikusi Xehetasunak", noJobsFound: "Ez daude lan eskaintzak.", jobAuthor: "Egilea", jobTimePartial: "Partziala", @@ -2572,201 +2875,31 @@ module.exports = { jobsFilterApplied: "ESKATUTA", jobsAppliedTitle: "Nire eskaerak", jobsAppliedBadge: "Izena emanda", + jobsFilterFavs: "Gustukoak", + jobsFavsTitle: "Gustukoak", + jobsFilterNeeds: "Laguntza behar du", + jobsNeedsTitle: "Laguntza behar du", + jobsSearchLabel: "Bilatu", jobsSearchPlaceholder: "Bilatu izenburua, etiketak, deskribapena...", jobsMinSalaryLabel: "Gutx. soldata", jobsMaxSalaryLabel: "Geh. soldata", + jobsSortLabel: "Ordenatu", jobsSortRecent: "Berrienak", jobsSortSalary: "Soldata altuena", jobsSortSubscribers: "Hautagai gehien", jobsSearchButton: "Bilatu", + jobsFavoriteButton: "Gustukoa", + jobsUnfavoriteButton: "Kendu gustukoa", + jobsMessageAuthorButton: "MP", jobsApplicants: "Hautagaiak", jobsUpdatedAt: "Eguneratua", jobSetOpen: "Ireki gisa markatu", + jobNewBadge: "BERRIA", jobsTagsLabel: "Etiketak", + jobsTagsPlaceholder: "etiketa1, etiketa2, etiketa3", noJobsMatch: "Ez dago bilaketarekin bat datorren lan-eskaintzarik.", - industrySearchPlaceholder: "Bilatu fabrikak…", - industryAllSectors: "Sektore guztiak", - industryVisitButton: "Bisitatu industria", - industryAdvanceTo: "Ezarri", - industryAmount: "Kopurua", - industryApproveBuild: "Onartu", - industryBlueprint: "Blueprint", - industryBlueprints: "Blueprint-ak", - industryBuild: "Build", - industryBuildNotes: "Oharrak", - industryBuildStart: "Hasiera data", - industryVoteUpdateButton: "Bozkatu eguneraketa", - industryVoteDeleteButton: "Bozkatu ezabaketa", - industryRevokeVote: "Errebokatu", - industryTimeLeft: "Geratzen den denbora", - industryBuildEnd: "Amaiera data", - industryBuilds: "Build-ak", - industryBuildTitle: "Izenburua", - industryContribute: "Lagundu", - industryContributeButton: "Ekarpena egin", - industryContributions: "Ekarpenak", - industryCreateBlueprintButton: "Sortu blueprint", - industryDistribute: "Banatu", - industryDistributeButton: "Banatu kuoten arabera", - industryDistribution: "Banaketa", - industryEcoAmount: "Kopurua (ECO)", - industryEditBlueprint: "Editatu blueprint", - industryFacility: "Fabrika", - industryKindLabel: "Mota", - industryKind_labor: "Lana", - industryKind_material: "Materiala", - industryKind_eco: "Funtsak", - industryLaborHours: "Lana (orduak)", - industryHours: "Orduak", - industryMaterialItem: "Materiala", - industryMaterials: "Materialak", - industryEstMaterials: "Materialak guztira", - industryEstLabor: "Lana guztira", - industryEstTotal: "Prezio estimatua", - industryBuildingPrice: "Eraikuntza prezioa", - industryFinalPrice: "Azken prezioa", - industryBlueprintDescPlaceholder: "Zehaztapenak, neurriak, pisua, dokumentazioa…", - industryMaterialValue: "Balioa (ECO)", - industryMember: "Kidea", - industryNewBlueprint: "Blueprint berria", - industryNewBuild: "Proposatu build bat", - industryEditBuild: "Editatu ekoizpena", - industryNeedBlueprint: "Sortu lehenik planoa: ekoizpen bakoitzak bat egiten du.", - industryNoBlueprints: "Oraindik ez dago blueprint-ik.", - industryNoBuilds: "Oraindik ez dago build-ik.", - industryNote: "Oharra", - industryOutput: "Irteera", - industryOutputKind: "Produktu mota", - industryOutputValue: "Irteera balioa (ECO, adib. salmentatik)", - industryPostJob: "Sortu lana", - industryPot: "Poltsa", - industryProposeBuildButton: "Proposatu build", - industryProposer: "Proposatzailea", - industryAuthor: "Egilea", - industrySellOnMarket: "Bidali merkatura", - industrySendToProjects: "Sortu proiektua", - industrySkills: "Trebetasunak", - industryTreasury: "Diruzaintza", - industryKind_physical: "Fisikoa", - industryKind_digital: "Digitala", - industryBuildStatus_PROPOSED: "PROPOSATUA", - industryBuildStatus_REJECTED: "BAZTERTUA", - industryBuildStatus_APPROVED: "ONARTUA", - industryBuildStatus_STOCKING: "HORNITZEN", - industryBuildStatus_IN_PRODUCTION: "EKOIZPENEAN", - industryBuildStatus_COMPLETED: "OSATUA", - industryBuildStatus_FAILED: "HUTS EGINDA", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "Lanpostu bat zure curriculumarekin bat dator", - jobsBotMatchIntro: "Lanpostu bat zure curriculumarekin bat dator.", - jobsBotMatchJob: "Lanpostua", - jobsBotMatchHint: "Hau jasotzen duzu zure curriculuma IAk kudeatzen duelako. Zure CVn desaktiba dezakezu.", - cvAiManaged: "IAk kudeatua", - switchOn: "PIZTUTA", - switchOff: "ITZALITA", - cvAiManagedHint: "Aktibo badago, JobsBotek mezu pribatuz abisatuko dizu lanpostu bat zure curriculumarekin bat datorrenean.", - cvMatchThreshold: "Abisatu bat etortze maila honetatik aurrera", - housingBotRequestedTitle: "Norbaitek zure lekuetako bat eskatu du.", - housingBotCancelledTitle: "Zure lekuetako baten eskaera ezeztatu da.", - housingBotYouRequestedTitle: "Leku bat eskatu duzu.", - housingBotYouCancelledTitle: "Eskaera bat ezeztatu duzu.", - housingBotUnavailableTitle: "Eskatu zenuen leku bat ez dago eskuragarri.", - housingBotUnavailableSentTitle: "Leku hau eskatu zutenei jakinarazi diezu.", - industryBotAdmittedTitle: "Fabrika batean onartu zaituzte.", - industryBotApplicationTitle: "Kidetza eskaera berria zure fabrikan.", - industryBotInvitedTitle: "Fabrika batera gonbidatu zaituzte.", - industryBotDissolvedTitle: "Zure fabrika bat desegin da.", - industryBotBuildApprovedTitle: "Build bat onartu da.", - industryBotDistributedTitle: "Build baten irteera banatu da.", - industryFilterRules: "ARAUAK", - industryRulesTitle: "Arauak", - industryRulesIntro: "Industry-k biztanleei ekoizpen bideak modu kolektiboan izatea ahalbidetzen die: Fabrika bat sareko lantegia da, inork pribatuki ez duena.", - industryRulesSteward: "Sortzailea arduraduna da — zaindaria, ez jabea: ezin du Fabrika bakarrik saldu, pribatizatu edo desegin.", - industryRulesMembership: "Kidetza politikak nola sartu erabakitzen du: Irekia (berehala), Botoz (kideen botoak onartuta) edo Gonbidapenez soilik (kide batek lehenik gonbidatu behar zaitu).", - industryRulesQuorum: "Quoruma erabaki batek balio izateko bozkatu behar duten gutxieneko kide kopurua da, Fabrika txiki bat pertsona bakar batek erabaki ez dezan.", - industryRulesMajority: "Gehiengoa onartzeko behar den boto frakzioa da (0,5 = erdia, 1 = aho batez); erabaki bat onartzen da BAI botoek gutxienez max(quoruma, kideak × gehiengoa) lortzen dutenean.", - industryRulesDecisions: "Kideek bozkatzen dute berriak onartzeko, build-ak onartzeko eta Fabrika desegiteko; arduradunak ezin ditu erabaki kolektibo hauek baztertu.", - industryRulesBlueprints: "Blueprint-ak errezeta libreak dira (COPYLEFT) — sarrerak (materialak, lan orduak, trebetasunak) eta irteera (ondasun fisiko edo digital bat) — eta edonork forka ditzake.", - industryRulesBuilds: "Build bat ekoizpen agindua da: PROPOSATUA → ONARTUA (botoz) → HORNITZEN → EKOIZPENEAN → OSATUA, eta onartu ondoren soilik onartzen ditu ekarpenak.", - industryRulesContributions: "Kideek lana (orduak), materialak (adierazitako ECO balioa) edo ECO ematen diote build bati; ekarpen bakoitzak karma irabazten du.", - industryRulesLaborRate: "Lan tarifa Fabrika honetan lan ordu baten ECO balioa da; orduak karma bihurtzen ditu ahalegina eta kapitala modu justuan konparatzeko: karma = orduak × tarifa + material balioa + ECO.", - industryRulesShares: "Build batean zure kuota zure karma ÷ karma guztia da, eta irteera balioaren zenbat jasotzen duzun zehazten du.", - industryRulesDistribution: "Ekoizpen bat amaitzean, stewardak pota (ekoizpenaren funtsak gehi salmenta balioa) banatzen du ekarpena egin dutenen artean, beren partaidetzaren arabera.", - industryRulesTreasury: "ECO ekarpenak arduradunak gordetzen ditu build-aren diruzaintza gisa banaketa arte; mugimendu guztiak sarean erregistratzen dira eta gatazkak Courts-era joan daitezke.", - industryJobs: "Lanpostuak", - industryReportButton: "Salatu", - industryCreateTask: "Sortu zeregina", - industryOpenDispute: "Ireki auzia", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "Sortu gonbidapena", - industryPauseVotes: "Egoera botoak", - industryTitle: "Industria", - industryDescription: "Ekoizpen bideak modu kolektiboan kudeatzeko modulua.", - typeIndustryBuild: "BUILD", - typeIndustryBlueprint: "BLUEPRINT", - typeIndustryAllocation: "BANAKETA", - typeIndustry: "INDUSTRIA", - modulesIndustryLabel: "Industria", - modulesIndustryDescription: "Ekoizpen bideak modu kolektiboan kudeatzeko modulua.", - industryFilterAll: "GUZTIAK", - industryFilterMember: "KIDEA", - industryFilterBlueprints: "PLANOAK", - industryFilterBuilds: "EKOIZPENAK", - industryFilterMine: "NIREAK", - industryFilterActive: "MARTXAN", - industryFilterPaused: "ETENDA", - industryFilterDissolved: "DESEGINDA", - industryAllTitle: "Industria", - industryMemberTitle: "Kide naizen fabrikak", - industryMineTitle: "Nire fabrikak", - industryActiveTitle: "Martxan", - industryPausedTitle: "Etendako fabrikak", - industryDissolvedTitle: "Desegindako fabrikak", - industryNoFacilitiesFound: "Ez da fabrikarik aurkitu.", - industryCreateFacility: "Sortu fabrika", - industryMembers: "Kideak", - industryMemberBadge: "KIDEA", - industryName: "Izena", - industryNamePlaceholder: "Fabrikaren izena", - industryDescriptionLabel: "Deskribapena", - industryDescriptionPlaceholder: "Zer ekoizten du fabrika honek?", - industrySector: "Sektorea", - industryMembershipPolicy: "Kidetza politika", - industryQuorum: "Quoruma", - industryMajority: "Gehiengoa", - industryLaborRate: "Lan tarifa", - industryCreateButton: "Sortu fabrika", - industryUpdateButton: "Eguneratu", - industrySteward: "Arduraduna", - industryStatusActive: "MARTXAN", - industryStatusPaused: "ETENDA", - industryStatusDissolved: "DESEGINDA", - industryStatusLabel: "Egoera", - industryJoinButton: "Elkartu", - industryApplyButton: "Eskatu sartzea", - industryLeaveButton: "Irten", - industryInviteButton: "Gonbidatu", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "Gonbidapena behar duzu elkartzeko.", - industryPauseButton: "Bozkatu gelditzea", - industryResumeButton: "Bozkatu berrekitea", - industryDeleteButton: "Ezabatu", - industryPendingApplicants: "Zain dauden eskaerak", - industryVotesYes: "bai", - industryVotesNo: "ez", - industryAlreadyVoted: "bozkatuta", - industryAdmit: "Onartu", - industryReject: "Ukatu", - industryDissolveTitle: "Desegin fabrika", - industryDissolveVote: "Bozkatu desegitea", - industryPolicy_open: "Irekia", - industryPolicy_vote: "Botoz", - industryPolicy_invite: "Gonbidapenez soilik", projectsTitle: "Proiektuak", projectsDescription: "Sortu, finantzatu eta jarraitu komunitateak gidatutako proiektuak zure sarean.", - projectSearchPlaceholder: "Bilatu proiektuak...", projectCreateProject: "Proiektu Bat Sortu", projectCreateButton: "Proiektu Bat Sortu", projectUpdateButton: "EGUNERATU", @@ -2810,8 +2943,14 @@ module.exports = { projectPledgeTitle: "Proiektua babestu", projectPledgePlaceholder: "Kantitatea ECOtan", projectBounties: "Saria", + projectBountiesInputLabel: "Sariak (lerro bakoitzean: Izenburua|Kantitatea [ECO]|Deskribapena)", + projectBountiesPlaceholder: "UI akatsa konpondu|100|Arazoaren esteka\nDokumentuak idatzi|250|Erabilera adibideak", projectNoBounties: "Ez da sariarik aurkitu.", projectTitle: "Izenburua", + projectAddBountyTitle: "Saria berri bat gehitu", + projectBountyTitle: "Sariaren izenburua", + projectBountyAmount: "Kantitatea (ECO)", + projectBountyDescription: "Deskribapena", projectMilestoneSelect: "Hitoa aukeratu", projectBountyCreateButton: "Saria sortu", projectBountyStatus: "Sariaren egoera", @@ -2826,15 +2965,19 @@ module.exports = { projectMilestoneTitle: "Hitoaren izenburua", projectMilestoneTargetPercent: "Portzentajea (%)", projectMilestoneDueDate: "Data", + projectMilestoneCreateButton: "Hitoa sortu", projectMilestoneStatus: "Hitoaren egoera", projectMilestoneOpen: "irekia", projectMilestoneDone: "osatua", projectMilestoneDue: "egonkor", + projectNoMilestones: "Ez da hitorik aurkitu.", projectMilestoneMarkDone: "Markatu amaitutako bezala", projectMilestoneTitlePlaceholder: "Hitoaren izenburua sartu", projectMilestoneDescriptionPlaceholder: "Sartu deskribapena", projectMilestoneDescription: "Hitoaren deskribapena", + projectBudgetGoal: "Aurrekontua (Helburua)", projectBudgetAssigned: "Sarietara esleituta", + projectBudgetRemaining: "Geratzen dena", projectBudgetOver: "⚠ Aurrekontua gaindituta: esleitutako zenbatekoak helburua gainditzen du", projectFollowers: "Jarraitzaileak", projectFollowersTitle: "Jarraitzaileak", @@ -2847,6 +2990,7 @@ module.exports = { projectBackersTotalPledged: "Ekarpenen guztizkoa", projectBackersYourPledge: "Zure ekarpena", projectBackersNone: "Oraindik ez dago ekarpenik.", + projectNoRemainingBudget: "Ez da aurrekonturik geratzen.", projectFilterApplied: "ESKATUTA", projectAppliedTitle: "ESKATUTA", projectFilterBackers: "BABESLEAK", @@ -2855,15 +2999,30 @@ module.exports = { projectBackerAmount: "Ekarpen osoa", projectBackerPledges: "Ekarpenak", projectBackerProjects: "Proiektuak", + projectPledgeAmount: "Kantitatea", projectSelectMilestoneOrBounty: "Hegoa edo Saria hautatu", projectPledgeButton: "Ekimen egin", footerLicense: "GPLv3", + footerPackage: "Paketea", + footerVersion: "Bertsioa", modulesModuleName: "Izena", modulesModuleDescription: "Deskribapena", modulesModuleStatus: "Egoera", modulesTotalModulesLabel: "Kargatutako Moduluak", modulesEnabledModulesLabel: "Gaituta", modulesDisabledModulesLabel: "Desgaituta", + modulesPopularLabel: "Nabarmenak", + modulesPopularDescription: "Gehien ikusi edo komentatu diren mezu ezagunak jasotzeko modulua.", + modulesTopicsLabel: "Gaiak", + modulesTopicsDescription: "Interes partekatutako kategorien araberako eztabaidak jasotzeko modulua.", + modulesSummariesLabel: "Laburpenak", + modulesSummariesDescription: "Eztabaida edo mezu luzeen laburpenak jasotzeko modulua.", + modulesLatestLabel: "Azkenak", + modulesLatestDescription: "Mezu eta eztabaida berrienak jasotzeko modulua.", + modulesThreadsLabel: "Hariak", + modulesThreadsDescription: "Gai edo galdera baten inguruko elkarrizketak jasotzeko modulua.", + modulesMultiverseLabel: "Multibertsoa", + modulesMultiverseDescription: "Beste federatutako kideetatik edukia jasotzeko modulua.", modulesInvitesLabel: "Gonbidapenak", modulesInvitesDescription: "Gonbidapen-kodeak kudeatu eta aplikatzeko modulua.", modulesWalletLabel: "Zorroa", @@ -2908,12 +3067,8 @@ module.exports = { modulesOpinionsDescription: "Iritziak aurkitu eta bozkatzeko modulua.", modulesTransfersLabel: "Transferentziak", modulesTransfersDescription: "Kontratu adimendunak (transferentziak) aurkitu eta kudeatzeko modulua.", - modulesFediverseLabel: "Multibertsoa", - modulesBlogsLabel: "Blogak", - modulesBlogsDescription: "Blogak aurkitzeko eta kudeatzeko modulua.", - modulesPollsLabel: "Inkestak", - modulesPollsDescription: "Sareari galdetzeko eta erantzunak zenbatzeko modulua.", - modulesFediverseDescription: "Kudeatu zure beste multibertso kontuak, edukia bidaltzea eta jasotzea barne.", + modulesFediverseLabel: "Fediverse", + modulesFediverseDescription: "Kudeatu zure beste fediverso kontuak, edukia bidaltzea eta jasotzea barne.", modulesFeedLabel: "Jarioa", modulesFeedDescription: "Testu laburrak (jarioak) aurkitu eta partekatzeko modulua.", modulesPixeliaLabel: "Pixelia", @@ -2945,41 +3100,41 @@ module.exports = { visibilityMakeHidden: "Ezkutatu", invitesInhabitantsTitle: "Biztanleak", invitesInhabitantsFollow: "Jarraitu", - aiNavPlaceholder: "Deskribatu zer behar duzun...", + aiNavPlaceholder: "Nora joan nahi duzu?", aiNavDisabled: "AI nabigazioa desgaituta dago.", - aiNavResultsTitle: "AINav iradokizunak", + aiNavResultsTitle: "AI nabigazio iradokizunak", aiNavQueryLabel: "Galdera", + aiNavResultsDescription: "Aukeratu zure bilaketarekin bat datorren ibilbidea.", + aiNavResultPath: "Ibilbidea", + aiNavResultDescription: "Zer egin dezakezu", aiNavResultMatch: "Bat-egitea", aiNavResultsEmpty: "Ez dago bat datorren ibilbiderik. Saiatu /search.", + aiNavSearchInstead: "Bilatu beste modu batez", modulesAINavLabel: "AINav", modulesAINavDescription: "Sareko edukiari buruzko hizkuntza naturalezko galderak egiteko modulua.", + directConnect: "Zuzeneko Konexioa", directConnectDescription: "Konektatu zuzenean parekide batera bere IP helbidea, portua eta gako publikoa sartuz. Parekidea jarraipen-konexio gisa gehituko da.", peerHost: "IP", peerPort: "Portua (lehenetsia: 8008)", peerPublicKey: "Gako Publikoa (@...ed25519)", connectAndFollow: "Konektatu", + deviceSourceLabel: "Gailua", modulesPresetTitle: "Konfigurazio Arruntak", - workflowsTitle: "Workflow-ak", - workflowsDescription: "Workflow batek gaia eta zein modulu dauden aktibo finkatzen ditu.", - workflowsSet: "Workflow-a ezarri", - workflow_default: "Lehenetsia", - workflow_jobs: "Lana", - workflow_ruling: "Gobernua", - workflow_politics: "Politika", - workflow_social: "Soziala", - workflow_business: "Negozioak", - workflow_night: "Gaua", - workflow_mobile: "Mugikorra", + modulesPreset_minimal: "Gutxienekoa", + modulesPreset_basic: "Oinarrizkoa", + modulesPreset_social: "Soziala", + modulesPreset_economy: "Ekonomia", + modulesPreset_full: "Osoa", statsCarbonFootprintTitle: "Karbono Aztarna", + statsCarbonFootprintNetwork: "Sarearen karbono aztarna", + statsCarbonFootprintYours: "Zure karbono aztarna", statsCarbonTombstone: "Tombstoning-aren aztarna", + feedSuccessMsg: "Feeda arrakastaz argitaratu da!", + dominantOpinionLabel: "Iritzi nagusia", uploadMedia: "Multimedia igo (max: 50MB)", reportsWhyInappropriateLabel: "Zergatik da desegokia?", - visitContent: "Bisitatu Edukia", - favoriteAdd: "Ainguratu Gogokoetan", - pmContentTooltip: "Bidali Mezu Pribatua", - favoriteRemove: "Kendu Gogokoetatik", - reportContent: "Salatu Edukia", + visitContent: "Edukia bisitatu", bankEcoinHours: "ECOin Orduak", mapsLabel: "Mapak", mapTitle: "Mapak", @@ -3007,13 +3162,14 @@ module.exports = { mapDescriptionLabel: "Deskribapena", mapDescriptionPlaceholder: "Deskribatu mapa edo kokapena...", mapTypeLabel: "Mapa mota", - mapInviteMode: "Gonbidapena", - mapTypeSingle: "BAKARRA", - mapTypeOpen: "IREKIA", - mapTypeClosed: "ITXIA", + mapTypeSingle: "BAKARRA (hasierako kokapena soilik)", + mapTypeOpen: "IREKIA (edonork gehitu ditzake markatzaileak)", + mapTypeClosed: "ITXIA (sortzaileak soilik gehitzen ditu markatzaileak)", mapTagsLabel: "Etiketak", mapTagsPlaceholder: "Sartu etiketak komaz bereizita", mapUrlLabel: "Maparen URLa", + mapPickCoordLabel: "Edo hautatu kokapen bat sarean:", + mapMarkersLabel: "markatzaileak", mapMarkersTitle: "Markatzaileak", mapMarkerDefault: "Markatzailea", mapMarkerLatLabel: "Markatzailearen latitudea", @@ -3040,9 +3196,14 @@ module.exports = { padTitle: "Pad", modulesPadsLabel: "Padak", modulesPadsDescription: "Testu editoreak lankidetzan kudeatzeko modulua.", + padFilterAll: "GUZTIAK", + padFilterMine: "NIREA", + padFilterRecent: "BERRIA", + padFilterOpen: "IREKIA", + padFilterClosed: "ITXIA", padCreate: "Pad Sortu", - padUpdate: "Eguneratu", - padDelete: "Ezabatu", + padUpdate: "Pad Eguneratu", + padDelete: "Pad Ezabatu", padTitleLabel: "Izenburua", padTitlePlaceholder: "Idatzi pad-aren izenburua...", padStatusLabel: "Egoera", @@ -3053,7 +3214,14 @@ module.exports = { padTagsLabel: "Etiketak", padTagsPlaceholder: "etiketa1, etiketa2, ...", padMembersLabel: "Kideak", + padVisitPad: "Pad Bisitatu", + padShareUrl: "URL Partekatu", padCreated: "Sortua", + padAuthor: "Egilea", + padGenerateCode: "Kodea Sortu", + padInviteCodeLabel: "Gonbidapen Kodea", + padInviteCodePlaceholder: "Sartu gonbidapen kodea...", + padValidateInvite: "Balioztatu", padStartEditing: "EDITZEN HASI!", padEditorPlaceholder: "Idazten hasi...", padSubmitEntry: "Bidali", @@ -3066,11 +3234,12 @@ module.exports = { padClosedSectionTitle: "Pad Itxiak", padCreateSectionTitle: "Pad Berria Sortu", padUpdateSectionTitle: "Pad Eguneratu", + padInviteGenerated: "Gonbidapen Kodea Sortua", typePad: "PADAK", padNew: "BERRIA", padAddFavorite: "Gogokoen Gehitu", padRemoveFavorite: "Gogokoetatik Kendu", - padClose: "Itxi", + padClose: "Itxi Pad", padBackToEditor: "Editorera itzuli", padSearchPlaceholder: "Bilatu padak...", @@ -3078,6 +3247,8 @@ module.exports = { modulesChatsDescription: "Txat zifratu publikoak aurkitu eta kudeatzeko modulua.", typeChat: "TXATAK", typeChatMessage: "TXAT MEZUA", + chatLabel: "TXATAK", + chatMessageLabel: "TXAT MEZUAK", chatsTitle: "Txatak", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3087,50 +3258,52 @@ module.exports = { chatDescription: "Deskribapena", chatCategory: "Kategoria", chatStatus: "EGOERA", + chatFilterAll: "GUZTIAK", + chatFilterMine: "NIREA", + chatFilterRecent: "BERRIA", + chatFilterFavorites: "GOGOKOAK", + chatFilterOpen: "IREKIA", + chatFilterClosed: "ITXIA", chatCreate: "Txata Sortu", - chatUpdate: "Eguneratu", - chatDelete: "Ezabatu", + chatUpdate: "Txata Eguneratu", + chatDelete: "Txata Ezabatu", chatClose: "Txata Itxi", chatVisitChat: "TXATA IKUSI", chatUntitled: "Izenburugabeko Txata", chatNoItems: "Ez da txatarik aurkitu.", chatParticipants: "Parte-hartzaileak", - chatMessagesLabel: "Mezuak", - chatThreadsLabel: "Hariak", + chatStartChatting: "TXATEATZEN HASI!", + chatGenerateCode: "Kodea Sortu", + chatShareUrl: "URLa Partekatu", chatCreatedAt: "SORTUA", chatSearchPlaceholder: "Txatak bilatu...", chatStatusOpen: "IREKIA", chatStatusInviteOnly: "GONBIDAPENEZ BAKARRIK", chatStatusClosed: "ITXIA", chatSendMessage: "Bidali", - chatJumpLatest: "Azken Mezua", - chatPickChat: "Aukeratu txat bat irekitzeko", - chatNoneYet: "Oraindik ez dago txatik eskuragarri.", - activitySearchPlaceholder: "Bilatu jardueran...", - trendingSearchPlaceholder: "Bilatu joeretan...", - opinionsSearchPlaceholder: "Bilatu iritzietan...", - votesSearchPlaceholder: "Bilatu bozketetan...", - welcomeStepUxTitle: "Aukeratu zure interfazea", - welcomeStepUxText: "Erabaki Oasis nolakoa den. Gero ezarpenetan alda dezakezu.", - welcomeStepUxAction: "Erabili ikuspegi hau", - chatRateLimitTitle: "Mezu gehiegi", - chatRateLimitMessage: "Gela honek ordu batean onartzen duen mezu mugara iritsi zara. Saiatu geroago.", chatMessagePlaceholder: "Idatzi zure mezua...", chatNoMessages: "Oraindik mezurik ez.", + chatLeave: "Txata Utzi", + chatInviteCodeLabel: "Gonbidapen kodea sartu", + chatJoinByInvite: "Sartu", chatTitlePlaceholder: "Txataren izenburua", chatDescriptionPlaceholder: "Txataren deskribapena", chatTagsPlaceholder: "etiketa1, etiketa2", chatImageLabel: "Aukeratu irudi-fitxategi bat (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "Gonbidapen Kodea", chatAuthor: "Egilea", + chatCreated: "Sortua", chatAddFavorite: "Gogokoen artean gehitu", chatRemoveFavorite: "Gogokoenetatik kendu", chatPM: "MP", chatStatusLabel: "Egoera", chatCategoryLabel: "Kategoria", + chatParticipantsLabel: "Parte-hartzaileak", gamesTitle: "Jokoak", gamesDescription: "Aurkitu eta jokatu joko batzuk.", gamesFilterAll: "GUZTIAK", gamesPlayButton: "JOLASTU!", + gamesBackToGames: "Jokoetara itzuli", modulesGamesLabel: "Jokoak", modulesGamesDescription: "Jokoak aurkitzeko eta jolasteko modulua.", modulesGraphosLabel: "Graphos", @@ -3147,6 +3320,8 @@ module.exports = { gamesArkanoidDesc: "Hautsi adreilu guztiak zure pala eta pilotarekin. Arcade klasiko bat.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Ping-pong klasikoa IA baten aurka. Lehenak 5 puntu lortzen duena irabazten du.", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "Denboraren aurka lasterketa! Saihestu trafikoa eta iritsi helmugara garaiz.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Gidatu zure ontzia asteroide eremuaren bidez. Tiro egin haiek jo aurretik.", gamesRockPaperScissorsTitle: "Harria Paper Artaziak", @@ -3167,6 +3342,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3177,6 +3354,12 @@ module.exports = { modulesCalendarsLabel: "Egutegiak", modulesCalendarsDescription: "Egutegiak aurkitu eta kudeatzeko modulua.", typeCalendar: "EGUTEGIAK", + calendarFilterAll: "GUZTIAK", + calendarFilterMine: "NIREAK", + calendarFilterOpen: "IREKIA", + calendarFilterClosed: "ITXIA", + calendarFilterRecent: "BERRIAK", + calendarFilterFavorites: "GOGOKOAK", calendarCreate: "Egutegia sortu", calendarUpdate: "Eguneratu", calendarDelete: "Ezabatu", @@ -3189,17 +3372,24 @@ module.exports = { calendarTagsLabel: "Etiketak", calendarTagsPlaceholder: "etiketa1, etiketa2...", calendarParticipantsLabel: "Parte-hartzaileak", + calendarParticipantsCount: "Parte-hartzaileak", + calendarVisitCalendar: "Egutegia bisitatu", calendarCreated: "Sortua", + calendarAuthor: "Egilea", calendarJoin: "Batu", calendarGenerateInvite: "Sortu gonbidapena", + calendarInviteCodePlaceholder: "Sartu gonbidapen-kodea...", + calendarValidateInvite: "Egiaztatu kodea", + calendarJoined: "Batuta", + calendarAddDate: "Data gehitu", + calendarAddNote: "Oharra gehitu", calendarDateLabel: "Data", calendarDatePlaceholder: "Data hau deskribatu...", - calendarNoteLabel: "Oharrak", + calendarNoteLabel: "Oharra", calendarNotePlaceholder: "Oharra gehitu...", calendarFirstDateLabel: "Data", calendarFirstNoteLabel: "Oharrak", calendarIntervalLabel: "Interval", - calendarIntervalNone: "Errepikapenik gabe", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3210,6 +3400,8 @@ module.exports = { calendarsDescription: "Aurkitu eta kudeatu zure sareko egutegiak.", calendarMonthPrev: "← Aurrekoa", calendarMonthNext: "Hurrengoa →", + calendarMonthLabel: "Datak", + calendarsShareUrl: "Partekatzeko URLa", calendarAllSectionTitle: "Egutegiak", calendarRecentSectionTitle: "Egutegi Berriak", calendarFavoritesSectionTitle: "Gogokoak", @@ -3223,6 +3415,7 @@ module.exports = { calendarRemoveFavorite: "Gogokoetatik kendu", calendarSearchPlaceholder: "Egutegiak bilatu...", calendarAddEntry: "Gehitu Sarrera", + calendarLeave: "Irten Egutegitik", statsCalendar: "Egutegiak", statsCalendarDate: "Egutegi datak", statsCalendarNote: "Egutegi oharrak", @@ -3269,6 +3462,7 @@ module.exports = { torrentSortOldest: "Zaharrenak", torrentSortTop: "Bozkatu enak", torrentNoMatch: "Ez dago torrent bat datorrenik.", + torrentMessageAuthorButton: "Mezua Bidali Egileari", torrentUpdatedAt: "Eguneratua", statsTorrent: "Torrentak", typeTorrent: "TORRENTAK", @@ -3276,18 +3470,10 @@ module.exports = { modulesTorrentsDescription: "Torrentak aurkitu eta kudeatzeko modulua.", favoritesFilterTorrents: "TORRENTAK", favoritesFilterMarket: "MERKATUA", - favoritesFilterEvents: "EKITALDIAK", - favoritesFilterForum: "FOROA", - favoritesFilterHousing: "ETXEBIZITZA", - favoritesFilterJobs: "LANPOSTUAK", - favoritesFilterProjects: "PROIEKTUAK", - favoritesFilterReports: "TXOSTENAK", - favoritesFilterTasks: "ZEREGINAK", - favoritesFilterTransfers: "TRANSFERENTZIAK", - favoritesFilterVotes: "BOZKETAK", favoritesFilterShopProducts: "DENDAKO ARTIKULUAK", tribeSectionTorrents: "TORRENTAK", tribeCreateTorrent: "Torrenta Igo", + tribeMediaTypeTorrent: "Torrenta", settingsWishTitle: "Nahia", settingsWishDesc: "Konfiguratu zure Avatarraren nahia.", settingsWishWhole: "Multiverse", @@ -3298,10 +3484,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "Bidalketa UX babes bat da. Jasotzea arreta-iragazkia.", + pmBlockedNonMutual: "Elkarrekiko laguntza duten biztanleei bakarrik bidal diezaiekezu.", inhabitantsPendingFollowsTitle: "Onespen zain dauden eskaerak", inhabitantsPendingAccept: "Onartu", inhabitantsPendingReject: "Ukatu", + bxEncrypted: "ZIFRATUA", + bxEncryptedHexLabel: "Testu zifratua (aurrebista)", tribeSectionGovernance: "GOBERNANTZA", + tribeSubStatusPublic: "PUBLIKOA", + tribeSubStatusPrivate: "PRIBATUA", + tribeSubInheritedPrivate: "PRIBATUA (tribu nagusitik heredatua)", tribePadInviteRequired: "Ez duzu pada atzipena. Eskatu gonbidapena.", tribeChatInviteRequired: "Ez duzu txata atzipena. Eskatu gonbidapena.", tribeGovernanceDesc: "Tribu honen barne gobernantza.", @@ -3341,31 +3533,44 @@ module.exports = { tribeGovNoHistorical: "Oraindik ez dago erregistrorik", logsTitle: "Egunkaria", logsDescription: "Erregistratu zure esperientzia sarean.", + logsReadMore: "Gehiago irakurri", logsViewTitle: "Egunkaria", logsColumnType: "Mota", logsView: "Ikusi", + logsManualPrompt: "Idatzi zure egunkaria", logsUpdateButton: "Eguneratu", - logsEditTitle: "Eguneratu erregistroa", + logsEditTitle: "Sarrera editatu", + logsCreateDescription: "Erregistratu zure esperientzia...", logsCreateTitle: "Sarrera sortu", logsWriteButton: "Idatzi", logsTextPlaceholder: "Deskribatu zure esperientziak...", + logsTextField: "Testua", + logsLabelPlaceholder: "Etiketa...", + logsLabelField: "Idatzi zure egunkaria (etiketa)", + logsAiDisabledWarn: "Gaitu AA modulua /modules-en AA-idatzitako sarrerak erabiltzeko.", + logsAiContextValue: "Blockchain-eguna", + logsAiContext: "Testuingurua", + logsAiModStatus: "AImod", + logsModeApply: "Aplikatu!", logsModeAIWritten: "AI-Assistant", logsModeManual: "Eskuz", logsModeAI: "AA", + logsColumnDelete: "Ezabatu", + logsColumnEdit: "Editatu", logsColumnLog: "Egunkaria", logsColumnDate: "Data", logsDelete: "Ezabatu", - logsEdit: "Eguneratu", + logsEdit: "Editatu", logsFilterAlways: "BETI", logsFilterYear: "AZKEN URTEA", logsFilterMonth: "AZKEN HILABETEA", logsFilterWeek: "AZKEN ASTEA", logsFilterToday: "GAUR", - logsComments: "Iruzkinak", - logsAuthorEntries: "Sarrerak", + logsFilterRecent: "BERRIAK", + logsFilterAll: "DENAK", logsCreate: "Sarrera sortu", logsExport: "Egunkaria esportatu", - logsExportOne: "Esportatu erregistroa", + logsExportOne: "Esportatu", logsViewDetails: "Ikusi xehetasunak", logsGenerateButton: "Sortu testua", logsSearchText: "Bilatu egunkarietan...", @@ -3375,25 +3580,31 @@ module.exports = { modulesLogsLabel: "Egunkaria", modulesLogsDescription: "Modulua zure esperientziak (AA laguntzailearen bidez) erregistratzeko.", statsLogsTitle: "Egunkaria", + statsLogsEntries: "Sarrerak", typeLog: "EGUNKARIA", + blockchainCycle: "Zikloa", larpTitle: "L.A.R.P.", larpDescription: "Esperimentazio kolaboratiborako zuzeneko rol-joko geruza bat.", - larpNoHousesMatch: "Ez dago bilaketarekin bat datorren etxerik.", - larpSearchPlaceholder: "Bilatu etxeak...", larpAllHouses: "Etxeak:", larpFilterRuling: "AGINTZEN", larpFilterHouses: "ETXEAK", larpFilterRules: "FAQ", larpRulesTitle: "L.A.R.P. FAQ", + larpRulesEntryTitle: "Hasierako etxea", larpRulesEntryText: "Bizilagun bakoitza ACADEMIA-n hasten da lehenespenez. ACADEMIA-tik, aukeratu etxe bat \"Etxe batera sartu\" panelean: horrek sarrera-proba irekitzen du. Erantzun 10 galderak eta bidali. Atalasea (7/10) gainditzen baduzu, automatikoki onartzen zaitu etxe horretan; bestela, baztertuta gelditzen zara eta ACADEMIA-n geratzen zara. Bietan, sistemak zure OasisID eta saiakera-zikloa erregistratzen ditu eta beste saiakera bat eragozten du 30 zikloko itxaronaldia igaro arte.", + larpRulesLeaveText: "Edozein etxetako kideek ACADEMIA-ra itzul daitezke noiznahi beren etxe-orritik; horrek kidetza berrabiarazten du, baina ez du saiakera-itxaronaldia indargabetzen.", + larpRulesGovernanceTitle: "Gobernu-horma", larpRulesGovernanceText: "Bere zikloan, etxe agintariak \"Agintzen\" intsignia darama eta bere horma da soilik AGINTZEN fitxapean publikoki erakusten dena.", - larpRulesWallText: "Etxe bakoitzak Horma bat du, non bere kideek idazten duten. Etxe baten Horma pribatua da, agintean dagoen etxearena eta ACADEMIArena izan ezik, edonork irakur baititzake.", + larpRulesSignTitle: "L.A.R.P. Ikurra", larpRulesSignText: "Bizilagunek aktiba dezakete (/profile/edit > Sentsoreak) beren uneko etxearen irudia zigilu gisa erakusteko profilean, egile- eta bizilagun-txartelean. Zigiluak etxearen orrira eramaten du.", larpPostsTitle: "Horma", larpPostsEmpty: "Oraindik ez dago argitalpenik.", + larpPostLabel: "Argitalpen berria", larpPostPlaceholder: "Zer du etxe honek esateko?", + larpPostPublic: "Publikoa (edonork ikusgai, bestela kideek soilik)", larpPostSubmit: "Argitaratu", + larpPostMembersOnly: "Kideek soilik", larpCycleLabel: "Zikloa:", audioBackToBcs: "Back to BCS", audioFilterBcs: "BCS", @@ -3513,9 +3724,12 @@ module.exports = { bankTaxesLookupNote: "Sartu bloke-ID bat zure jario lokalean bilatzeko.", bankTaxesUserNoteChipsClick: "Egin klik edozein chiptan bere blokea blockexplorer-en ikuskatzeko.", mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntry: "Gonbidapen kodea", larpRulesInviteEntryText: "Etxeko kideek gonbidapen kodeak sor ditzakete, etxerako sarrera zuzena ematen dutenak.", larpRulesOneHouseText: "Une bakoitzean etxe bakar batekoa izan zaitezke gehienez. Inori ez baduzu, ACADEMIA-n zaude. ACADEMIA ez den etxeko orri bakoitzak \"Etxetik atera\" botoia erakusten die kideei, kidetza ACADEMIA-ra berrezartzen duena. Ateratzea EZ du proba-itxaronaldia indargabetzen.", - larpRulesTestEntryText: "WILL testean aukera bakoitzak etxe bat edo gehiago haztatzen ditu; bidaltzean, sistemak puntuazioak batzen ditu eta puntu gehien dituen etxean onartzen zaitu automatikoki.", + larpRulesOneHouseTitle: "Etxe bana aldi bakoitzean", + larpRulesTestEntry: "WILL proba", + larpRulesTestEntryText: "Aukera bakoitzak etxe bati edo gehiagori pisua ematen die; bidaltzean, sistemak puntuak batzen ditu eta puntuazio handiena duen etxean automatikoki onartzen zaitu.", larpWillTestHint: "Behin osatutakoan, zure erantzunekin ondoen bat datorren etxera esleituko zaitu. Zure erantzun indibidualak lokalki prozesatzen dira eta inoiz ez dira gordetzen — soilik etxe-esleipenaren emaitza argitaratzen da zure jarioan.", larpWillTestTitle: "WILL proba", settingsLanDesc: "Aldian behin iragarri pareko hau sare lokal bereko beste Oasis instantziei. Desgaitu UDP igorpenak gelditzeko.", @@ -3534,27 +3748,31 @@ module.exports = { aiExchangeMarkHelpful: "+1 lagungarri", larpCyclesUntilRuling: "Hurrengo gobernu-zikloa", larpVisitTribe: "Tribua bisitatu", - larpStartJourney: "Hasi bidaia", - larpGoverning: "Agintean:", - larpStartHere: "Hasi hemen:", + larpGoverning: "Agintzen:", larpMyHouse: "Nire Etxea:", larpMembersCount: "Kideak", + larpMembersTitle: "Kideak", + larpNoMembers: "Oraindik ez dago kiderik.", larpRolesLabel: "Rolak", larpFunctionLabel: "Funtzioa", larpMonthLabel: "Gobernu-zikloa", larpLeaveToAcademia: "Itzuli ACADEMIA-ra", - larpAcademiaJoinTitle: "L.A.R.P. Etxeak", + larpAcademiaJoinTitle: "Etxe batera sartu", + larpAcademiaJoinHint: "Egin profil psikologikoaren proba zuri ondoen egokitzen zaizun etxera esleitua izateko, edo erabili etxeko kide batek partekatutako gonbidapen-kodea.", + larpTakeTestButton: "Profil-proba egin", larpInviteRedeem: "Etxera sartu", larpInviteCreate: "Gonbidapen-kodea sortu", larpInvitePlaceholder: "Gonbidapen-kodea", larpInviteBannerTitle: "Gonbidapen-kode berria", larpInviteBannerHint: "Partekatu kode hau ACADEMIA-n dagoen norbaitekin. 30 zikloko edo erabilera bakar baten ostean iraungitzen da.", + larpTestAssignedHouse: "Esleitutako etxea", larpTestRankingTitle: "Etxe bakoitzeko puntuazioa", invitesHousesTitle: "L.A.R.P.", invitesHouseInviteCodePlaceholder: "Etxearen gonbidapen-kodea", invitesHouseJoinButton: "Etxera sartu", larpLastAttemptTitle: "Zure azken saiakera", larpLastAttemptHouse: "Etxea", + larpLastAttemptResult: "Emaitza", larpLastAttemptWhen: "Noiz", larpLastAttemptCooldown: "Hurrengo saiakera", larpTestTitle: "Sarrera-proba", @@ -3563,14 +3781,39 @@ module.exports = { larpTestCooldownActive: "Hurrengo proba erabilgarri", larpTestCooldownDays: "ziklo", larpTestResultTitle: "Probaren emaitza", + larpTestPassed: "Proba gainditua — ongi etorri!", + larpTestFailed: "Proba ez da gainditu", larpTestScore: "Puntuazioa", larpTestNextAttempt: "Beste proba bat egin dezakezu 30 ziklo barru.", larpGoToHouse: "Joan zure etxera", larpBackToAcademia: "Itzuli ACADEMIA-ra", + larpBackToHouses: "◀ Etxeak", larpBadgeYou: "Zu", larpBadgeRuling: "Agintzen", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Oasis-en zuzeneko rol-joko geruzaren modulua (9 Etxeak).", + melodyUploadTitlePlaceholder: "Composition title", + melodyUploadDescPlaceholder: "Optional description", + ecoinTaxLabel: "ECOin Tax", + bankTaxesNetworkTitle: "Network Taxes", + bankTaxesUserTitle: "Your taxes", + bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", + bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", + bankTaxesExample: "Example", + bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", + statsEcoTaxTitle: "ECO Tax", + audioTranscodeTitle: "BCS Transcode", + audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", + audioTranscodeStegoTitle: "Embedded in the waveform", + audioBackToAudio: "Back to audio", + audioAuthorLabel: "Author", + courtsEvidenceFileLabel: "Evidence file (image, audio, video or PDF)", + courtsCaseMediators: "Mediators", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + mapClickToCreate: "Click on the map to select a location", + mapClickToAddMarker: "Click on the map to add a marker", + gameScoreLabel: "GAME SCORES", larpProfileQ1: "Arazo konplexu baten aurrean, lehenik zer egiten duzu?", larpProfileQ1O1: "Besteekin hitz egin adostasuna lortzeko", larpProfileQ1O2: "Prototipo bat egin probatzeko", diff --git a/src/client/assets/translations/oasis_fr.js b/src/client/assets/translations/oasis_fr.js index 50ccf57..af3da9d 100644 --- a/src/client/assets/translations/oasis_fr.js +++ b/src/client/assets/translations/oasis_fr.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { fr: { + bbAdd: "Ajouter", + bbQuickActions: "Actions rapides", + bbPickTitle: "Épingler un module", + bbPickDesc: "Choisis un module pour l'emplacement + de la barre inférieure.", + bbPickNone: "Aucun", + bbManage: "Modifier", + bbYourBar: "Ta barre", + bbRemove: "Retirer", + bbFull: "Maximum atteint", + bbDone: "Terminé", + activityChangeFilter: "Changer de filtre", + emptyConnectHint: "Connecte-toi à un pub pour synchroniser avec le réseau.", + emptyConnectCta: "Se connecter à un pub", + menuCommunity: "Communauté", + activityGroupCommunity: "Communauté et gouvernance", + activityGroupOrg: "Organisation", + activityGroupComm: "Communication", + activityGroupEconomy: "Économie", + activityGroupMedia: "Contenu et médias", + activityGroupOther: "Autres", + languageName: "Français", shopOrderStatusPaid: "Paiement reçu", shopOrderStatusReceived: "Reçu", shopOrderMarkPaid: "Marquer paiement reçu", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "Vous n’avez pas encore d’achats.", shopOrderPmSeller: "PM Seller", shopOrderSeller: "Vendeur", + shopOrderPaymentConcept: "Paiement", shopOrderInvoice: "Facture", shopOrderInvoiceConcept: "Facture", shopOrderStatusPending: "En attente", shopOrderStatusAccepted: "Accepté", shopOrderStatusRejected: "Rejeté", shopOrderStatusShipped: "Expédié", + shopOrderStatusDelivered: "Livré", shopOrderAccept: "Accepter", shopOrderReject: "Rejeter", shopOrderMarkShipped: "Marquer expédié", + shopOrderMarkDelivered: "Marquer livré", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "Contrat", shopOrderContractConcept: "Commande boutique", shopOrdersPending: "Commandes en attente", all: "Tous", @@ -50,13 +75,14 @@ module.exports = { viewJson: "Voir le JSON", matchScore: "Score de correspondance", preferencesLabel: "Préférences", - inhabitantProfileTitle: "Habitant", + inhabitantProfileTitle: "Profil de l’habitant", contentWarningLabel: "Avertissement de contenu", invalidPost: "Contenu invalide", invalidPostHint: "Ce message a un texte vide/invalide.", spreadBy: "Par", spreadChron: "Diffusion", spreaded: "Diffusé", + spreadMore: "plus", spreadContentUnavailable: "Contenu pas encore disponible (réplication en attente)", filteredByTag: "Filtré par étiquette", filteredByTagTitle: "Filtré par étiquette", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "Aucune gravité", calendarDeleteDate: "Supprimer la date", calendarIntervalUntil: "Jusqu’au", + bookmarkCategory: "Catégorie", bookmarkLastVisit: "Dernière visite", profileClearnetBookmarksLabel: "Favoris", + forumFilterHot: "Populaires", invitesPort: "Port", currentlyUnrecheable: "ERREUR !", peerHostValidation: "IPv4 valide (ex. 192.168.1.100) ou nom d’hôte (ex. pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "Estimation max annuelle", statsCarbonNetwork: "Total du réseau", statsCarbonOfEstMax: "de la capacité max estimée", - statsCarbonYearProgress: "de l'année écoulée", - statsNetworkTitle: "Réseau", - statsStorageTitle: "Stockage", - statsEcoTaxTitle: "Taxe ECO", - statsAccountTitle: "Compte", - statsAveragesTitle: "Moyennes", statsCarbonOfNetwork: "du total du réseau", statsCarbonUser: "Votre empreinte", statsContentCountColumn: "Nombre", @@ -154,6 +176,10 @@ module.exports = { statsTopTagsTitle: "Principales étiquettes", statsTopTypesTitle: "Principaux types de contenu", statsTotalMsgs: "Total des messages", + feedViewDetails: "Voir les détails", + eventFileLabel: "Joindre une image", + taskFileLabel: "Joindre une image", + courtsRespondentRequired: "L'identifiant du défendeur est requis", extended: "Multivers", extendedDescription: [ "Lorsque vous soutenez quelqu’un, vous pouvez télécharger les publications des habitants qu’il soutient, et ces publications apparaîtront ici, triées de la plus récente.", @@ -197,22 +223,29 @@ module.exports = { graphosShowing: "Affichage", graphosYou: "Toi", graphosTotalNodes: "Nœuds totaux", + manualMode: "Mode Manuel", mentions: "Mentions", mentionsDescription: [ - strong("Chaque message qui vous @mentionne"), - ".", + strong("Publications qui vous @mentionnent"), + ", triées des plus récentes.", ], + nextPage: "Suivant", + previousPage: "Précédent", noMentions: "Vous n’avez pas encore reçu de @mentions.", - mentionsSearchPlaceholder: "Rechercher des mentions...", privateReminders: "RAPPELS", inboxFiltersLabel: "Filtres :", inboxToggleToMutuals: "Passer à mutuels", inboxToggleToWhole: "Passer à tous", + privateFrom: "De", + privateTo: "Pour", privateDate: "Date", peers: "Nœuds", searchDescription: "Description", + imageSearch: "Rechercher des images", searchFromLabel: "De", searchToLabel: "À", + searchMinOpinionsLabel: "Min opinions", + searchInhabitantLabel: "Habitant (Oasis ID)", searchQueryLabel: "Recherche", searchPerPageLabel: "Résultats par page", settings: "Configuration", @@ -224,38 +257,67 @@ module.exports = { melodyTitle: 'Melody', melodyDescription: 'Joue la mélodie de ta blockchain — chaque bloc devient une note.', melodyEmpty: 'Pas encore de notes — ta blockchain n’a pas encore produit de bloc.', + melodyCompositionHelp: 'Chaque bloc de ta blockchain devient une note musicale selon son type et sa taille.', + melodyStatsTitle: 'Répartition par type', + melodyInhabitantLabel: 'Habitant', melodyTotalBlocks: 'Notes', + melodyType: 'Type', + melodyCount: 'Blocs', melodyFilterAll: 'TOUTES', + melodyFilterShare: 'Téléverser une mélodie', + melodyShareTitle: 'Partage ta mélodie', + melodyShareHelp: 'Publie un instantané de ta mélodie actuelle pour que d’autres puissent l’écouter et la remixer.', + melodyShareTitleLabel: 'Titre (optionnel)', + melodyShareTitlePlaceholder: 'Un fantôme de blockchain', + melodyShareSubmit: 'Publier la mélodie', + melodyNoShared: 'Aucune mélodie blockchain pour l’instant.', melodyRegenerate: 'Régénérer', melodyDownload: "Télécharger BCS", + profileActivityTitle: 'Activité', + profileActivityMore: 'Voir toute l’activité', profileContentSectionTitle: 'Contenu de l’avatar', profileContentHelp: 'Choisis les modules à afficher sur ton avatar.', profileSensorsHelp: 'Métriques optionnelles affichées sur ton avatar.', profileClearnetHelp: 'Modules accessibles depuis l’extérieur d’Oasis.', profileHubAll: 'Tout', + profileHubTitle: 'Contenu Public', + footerPulseTitle: 'Pouls', + footerPulsePeers: 'Pairs', + footerPulseInbox: 'Boîte', + footerPulseSync: 'Dernière sync', + footerPulseEcoValue: 'ECO/h', + footerPulseLastActivity: 'Dernière activité', footerLicenseLabel: 'Licence', profileSwitchToOasis: 'Retour à Oasis', - profileVisibilityFediverse: "Multivers", + profileSwitchToClearnet: 'Plonger dans le Clearnet', + profileVisibilityFediverse: "Fediverse", + profileSwitchToFediverse: "Plonger dans le fédiverse", + coordLabel: 'Coordonnées (ex., A3)', + coordPlaceholder: 'Saisissez une coordonnée', contributorsTitle: "Contributions", + pixeliaBy: "par", colorLabel: 'Choisir une couleur', paintButton: 'Dessiner !', + invalidCoordinate: 'Coordonnée incorrecte', + goToMuralButton: "Voir la fresque", totalPixels: 'Total de pixels', modules: "Modules", modulesViewTitle: "Modules", modulesViewDescription: "Définissez votre environnement idéal en activant et désactivant des modules.", inbox: "Boîte", multiverse: "Multivers", - fediverse: "Multivers", - fediverseDescription: "Gérez vos autres comptes du multiverse, y compris l'envoi et la réception de contenu.", + fediverse: "Fediverse", + fediverseDescription: "Gérez vos autres comptes du fédiverse, y compris l'envoi et la réception de contenu.", fediverseStatus: "Statut", - fediverseDisconnected: "Multiverse déconnecté. Vérifiez %LINK% ou l'état de la connexion.", - fediverseSettingsTitle: "Multivers", + fediverseDisconnected: "Fédiverse déconnecté. Vérifiez %LINK% ou l'état de la connexion.", + fediverseSettingsTitle: "Fediverse", fediverseInstanceLabel: "Adresse", fediverseTokenLabel: "Jeton d'accès", fediverseTokenHelp: "Définissez votre jeton d'accès. Il servira à gérer votre compte Mastodon depuis Oasis.", fediverseConnect: "Connecter", fediverseConnectedAs: "Connecté en tant que", fediverseDisconnect: "Déconnecter", + fediverseEmpty: "Lorsque vous connectez d'autres comptes du fédiverse depuis %LINK%, vous pouvez les gérer ici.", fediverseEmptyLink: "vos réglages", fediverseComposePlaceholder: "À quoi pensez-vous ?", fediverseReplyPlaceholder: "Écrivez votre réponse…", @@ -264,15 +326,21 @@ module.exports = { fediverseReply: "Répondre", fediverseBoosted: "a partagé", fediverseNoPosts: "Votre fil est vide.", + fediverseThread: "Fil", fediverseTimeline: "Fils", + fediverseManageTimeline: "Gérer le fil", fediverseFollowers: "Abonnés", fediverseFollowing: "Abonnements", fediversePosts: "Publications", fediverseJoined: "Inscrit", + fediverseOverview: "Vue d'ensemble", fediverseRefresh: "Actualiser", fediverseWrite: "Écrire", fediversePreview: "Aperçu", + fediverseEdit: "Modifier", + fediverseOpenFeed: "Voir le flux", fediverseManage: "Gérer", + fediverseStatusNotConnected: "Non connecté", fediverseError: "Impossible de contacter Mastodon.", fediverseErrInstance: "URL d'instance invalide.", fediverseErrAuth: "Jeton invalide ou expiré.", @@ -283,6 +351,17 @@ module.exports = { fediverseErrPost: "Impossible de publier.", fediverseErrMedia: "Impossible d'envoyer le fichier.", fediverseErrEmpty: "Écrivez quelque chose ou joignez un fichier.", + popularLabel: "À la une", + topicsLabel: "Sujets", + latestLabel: "Derniers", + summariesLabel: "Résumés", + threadsLabel: "Fils", + multiverseLabel: "Multivers", + inboxLabel: "Boîte", + invitesLabel: "Invitations", + walletLabel: "Portefeuille", + legacyLabel: "Clés", + cipherLabel: "Chiffreur", bookmarksLabel: "Marque-pages", videosLabel: "Vidéos", torrentsLabel: "Torrents", @@ -293,14 +372,18 @@ module.exports = { inhabitantsLabel: "Habitants", trendingLabel: "Tendances", eventsLabel: "Événements", + tasksLabel: "Tâches", apply: "Appliquer", menuPersonal: "Personnel", + menuContent: "Contenu", + menuBlogs: "Blogs", menuGovernance: "Gouvernance", menuOffice: "Bureau", - menuNetwork: "Communauté", + menuMultiverse: "Multivers", + menuNetwork: "Réseau", menuCreative: "Créatif", menuEconomy: "Économie", - menuMedia: "Bibliothèque", + menuMedia: "Médias", menuTools: "Outils", comment: "Commenter", subtopic: "Sous-sujet", @@ -310,6 +393,13 @@ module.exports = { follow: "Soutenir", block: "Bloquer", unblock: "Débloquer", + newerPosts: "Nouveaux posts", + olderPosts: "Anciens posts", + feedRangeEmpty: "La plage appliquée est vide pour ce fil. Essayez de voir le ", + seeFullFeed: "fil complet", + feedEmpty: "Le réseau Oasis n’a jamais vu de posts depuis ce compte.", + beginningOfFeed: "Ceci est le début du fil", + noNewerPosts: "Aucun nouveau post reçu pour l’instant.", relationshipNotFollowing: "Ne vous soutient pas", relationshipTheyFollow: "Vous soutient", relationshipMutuals: "Soutien mutuel", @@ -319,20 +409,25 @@ module.exports = { relationshipBlockedBy: "Vous bloque", relationshipMutualBlock: "Blocage mutuel", relationshipNone: "Vous ne soutenez pas", + relationshipConflict: "Conflit", relationshipBlockingPost: "Post bloqué", viewLikes: "Voir les soutiens", spreadedDescription: "Liste des posts soutenus par habitant.", + totalspreads: "Soutiens totales", + attachFiles: "Joindre des fichiers", preview: "Prévisualiser", publish: "Publier", contentWarningPlaceholder: "Ajoutez un titre à votre post (optionnel)", - publishWarningPlaceholder: "Corps limité à 7000 caractères.", - publishTooLong: "Votre publication est trop longue. Veuillez la raccourcir.", + privateWarningPlaceholder: "Ajoutez des habitants pour envoyer un post privé (optionnel)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "RAPPELEZ-VOUS : En raison de la technologie blockchain, une fois que vous avez publié quelque chose, cela ne peut pas être modifié ni supprimé. Réfléchissez bien !", ], commentWarning: [ "RAPPELEZ-VOUS : En raison de la technologie blockchain, une fois que vous avez publié quelque chose, cela ne peut pas être modifié ni supprimé. Réfléchissez bien !", ], + commentPublic: "public", + commentPrivate: "privé", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -352,6 +447,7 @@ module.exports = { ".", ], publishCustom: "Écrire un post avancé", + subtopicLabel: "Créez un sous-sujet pour ce post", messagePreview: "Prévisualisation", mentionsMatching: "Mentions", mentionsName: "Nom", @@ -374,93 +470,21 @@ module.exports = { ssbLogStream: "Blockchain", ssbLogStreamDescription: "Configurez la limite de messages pour les flux de la blockchain.", saveSettings: "Enregistrer la configuration", + exportTitle: "Exporter des données", exportDescription: "Définissez un mot de passe (min 32 caractères) pour chiffrer votre secret", exportDataTitle: "Sauvegarde", exportDataDescription: "Télécharger votre blockchain (clé privée exclue !)", exportDataButton: "Télécharger la blockchain", + pubWallet: "Portefeuille du PUB", + pubWalletDescription: "Définissez l’URL du portefeuille du PUB. Elle sera utilisée pour les transactions du PUB (RBU incluse).", + pubWalletConfiguration: "Enregistrer la configuration", + importTitle: "Importer des données", importDescription: "Importez votre secret chiffré (clé privée) pour activer votre avatar", + importAttach: "Joindre le fichier chiffré (.enc)", + passwordLengthInfo: "Le mot de passe doit contenir au moins 32 caractères.", passwordImport: "Saisissez votre mot de passe pour déchiffrer les données qui seront enregistrées sur votre système (nom : secret)", exportPasswordPlaceholder: "Utilisez des minuscules, majuscules, chiffres et symboles", - fileInfo: "Votre secret chiffré sera téléchargé sur votre appareil (nom : oasis.enc)", - developer: "Développement", - devTitle: "Développement", - welcomeBannerText: "Bienvenue sur OASIS. Suivez ces quelques étapes pour configurer votre avatar.", - aiSuggestionBanner: "Suggestion :", - aiSuggestionsEnable: "Suggestions de l'IA", - welcomeBannerAction: "Commencer !", - welcomeTitle: "Bienvenue", - welcomeStepGreetingAction: "Envoyer", - welcomeJoinPubButton: "Rejoindre le pub", - welcomeStepLarpAction: "Rejoindre « L Académie »", - welcomeStepLarpText: "Rejoignez notre jeu de rôle grandeur nature.", - welcomeStepLarpTitle: "Rejoindre le L.A.R.P.", - welcomeStepGreetingTitle: "Envoyez un « Hello world ! »", - welcomeStepGreetingText: "Envoyez un message pour que les autres habitants puissent vous découvrir.", - welcomeJoinPub: "Rejoindre", - welcomeDescription: "Quelques choses à faire avant de commencer.", - welcomeProgress: "Terminé", - welcomeStepDone: "fait", - welcomeStepPending: "en attente", - welcomeSkip: "Pas maintenant", - welcomeStepLanguageTitle: "Choisissez votre langue", - welcomeStepLanguageText: "Oasis parle plusieurs langues. Vous pouvez en changer quand vous voulez.", - welcomeStepProfileTitle: "Modifiez votre profil", - welcomeStepProfileText: "Choisissez un nom, ajoutez une description et mettez une image pour que les autres habitants vous reconnaissent.", - welcomeStepProfileAction: "Enregistrer le profil", - welcomeStepFederationTitle: "Rejoignez le réseau principal", - welcomeStepFederationText: "Connectez-vous à notre pub pour rencontrer d autres habitants et commencer à répliquer.", - welcomeStepFederationAction: "Voir les invitations", - welcomeStepBackupTitle: "Sauvegardez votre ID", - welcomeStepBackupText: "Votre identité est un fichier de clés sur cet appareil.", - welcomeStepBackupWarning: "SI VOUS LE PERDEZ, PERSONNE NE POURRA LE RÉCUPÉRER.", - welcomeStepBackupAction: "Sauvegarder !", - welcomeCompleteTitle: "Tout est prêt.", - welcomeCompleteText: "Votre nœud est prêt. À partir d'ici le réseau grandit avec les gens que vous suivez.", - welcomeFindPeople: "Trouver des habitants", - devFilterFiles: "FICHIERS", - devFilterSearch: "CHERCHER", - devFilterModules: "MODULES", - devFilterTranslations: "TRADUCTIONS", - devFilterStyles: "STYLES", - devFilterTests: "TESTS", - devVersion: "Version", - devNodeVersion: "Node", - devStatsTests: "Suites de tests", - devParentFolder: "Dossier parent", - devOpenFolder: "ouvrir", - devDescription: "Explorez le code source exécuté sur ce nœud.", - devRoot: "racine", - devSearchPlaceholder: "Texte à chercher dans le code", - devAnyExtension: "Tout fichier", - devStatsFiles: "Fichiers", - devStatsLines: "Lignes", - devStatsModules: "Modules", - devStatsLanguages: "Langues", - devNotViewable: "non affichable", - devEmptyFolder: "Ce dossier est vide.", - devSize: "Taille", - devShowing: "Affichage", - devLine: "ligne", - devReportBug: "Signaler un Bug", - devCreateTask: "Créer une Tâche", - devRaw: "RAW", - devReportContext: "Trouvé en lisant le code source d'Oasis", - devTaskPrefix: "Réviser", - devPrevPage: "Lignes précédentes", - devNextPage: "Lignes suivantes", - devMatches: "correspondances", - devFilesScanned: "fichiers analysés", - devNoResults: "Aucune correspondance, pour l'instant.", - devColModule: "Module", - devColState: "État", - devColModel: "Modèle", - devColView: "Vue", - devColRoutes: "Routes", - devColTests: "Tests", - devOn: "actif", - devOff: "inactif", - modulesDevLabel: "Développement", - modulesDevDescription: "Module pour gérer le code source d'Oasis.", + fileInfo: "Votre secret chiffré sera enregistré sur votre système (nom : oasis.enc)", themeIntro: "Choisissez un thème.", setTheme: "Définir le thème", @@ -468,13 +492,16 @@ module.exports = { languageDescription: "Si vous souhaitez utiliser une autre langue, sélectionnez-la ici.", setLanguage: "Définir la langue", + peerConnections: "Nœuds", peerConnectionsIntro: "Gérez toutes vos connexions avec d’autres nœuds.", peerConnectionsTitle: "Connexions", online: "En ligne", offline: "Hors ligne", discovered: 'Découverts', unknown: 'Inconnus', + lanBroadcastLabel: "Diffusion LAN", lanBroadcastActive: "Active (multicast UDP sur le LAN)", + lanBroadcastDisabled: "Désactivée (mode pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -487,6 +514,8 @@ module.exports = { noConnections: "Aucun nœud connecté.", noDiscovered: "Vous n’avez découvert aucun nœud.", noUnknownPeers: "Aucun pair inconnu dans le graphe d’amis.", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -496,6 +525,9 @@ module.exports = { peerImport: "Importer", peerImportTitle: "Importer la liste de pairs", peerImportPlaceholder: "Colle une adresse multiserver par ligne, ou téléverse un fichier .txt…", + noSupportedConnections: "Aucun nœud pris en charge.", + noBlockedConnections: "Aucun nœud bloqué.", + noRecommendedConnections: "Aucun nœud recommandé.", connectionActionIntro: "", startNetworking: "Démarrer le réseau", @@ -509,19 +541,9 @@ module.exports = { homePageDescription: "Sélectionnez le module que vous souhaitez comme page d'accueil.", saveHomePage: "Enregistrer la page d'accueil", invites: "Invitations", + invitesTitle: "Invitations", + invitesInvites: "Invitations", invitesDescription: "Gérer et appliquer des codes d’invitation dans votre réseau.", - invitesPubsHint: "Collez un code d'invitation de pub pour vous fédérer avec lui et répliquer ses habitants.", - invitesFederationsHint: "Pubs avec lesquels vous êtes fédéré : actualisez-les, retirez les injoignables ou exportez la liste.", - invitesHousesHint: "Saisissez un code de maison pour rejoindre une maison du LARP et participer à son économie.", - invitesTribesHint: "Saisissez un code de tribu pour en devenir membre et accéder à son contenu privé.", - invitesChatsHint: "Saisissez un code de chat pour rejoindre la conversation et ses fils.", - invitesPadsHint: "Saisissez un code de pad pour écrire à plusieurs sur un document partagé.", - invitesCalendarsHint: "Saisissez un code de calendrier pour partager dates et rappels.", - invitesEventsHint: "Saisissez un code d'événement pour rejoindre un événement privé.", - invitesForumsHint: "Saisissez un code de forum pour lire et répondre dans un forum privé.", - invitesMapsHint: "Saisissez un code de carte pour ajouter et voir des lieux sur une carte partagée.", - invitesShopsHint: "Saisissez un code de boutique pour rejoindre une boutique et son catalogue.", - invitesInhabitantsHint: "Saisissez un code d'habitant pour vous suivre mutuellement et échanger du contenu.", invitesTribesTitle: "Tribus", invitesTribeInviteCodePlaceholder: "Saisissez le code de la tribu", invitesTribeJoinButton: "Entrer dans la tribu", @@ -552,6 +574,7 @@ module.exports = { invitesAlreadyFederated: "Vous êtes déjà fédéré avec ce pub.", invitesFederationsTitle: "Fédérations", invitesAcceptedInvites: "Fédérés", + invitesNoInvites: "Aucune invitation acceptée pour le moment.", invitesNoFederatedPubs: "Aucun pub fédéré.", invitesUnreachablePubs: "Inaccessibles", invitesNoUnreachablePubs: "Aucun pub inaccessible.", @@ -567,24 +590,39 @@ module.exports = { panicMode: "Mode Panique !", encryptData: "Définissez un mot de passe (min 32 caractères) pour chiffrer votre blockchain", decryptData: "Saisissez le mot de passe pour déchiffrer votre blockchain", + panicModeDescription: "Chiffrer/Déchiffrer ou SUPPRIMER votre blockchain", removeDataDescription: "ATTENTION : Ce processus ne peut pas être annulé.", + encryptPanicButton: "Chiffrer la blockchain", + decryptPanicButton: "Déchiffrer la blockchain", removePanicButton: "SUPPRIMER TOUTES VOS DONNÉES !", searchTitle: "Rechercher", searchDescriptionLabel: "Rechercher du contenu dans votre réseau.", + searchLanguagesLabel: "Langues", + searchSkillsLabel: "Compétences", searchPlaceholder:"Rechercher du contenu...", + searchDateLabel:"Date", searchLocationLabel:"Localisation", searchPriceLabel:"Prix", + searchUrlLabel:"URL", searchCategoryLabel:"Catégorie", + searchStartLabel:"Date de début", + searchEndLabel:"Date de fin", searchPriorityLabel:"Priorité", searchStatusLabel:"État", statusLabel:"État", + totalVotesLabel:"Total des votes", noResultsFound:"Aucun résultat trouvé.", votesOption:"Opinions votées", voteYesLabel:"Oui", voteNoLabel:"Non", allTypesLabel: "SANS LIMITES", + ABSTENTIONLabel:"ABSTENTION", YESLabel:"OUI", NOLabel:"NON", + FOLLOW_MAJORITYLabel: "SUIVRE LA MAJORITÉ", + CONFUSEDLabel: "CONFUS", + NOT_INTERESTEDLabel: "SANS INTÉRÊT", + StatusLabel:"État", votesOptionYesLabel:"Oui", votesOptionNoLabel:"Non", votesOptionAbstentionLabel:"Abstention", @@ -592,6 +630,7 @@ module.exports = { votesCreatedByLabel:"Créé le", voteStatusOpen:"OUVERT", voteStatusClosed:"FERMÉ", + imageSearchLabel: "Saisissez des mots pour rechercher les images étiquetées avec ces mots.", commentDescription: ({ parentUrl }) => [ " a commenté dans ", a({ href: parentUrl }, " fil"), @@ -602,20 +641,53 @@ module.exports = { a({ href: parentUrl }, " un post"), ], subtopicTitle: ({ authorName }) => [`A créé un sous-sujet dans le post de @${authorName}`], + mysteryDescription: "a publié un post mystérieux", oasisDescription: "OASIS Réseau de Projets", searchSubmit: "Rechercher...", + postLabel: "PUBLICATIONS", + aboutLabel: "HABITANTS", + feedLabel: "FLUX", votesLabel: "GOUVERNANCE", + reportLabel: "RAPPORTS", + imageLabel: "IMAGES", + videoLabel: "VIDÉOS", + audioLabel: "AUDIOS", + documentLabel: "DOCUMENTS", + torrentLabel: "TORRENTS", pdfFallbackLabel: "Document PDF", + eventLabel: "ÉVÉNEMENTS", + taskLabel: "TÂCHES", + transferLabel: "TRANSFERTS", + curriculumLabel: "CURRICULUM VITAE", + bookmarkLabel: "LIENS", + tribeLabel: "TRIBUS", + marketLabel: "MARCHÉ", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "PORTEFEUILLES", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "Appliquer", + subjectLabel: "Sujet", editProfile: "Éditer l’Avatar", + profileQrAlt: "Code QR du profil", + profileQrHint: "Scannez pour copier cet identifiant Oasis.", oasisIdLabel: "OasisID", - spreadContent: "Répliquer", + spreadLabel: "Diffuser", + spreadHint: "Diffusez ceci à ceux qui vous soutiennent (réplique via votre feed).", welcomePmSubject: "Hello World!", - welcomePmBody: "Bienvenue sur Oasis — un réseau social libre, pair-à-pair, fédéré et chiffré, avec une architecture distribuée sur invitation uniquement.\n\nQuelques endroits intéressants par où commencer :\n\n- /profile — Édite ton avatar, ton nom, ta description et les modules visibles sur ta partie publique.\n- /invites — Ajoute un pub pour fédérer avec le reste du réseau.\n- /peers — Vois qui est connecté, rescanne ton LAN, exporte ou importe des listes de pairs.\n- /inhabitants — Découvre et visite les avatars d'autres personnes.\n- /tribes — Crée ou rejoins des groupes privés chiffrés de bout en bout.\n- /inbox — Lis et envoie des messages privés.\n- /activity — Vois l'activité récente, la tienne et celle de ton réseau.\n- /publish — Écris un post ou partage une image, un audio, une vidéo ou un document.\n- /search — Recherchez le contenu du réseau par type.\n\nQuelques endroits intéressants pour se connecter :\n\n- /multiverse — Gérez vos autres comptes du multiverse, y compris l'envoi et la réception de contenu.\n\nCe message a été envoyé en privé de toi à toi-même : aucune connexion n'était nécessaire pour le recevoir.", + welcomePmBody: "Bienvenue sur Oasis — un réseau social libre, pair-à-pair, fédéré et chiffré, avec une architecture distribuée sur invitation uniquement.\n\nQuelques endroits intéressants par où commencer :\n\n- /profile — Édite ton avatar, ton nom, ta description et les modules visibles sur ta partie publique.\n- /invites — Ajoute un pub pour fédérer avec le reste du réseau.\n- /peers — Vois qui est connecté, rescanne ton LAN, exporte ou importe des listes de pairs.\n- /inhabitants — Découvre et visite les avatars d'autres personnes.\n- /tribes — Crée ou rejoins des groupes privés chiffrés de bout en bout.\n- /inbox — Lis et envoie des messages privés.\n- /activity — Vois l'activité récente, la tienne et celle de ton réseau.\n- /publish — Écris un post ou partage une image, un audio, une vidéo ou un document.\n\nQuelques endroits intéressants pour se connecter :\n\n- /fediverse — Gérez vos autres comptes du fédiverse, y compris l'envoi et la réception de contenu.\n\nCe message a été envoyé en privé de toi à toi-même : aucune connexion n'était nécessaire pour le recevoir.", + profileVisibilityTitle: "Visibilité du profil public", + profileVisibilityHint: "Choisissez les champs visibles par les autres habitants sur votre profil. Par défaut, seul Karma est affiché.", profileSensorsSectionTitle: "Capteurs", profileVisibilityActivity: "Niveau d’activité", + profileVisibilityDevice: "Appareil", profileDeviceLockedHint: "Ce capteur est toujours actif : il permet aux autres de voir depuis quel appareil vous vous connectez et ne peut pas être désactivé.", profileVisibilityKarma: "Score KARMA", profileVisibilityUbi: "UBI", @@ -623,6 +695,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "Clé GPG", + profileVisibilityClearnet: "Publier mon profil", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Boutiques", profileClearnetJobsLabel: "Offres d’emploi", @@ -676,6 +749,7 @@ module.exports = { " ou l’état de votre connexion.", ], walletSentToLine: ({ destination, amount }) => `Envoyer ECO ${amount} à ${destination}`, + walletSettingsTitle: "Portefeuille", walletSettingsDescription: "Intégrer Oasis avec votre portefeuille ECOin.", walletSettingsDocLink: "Guide d’installation d’ECOin", walletStatusMessages: { @@ -697,19 +771,37 @@ module.exports = { cipherTitle: "Chiffreur", cipherDescription: "Chiffrez et déchiffrez votre texte de manière symétrique (à l’aide d’un mot de passe partagé).", randomPassword: "Mot de passe aléatoire", + cipherEncryptTitle: "Chiffrer le texte", + cipherEncryptDescription: "Saisissez le texte à chiffrer", + cipherTextLabel: "Texte à chiffrer", cipherTextPlaceholder: "Saisissez le texte à chiffrer...", cipherPasswordLabel: "Définissez un mot de passe (minimum 32 caractères) pour chiffrer votre texte", cipherPasswordDecryptLabel: "Définissez un mot de passe (32 caractères minimum) pour déchiffrer votre texte", cipherPasswordPlaceholder: "Saisissez un mot de passe...", cipherEncryptButton: "Chiffrer", + cipherDecryptTitle: "Déchiffrer le texte", + cipherDecryptDescription: "Saisissez le texte à déchiffrer", cipherEncryptedMessageLabel: "Texte chiffré", cipherDecryptedMessageLabel: "Texte déchiffré", + cipherPasswordUsedLabel: "Mot de passe utilisé pour chiffrer (gardez-le !)", cipherEncryptedTextPlaceholder: "Saisissez le texte chiffré...", + cipherIvLabel: "IV", + cipherIvPlaceholder: "Saisissez le vecteur d’initialisation...", cipherDecryptButton: "Déchiffrer", password: "Mot de passe", text: "Texte", encryptedText: "Texte chiffré", iv: "Vecteur d’initialisation (IV)", + encryptTitle: "Chiffrez votre texte", + encryptDescription: "Saisissez le texte que vous souhaitez chiffrer et fournissez un mot de passe.", + encryptButton: "Chiffrer", + decryptTitle: "Déchiffrez votre texte", + decryptDescription: "Saisissez le texte chiffré et fournissez le même mot de passe que celui utilisé pour le chiffrement.", + decryptButton: "Déchiffrer", + passwordLengthError: "Le mot de passe doit comporter au moins 32 caractères.", + missingFieldsError: "Texte, mot de passe ou IV non fournis.", + encryptionError: "Erreur lors du chiffrement du texte.", + decryptionError: "Erreur lors du déchiffrement du texte.", bookmarkTitle: "Marque-pages", bookmarkDescription: "Découvrez et gérez des marque-pages dans votre réseau.", bookmarkAllSectionTitle: "Marque-pages", @@ -735,6 +827,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "Optionnel", bookmarkTagsLabel: "Étiquettes", bookmarkTagsPlaceholder: "Saisissez des tags séparés par des virgules", + bookmarkCategoryLabel: "Catégorie", + bookmarkCategoryPlaceholder: "Optionnel", bookmarkLastVisitLabel: "Dernière visite", bookmarkSearchPlaceholder: "Rechercher URL, tags, catégorie, auteur...", bookmarkSortRecent: "Plus récents", @@ -749,6 +843,8 @@ module.exports = { noLastVisit: "Aucune dernière visite", videoTitle: "Vidéos", videoDescription: "Explorez et gérez du contenu vidéo dans votre réseau.", + videoPluginTitle: "Titre", + videoPluginDescription: "Description", videoMineSectionTitle: "Vos vidéos", videoCreateSectionTitle: "Téléverser une vidéo", videoUpdateSectionTitle: "Mettre à jour la vidéo", @@ -780,6 +876,7 @@ module.exports = { videoSortOldest: "Les plus anciennes", videoSortTop: "Les plus votées", videoSearchButton: "Rechercher", + videoMessageAuthorButton: "MP", videoUpdatedAt: "Mis à jour", videoNoMatch: "Aucune vidéo ne correspond à votre recherche.", documentTitle: "Documents", @@ -820,6 +917,8 @@ module.exports = { documentUpdatedAt: "Mis à jour", audioTitle: "Audios", audioDescription: "Explorez et gérez du contenu audio dans votre réseau.", + audioPluginTitle: "Titre", + audioPluginDescription: "Description", audioMineSectionTitle: "Vos audios", audioCreateSectionTitle: "Téléverser un audio", audioUpdateSectionTitle: "Mettre à jour l’audio", @@ -830,6 +929,7 @@ module.exports = { audioFilterMine: "À MOI", audioFilterRecent: "RÉCENTS", audioFilterTop: "TOP", + audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Téléverser un audio", audioUpdateButton: "Mettre à jour", audioDeleteButton: "Supprimer", @@ -856,7 +956,6 @@ module.exports = { audioFilterFavorites: "FAVORIS", favoritesTitle: "Favoris", favoritesDescription: "Tous vos favoris au même endroit.", - favoritesSearchPlaceholder: "Rechercher dans les favoris...", favoritesFilterAll: "TOUS", favoritesFilterRecent: "RÉCENTS", favoritesFilterAudios: "AUDIOS", @@ -872,13 +971,18 @@ module.exports = { favoritesNoItems: "Aucun favori pour le moment.", yourContacts: "Vos contacts", allInhabitants: "Habitants", - allCVs: "CV", + allCVs: "Tous les CV", discoverPeople: "Découvrez des habitants dans votre réseau.", + allInhabitantsButton: "TOUS", + contactsButton: "SOUTIENS", + CVsButton: "CVs", + matchSkills: "Faire correspondre des compétences", + matchSkillsButton: "FAIRE CORRESPONDRE LES COMPÉTENCES", + suggestedButton: "SUGGÉRÉS", searchInhabitantsPlaceholder: "FILTRER les habitants PAR NOM …", filterLocation: "FILTRER les habitants PAR LOCALISATION …", filterLanguage: "FILTRER les habitants PAR LANGUE …", filterSkills: "FILTRER les habitants PAR COMPÉTENCES …", - cvSkillsCount: "Compétences", applyFilters: "Appliquer les filtres", locationLabel: "Localisation", languagesLabel: "Langues", @@ -887,18 +991,17 @@ module.exports = { mutualFollowers: "Soutiens mutuels", suggestedFollowsYou: "Te suit", latestInteractions: "Dernières interactions", + viewAvatar: "Voir l’Avatar", + viewCV: "Voir le CV", suggestedSectionTitle: "Suggérés", topkarmaSectionTitle: "Meilleur Karma", topecoSectionTitle: "Meilleur Éco", topactivitySectionTitle: "Top activité", - "TOP INACTIVITYButton": "PLUS INACTIFS", - topinactivitySectionTitle: "Habitants les moins actifs", blockedSectionTitle: "Bloqués", gallerySectionTitle: "GALERIE", - topSectionTitle: "TOP", + blockedButton: "BLOQUÉS", blockedLabel: "Utilisateur bloqué", inhabitantviewDetails: "Voir les détails", - cvVisitButton: "Voir le CV", keepReading: "Lire la suite...", oasisId: "ID", noInhabitantsFound: "Aucun habitant trouvé pour l’instant.", @@ -911,7 +1014,6 @@ module.exports = { parliamentFilterCandidatures: "CANDIDATURES", parliamentFilterProposals: "PROPOSITIONS", parliamentFilterLaws: "LOIS", - parliamentFilterRevocations: "RÉVOCATIONS", parliamentFilterHistorical: "HISTORIQUE", parliamentFilterLeaders: "DIRIGEANTS", parliamentFilterRules: "RÈGLES", @@ -937,8 +1039,10 @@ module.exports = { parliamentPoliciesDeclined: "LOIS REJETÉES", parliamentPoliciesDiscarded: "LOIS REJETÉES", parliamentEfficiency: "% EFFICACITÉ", + parliamentFilterRevocations: 'RÉVOCATIONS', parliamentRevocationFormTitle: 'Révoquer la Loi', parliamentRevocationLaw: 'Loi', + parliamentRevocationTitle: 'Titre', parliamentRevocationReasons: 'Motifs', parliamentCurrentRevocationsTitle: 'Révocations en Cours', parliamentFutureRevocationsTitle: 'Révocations à Venir', @@ -950,12 +1054,23 @@ module.exports = { parliamentNoGovernments: "Il n’y a pas encore de gouvernements.", parliamentCandidatureFormTitle: "Proposer une Candidature", parliamentCandidatureIdPh: "ID Oasis (@...) ou nom de la tribu", + parliamentCandidatureSlogan: "Slogan (max 140 car.)", + parliamentCandidatureSloganPh: "Un court slogan", parliamentCandidatureMethod: "Méthode", parliamentCandidatureProposeBtn: "Proposer une Candidature", + parliamentThDate: "Date de proposition", + parliamentThSlogan: "Slogan", + parliamentThSince: "Profil depuis", + parliamentThVotes: "Votes reçus", + parliamentThVoteAction: "Voter", + parliamentTypeUser: "Habitant", + parliamentTypeTribe: "Tribu", parliamentVoteBtn: "Voter", parliamentProposalFormTitle: "Proposer une Loi", parliamentProposalDescription: "Description (≤1000)", parliamentProposalPublish: "Publier la Proposition", + parliamentFinalize: "Finaliser", + parliamentDeadline: "Date limite", parliamentNoProposals: "Il n’y a pas encore de propositions de loi.", parliamentNoLaws: "Il n’y a pas encore de lois approuvées.", parliamentMethodDEMOCRACY: "Démocratie", @@ -973,21 +1088,29 @@ module.exports = { parliamentVotesSlashTotal: "Votes/Total", parliamentVotesNeeded: "Votes Requis", parliamentFutureLawsTitle: "Lois futures", + parliamentNoFutureLaws: "Pas encore de lois futures", parliamentVoteAction: "Voter", + parliamentLeadersTitle: "Dirigeants", parliamentThLeader: "AVATAR", parliamentPopulation: "Population", + parliamentThType: "Type", + parliamentThInPower: "Au pouvoir", parliamentThPresented: "CANDIDATURES", parliamentNoLeaders: "Aucun dirigeant", parliamentCandidaturesListTitle: "Liste des Candidatures", parliamentTimeRemaining: "Temps restant", + parliamentNoLeader: "Pas encore de dirigeants.", parliamentElectionsStatusTitle: "Prochain Gouvernement", parliamentProposalDeadlineLabel: "Date limite", parliamentProposalTimeLeft: "Temps restant", + parliamentProposalOnTrack: "En bonne voie pour être adoptée", + parliamentProposalOffTrack: "Soutien insuffisant", parliamentRulesTitle: "Fonctionnement du Parlement", parliamentRulesIntro: "Les élections se résolvent tous les 2 mois ; les candidatures sont continues et sont réinitialisées lorsqu’un gouvernement est choisi.", parliamentRulesCandidates: "Tout habitant peut se proposer, proposer un autre habitant ou n’importe quelle tribu. Chaque habitant peut proposer jusqu’à 3 candidatures par cycle ; les doublons dans le même cycle sont rejetés.", parliamentRulesElection: "Gagne la candidature ayant le plus de votes au moment de la résolution.", parliamentRulesTies: "En cas d’égalité : karma d’habitant le plus élevé ; sinon, profil le plus ancien ; sinon, proposition la plus précoce ; puis ordre lexicographique par ID.", + parliamentRulesFallback: "Si personne ne vote, la dernière candidature proposée gagne. S’il n’y a aucune candidature, une tribu aléatoire est sélectionnée.", parliamentRulesTerm: "L’onglet Gouvernement affiche le gouvernement actuel et ses statistiques.", parliamentRulesMethods: "Formes : Anarchie (majorité simple), Démocratie (50 % + 1), Majorité (80 %), Minorité (20 %), Karmatocratie (propositions au karma le plus élevé), Dictature (approbation instantanée).", parliamentRulesAnarchy: "L’Anarchie est le mode par défaut : si aucune candidature n’est élue à la résolution, l’Anarchie est proclamée. En Anarchie, tout habitant peut proposer des lois.", @@ -1009,7 +1132,11 @@ module.exports = { courtsCaseFormTitle: "Ouvrir une affaire", courtsCaseRespondent: "Accusé / Défendeur", courtsCaseRespondentPh: "ID Oasis (@...) ou nom de Tribu", + courtsCaseMediatorsAccuser: "Médiateurs (accusation)", courtsCaseMethod: "Méthode de résolution", + courtsCaseDescription: "Description (1000 caractères max)", + courtsCaseEvidenceTitle: "Preuves de l'affaire", + courtsCaseEvidenceHelp: "Joignez des images, audios, documents (PDF) ou vidéos qui appuient votre affaire.", courtsCaseSubmit: "Déposer l'affaire", courtsNominateJudge: "Nommer un juge", courtsJudgeId: "Juge", @@ -1055,6 +1182,22 @@ module.exports = { courtsRulesMisconduct: "Le harcèlement, la manipulation ou les preuves fabriquées peuvent entraîner une résolution négative immédiate.", courtsRulesGlossary: "Affaire : enregistrement d’un conflit. Preuves : éléments à l’appui des affirmations. Verdict : décision avec remède. Appel : demande de réexamen du verdict.", courtsFilterActions: "ACTIONS", + courtsNoActions: "Aucune action en attente pour votre rôle.", + courtsCaseTitlePlaceholder: "Brève description du conflit", + courtsCaseSeverity: "Gravité", + courtsCaseSeverityNone: "Sans étiquette de gravité", + courtsCaseSeverityLOW: "Faible", + courtsCaseSeverityMEDIUM: "Moyenne", + courtsCaseSeverityHIGH: "Élevée", + courtsCaseSeverityCRITICAL: "Critique", + courtsCaseSubject: "Sujet", + courtsCaseSubjectNone: "Sans étiquette de sujet", + courtsCaseSubjectBEHAVIOUR: "Comportement", + courtsCaseSubjectCONTENT: "Contenu", + courtsCaseSubjectGOVERNANCE: "Gouvernance / règles", + courtsCaseSubjectFINANCIAL: "Financier / ressources", + courtsCaseSubjectOTHER: "Autre", + courtsHiddenRespondent: "Caché (visible uniquement pour les rôles impliqués).", courtsThRole: "Rôle", courtsRoleAccuser: "Accusation", courtsRoleDefence: "Défense", @@ -1065,19 +1208,51 @@ module.exports = { courtsAssignJudgeBtn: "Choisir un juge", trendingTitle: "Tendances", exploreTrending: "Explorez le contenu le plus populaire dans votre réseau.", + bookmarkButton: "MARQUE-PAGES", + transferButton: "TRANSFERTS", + eventButton: "ÉVÉNEMENTS", + taskButton: "TÂCHES", votesButton: "GOUVERNANCE", - shopProductButton: "BOUTIQUE", + reportButton: "RAPPORTS", + feedButton: "FLUX", + marketButton: "MARCHÉ", + imageButton: "IMAGES", + audioButton: "AUDIOS", + videoButton: "VIDÉOS", + documentButton: "DOCUMENTS", + torrentButton: "TORRENTS", + noTrendingFound: "Aucun contenu populaire trouvé.", + noContentMessage: "Aucun contenu populaire disponible pour le moment.", + trendingDescription: "Description", + trendingDate: "Date", + trendingLocation: "Localisation", + trendingPrice: "Prix", + trendingUrl: "URL", + trendingCategory: "Catégorie", + trendingStart: "Début", + trendingEnd: "Fin", + trendingPriority: "Priorité", + trendingStatus: "État", + trendingFrom: "De", + trendingTo: "À", + trendingConcept: "Concept", + trendingAmount: "Montant", + trendingDeadline: "Échéance", + trendingItemStatus: "Statut de l’article", + trendingTotalVotes: "Total des votes", trendingTotalOpinions: "Total des avis", - trendingNoContentMessage: "Pas encore de tendances.", + trendingNoContentMessage: "Aucune tendance disponible pour le moment.", + trendingAuthor: "Par", + trendingCreatedAtLabel: "Créé le", trendingTotalCount: "Total des compteurs", tasksTitle: "Tâches", tasksDescription: "Découvrez et gérez des tâches dans votre réseau.", - taskSearchPlaceholder: "Rechercher des tâches...", taskTitleLabel: "Titre", taskDescriptionLabel: "Description", taskStartTimeLabel: "Date de début", taskEndTimeLabel: "Date de fin", taskPriorityLabel: "Priorité", + taskPrioritySelect: "Sélectionner la priorité", taskPriorityUrgent: "Urgente", taskPriorityHigh: "Élevée", taskPriorityMedium: "Moyenne", @@ -1087,13 +1262,13 @@ module.exports = { taskVisibilityLabel: "Visibilité", taskPublic: "public", taskPrivate: "privé", + taskCreatedAt: "Créé le", + taskBy: "Par", taskStatus: "État", taskStatusOpen: "Ouverte", taskStatusInProgress: "En cours", taskStatusClosed: "Fermée", taskAssignedTo: "Assignée à", - taskAssignedChip: "Assignée", - taskUnassignedChip: "Non assignée", taskAssignees: "Assignés", taskAssignButton: "M’assigner", taskUnassignButton: "Désassigner", @@ -1106,6 +1281,7 @@ module.exports = { taskFilterInProgress: "EN COURS", taskFilterClosed: "FERMÉES", taskFilterAssigned: "ASSIGNÉES", + taskFilterArchived: "ARCHIVÉES", taskFilterUrgent: "URGENTES", taskFilterHigh: "HAUTES", taskFilterMedium: "MOYENNES", @@ -1118,21 +1294,23 @@ module.exports = { taskInProgressTitle: "Tâches en cours", taskClosedTitle: "Tâches fermées", taskAssignedTitle: "Tâches assignées", + taskArchivedTitle: "Tâches archivées", + taskPublicTitle: "Tâches publiques", + taskPrivateTitle: "Tâches privées", notasks: "Aucune tâche disponible.", noLocation: "Aucune localisation spécifiée", taskSetStatus: "Définir l'état", + eventTitle: "Événements", eventDateLabel: "Date", - eventRecurrenceLabel: "Récurrence", - eventRecurrenceUntil: "Répéter jusqu'au", - eventRecurrenceHint: "Laissez la date de fin vide pour un événement unique.", - eventUpcomingDates: "Prochaines dates", eventsTitle: "Événements", eventsDescription: "Découvrez et gérez des événements dans votre réseau.", - eventSearchPlaceholder: "Rechercher des événements...", + eventDescription: "Description", eventPrice: "Prix", eventStatus: "État", + eventOrganizer: "Organisateur", eventAllSectionTitle: "Événements", eventMineSectionTitle: "Vos événements", + eventArchivedTitle: "Événements archivés", eventCreateSectionTitle: "Créer un événement", eventUpdateSectionTitle: "Mettre à jour l'événement", eventDeleteButton: "Supprimer", @@ -1140,26 +1318,11 @@ module.exports = { eventAddToCalendar: "Ajouter au calendrier", eventVisitCalendar: "Voir le calendrier", viewTask: "Voir la tâche", - viewEvent: "Voir l’événement", - viewPad: "Voir le pad", - viewMap: "Voir la carte", - viewCalendar: "Voir le calendrier", viewReport: "Voir le rapport", viewTransfer: "Voir le transfert", viewItem: "Voir l'élément", viewShop: "Voir la boutique", viewProject: "Voir le projet", - viewJob: "Voir l'Emploi", - viewPlace: "Voir le Lieu", - generatePdf: "Générer un PDF", - sharePm: "Partager par MP", - galleryImages: "Images (taille max : 50 Mo chacune)", - galleryPhotos: "Images", - galleryAddPhoto: "Ajouter une Image", - galleryRemovePhoto: "Retirer", - galleryVideo: "Vidéo (taille max : 50 Mo)", - galleryAddVideo: "Ajouter une Vidéo", - galleryRemoveVideo: "Retirer la vidéo", viewVotation: "Voir le vote", voteCastTitle: "Voter", voteResults: "Résultats", @@ -1167,15 +1330,29 @@ module.exports = { eventDescriptionLabel: "Description", eventDescriptionPlaceholder: "Saisissez la description de l'événement...", eventUpdateButton: "Mettre à jour", + eventAttendeesLabel: "Participants", + eventAttendeesPlaceholder: "Saisissez les participants, séparés par des virgules...", eventTagsLabel: "Étiquettes", + eventTagsPlaceholder: "Saisissez les étiquettes de l'événement, séparées par des virgules...", + eventTags: "Étiquettes", eventPriceLabel: "Prix", eventUrlLabel: "URL", eventAttendees: "Participants", + noAttendees: "Aucun participant pour l'instant", + eventCreatedAt: "Créé le", eventLocation: "Localisation", eventLocationLabel: "Localisation", + eventNoLocation: "Aucune localisation spécifiée", + eventNoURL: "Aucune URL spécifiée", + eventBy: "Par", noevents: "Aucun événement disponible.", eventDate: "Date", + eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "Assister à l'événement", + eventUnattendButton: "Ne plus assister à l'événement", + eventCreatedBy: "Créé par", + eventAttendeesCount: "Nombre de participants", + eventCreatedByYou: "Vous avez créé cet événement", eventFilterAll: "TOUS", eventFilterMine: "MIENS", eventFilterToday: "AUJOURD'HUI", @@ -1183,9 +1360,18 @@ module.exports = { eventFilterMonth: "CE MOIS", eventFilterYear: "CETTE ANNÉE", eventFilterArchived: "ARCHIVÉS", + eventTodayTitle: "Événements d'aujourd'hui", + eventThisWeekTitle: "Événements cette semaine", + eventThisMonthTitle: "Événements ce mois", + eventThisYearTitle: "Événements cette année", eventPrivacyLabel: "Visibilité", eventPublic: "Public", eventPrivate: "Privé", + eventPublicTitle: "Événements publics", + eventNoPrice: "Événement gratuit", + eventNoImage: "Aucune image téléchargée", + eventAttendConfirmation: "Vous assistez maintenant à cet événement", + eventUnattendConfirmation: "Vous ne participez plus à cet événement", eventAttended: "Présent", eventUnattended: "Absent", eventStatusOpen: "Ouvert", @@ -1196,14 +1382,9 @@ module.exports = { tagsTopSectionTitle: "Étiquettes principales", tagsCloudSectionTitle: "Nuage d'étiquettes", tagsFilterAll: "TOUS", - tagsFilterMine: "À MOI", - tagsFilterRecent: "RÉCENTES", - tagsMineSectionTitle: "Mes étiquettes", - tagsRecentSectionTitle: "Étiquettes récentes", tagsFilterTop: "TOP", tagsFilterCloud: "NUAGE", tagsNoItems: "Aucune étiquette disponible.", - tagsSearchPlaceholder: "Rechercher des étiquettes...", tagsTableHeaderTag: "ÉTIQUETTE/LINK :", tagsTableHeaderCount: "COMPTEUR :", transfersTitle: "Transferts", @@ -1243,12 +1424,18 @@ module.exports = { transfersDeadline: "Date limite", transfersTags: "Étiquettes", transfersStatus: "État", + transfersStatusUnconfirmed: "NON CONFIRMÉE", + transfersStatusClosed: "FERMÉE", + transfersStatusDiscarded: "REJETÉE", + transfersCreatedAt: "Créé le", transfersConfirmations: "CONFIRMATIONS", - transfersExportContract: "Générer une Facture", + transfersContainingBlock: "Bloc contenant", + transfersExportContract: "Créer le contrat", transfersConfirmButton: "Confirmer le transfert", transfersNoItems: "Aucun transfert trouvé.", transfersMineSectionTitle: "Vos transferts", transfersUBISectionTitle: "Transferts RBU", + transfersMarketSectionTitle: "Transferts du marché", transfersTopSectionTitle: "Transferts principaux", transfersPendingSectionTitle: "Transferts en attente", transfersUnconfirmedSectionTitle: "Transferts non confirmés", @@ -1256,13 +1443,21 @@ module.exports = { transfersDiscardedSectionTitle: "Transferts rejetés", transfersCreateSectionTitle: "Créer un transfert", transfersAllSectionTitle: "Transferts", + transfersFilterFavs: "Favoris", + transfersFavsSectionTitle: "Transferts favoris", + transfersSearchLabel: "Recherche", transfersSearchPlaceholder: "Rechercher concept, tags, utilisateurs...", transfersMinAmountLabel: "Montant min.", transfersMaxAmountLabel: "Montant max.", + transfersSortLabel: "Trier par", transfersSortRecent: "Plus récents", transfersSortAmount: "Montant le plus élevé", transfersSortDeadline: "Échéance la plus proche", transfersSearchButton: "Rechercher", + transfersFavoriteButton: "Favori", + transfersUnfavoriteButton: "Retirer favori", + transfersMessageUserButton: "Message", + transfersExpiringSoonBadge: "BIENTÔT", transfersExpiredBadge: "EXPIRÉ", transfersUpdatedAt: "Mis à jour", transfersNoMatch: "Aucun transfert ne correspond à votre recherche.", @@ -1275,9 +1470,6 @@ module.exports = { voteClosedTitle: "Votes fermés", voteAllSectionTitle: "Gouvernance", voteCreateButton: "Créer un vote", - voteErrorTitle: "Vérifiez le formulaire", - voteErrorDeadlineMin: "La date limite doit être d’au moins {days} jours à partir de maintenant.", - voteErrorGeneric: "Le vote n’a pas pu être enregistré.", voteUpdateButton: "Mettre à jour", voteDeleteButton: "Supprimer", voteOptionYes: "OUI", @@ -1307,6 +1499,16 @@ module.exports = { voteNoQuestion: "Aucune question fournie", voteUnknownCreator: "Créateur inconnu", voteUnknownDate: "Date inconnue", + errorVoteNotFound: "Vote introuvable", + errorAlreadyVoted: "Vous avez déjà voté.", + errorVoteClosedCannotEdit: "Impossible de modifier un vote fermé", + errorVoteDeadlinePassed: "Le délai de ce vote est passé", + errorRetrievingVote: "Erreur lors de la récupération du vote", + errorCreatingVote: "Erreur lors de la création du vote", + errorVoteAlreadyVoted: "Impossible de modifier après avoir voté", + errorDeletingOldVote: "Erreur lors de la suppression du vote précédent", + errorCreatingUpdatedVote: "Erreur lors de la création du vote mis à jour", + errorCreatingTombstone: "Erreur lors de la création de la pierre tombale", voteDetailSectionTitle: 'Détails du vote', voteCommentsLabel: 'Commentaires', voteCommentsForumButton: 'Ouvrir la discussion', @@ -1316,6 +1518,7 @@ module.exports = { voteNewCommentButton: 'Publier le commentaire', voteNewCommentLabel: 'Ajouter un commentaire', cvTitle: "CV", + cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Modifier le CV", cvCreateSectionTitle: "Créer un CV", cvDescription: "Gérez et partagez vos compétences professionnelles et informations.", @@ -1334,121 +1537,29 @@ module.exports = { cvLocationLabel: "Localisation", cvStatusLabel: "État", cvPreferencesLabel: "Préférences", + cvOasisContributorLabel: "Contributeur d’Oasis", cvPersonal: "Personnel", cvOasis: "Contributeur d’Oasis (optionnel)", - cvOasisContributorView: "Contribution", + cvOasisContributorView: "Contribution dans Oasis", cvEducational: "Éducatif (optionnel)", cvEducationalView: "Éducatif", cvProfessional: "Professionnel (optionnel)", cvProfessionalView: "Professionnel", cvAvailability: "Disponibilité (optionnel)", + cvAvailabilityView: "Disponibilité", cvUpdateButton: "Mettre à jour", cvCreateButton: "Créer un CV", + cvContactLabel: "Contact", cvCreatedAt: "Créé le", cvUpdatedAt: "Mis à jour le", cvEditButton: "Mettre à jour", cvDeleteButton: "Supprimer", cvNoCV: "Aucun CV trouvé.", blogSubject: "Sujet", - blogMessage: "Message", + blogMessage: "Corps limité à 8096 caractères.", blogImage: "Télécharger un média (max: 50 Mo)", - blogMedia: "Pièces jointes (jusqu'à 8 images et une vidéo)", blogPublish: "Aperçu", - pollsTitle: "Sondages", - dataTitle: "Correspondances", - dataDescription: "Explorez et visualisez les correspondances de votre réseau.", - dataFilterAll: "TOUT", - dataFilterMine: "LES MIENS", - dataFilterRecent: "RÉCENTS", - dataFilterTop: "TOP", - dataKindInhabitants: "HABITANTS", - dataKindJobs: "EMPLOIS", - dataKindProjects: "PROJETS", - dataKindEvents: "ÉVÉNEMENTS", - dataKindTribes: "TRIBUS", - dataKindMarket: "MARCHÉ", - dataKindHousing: "LOGEMENT", - dataKindIndustry: "INDUSTRIE", - dataKindTasks: "TÂCHES", - dataKindReports: "RAPPORTS", - dataKindVotes: "VOTES", - dataSearchPlaceholder: "Rechercher dans les données croisées...", - dataCohesionTitle: "Coefficient de cohésion (CC)", - dataCcShort: "CC", - dataCohesionHint: "Affinité moyenne entre tout ce que le réseau a publié.", - dataAffinity: "Affinité", - dataCommonTerms: "Mot-clé", - dataStatPeople: "CV", - dataStatConnected: "Connectés", - dataStatSkills: "Compétences", - dataBestMatch: "Meilleur match", - dataStatIsolated: "Isolés", - dataStatEntities: "Entités comparées", - dataStatTerms: "Termes distincts", - dataStatPairs: "Paires connectées", - dataMatchesTitle: "Correspondances", - dataMatchesHint: "Suggestions personnalisées de l'algorithme.", - dataTermsTitle: "Mots-clés", - dataTermsHint: "Les termes présents dans le plus de contenus.", - dataConnections: "Correspondances liées", - dataTopicTitle: "Tout ce qui touche à", - dataTopicHint: "Ce que l'algorithme relie à ce sujet", - dataNoMatches: "Aucune donnée croisée à afficher.", - dataNoProfile: "Publiez du contenu ou rédigez votre CV pour que l'algorithme apprenne ce qui vous intéresse.", - pollsDescription: "Module pour interroger le réseau et compter les réponses.", - pollFilterAll: "TOUS", - pollFilterMine: "LES MIENS", - pollFilterRecent: "RÉCENTS", - pollFilterTop: "TOP", - pollFilterVoted: "VOTÉS", - pollFilterOpen: "OUVERTS", - pollFilterClosed: "FERMÉS", - pollCreateButton: "Créer un Sondage", - pollCreateTitle: "Créer un Sondage", - pollEditTitle: "Modifier le Sondage", - pollQuestion: "Question", - pollQuestionPlaceholder: "Que voulez-vous demander ?", - pollOptions: "Options (une par ligne)", - pollOutcome: "Résultat", - pollNoVotesYet: "Pas encore de votes", - pollTie: "Égalité", - pollOptionsPlaceholder: "Place\nParc\nBar", - pollAnonymous: "Anonyme", - pollAnonymousLabel: "Sondage anonyme", - pollMultiple: "Choix multiple", - pollMultipleLabel: "Autoriser plusieurs réponses", - pollDeadline: "Date limite", - pollTags: "Étiquettes", - pollPublishButton: "Publier le Sondage", - pollUpdateButton: "Modifier", - pollCloseButton: "Fermer", - pollDeleteButton: "Supprimer", - pollVoteButton: "Voter", - pollChangeVote: "Changer de Vote", - pollVoters: "Votants", - pollComments: "Commentaires", - pollStatusLabel: "Statut", - pollStatusOpen: "OUVERT", - pollStatusClosed: "FERMÉ", - pollSearchPlaceholder: "Rechercher des sondages...", - pollsNoItems: "Aucun sondage trouvé.", - viewPoll: "Voir le Sondage", - pollChatCreate: "Créer un Sondage", - pollInChat: "Sondage", - blogTitle: "Blogs", - blogDescription: "Module pour découvrir et gérer les blogs.", - blogFilterAll: "TOUS", - blogFilterMine: "LES MIENS", - blogFilterRecent: "RÉCENTS", - blogFilterFavorites: "FAVORIS", - blogFilterTop: "TOP", - blogCreateButton: "Créer un Blog", - blogSearchPlaceholder: "Rechercher des blogs...", - blogComments: "Commentaires", - blogAllowComments: "Autoriser les commentaires", - blogCommentsClosed: "L'auteur a fermé les commentaires de ce blog.", - blogNoItems: "Aucun blog trouvé.", - viewBlog: "Voir le Blog", + noPopularMessages: "Aucun message populaire publié pour l’instant", forumTitle: "Forums", forumCategoryLabel: "Catégorie", forumTitleLabel: "Titre", @@ -1456,22 +1567,43 @@ module.exports = { forumCreateButton: "Créer un forum", forumCreateSectionTitle: "Créer un forum", forumDescription: "Discutez ouvertement avec d’autres habitants de votre réseau.", - forumSearchPlaceholder: "Rechercher des forums...", forumFilterAll: "TOUS", forumFilterMine: "MIENS", forumFilterRecent: "RÉCENTS", forumFilterTop: "TOP", + forumMineSectionTitle: "Vos forums", + forumRecentSectionTitle: "Forums récents", + forumAllSectionTitle: "Forums", forumDeleteButton: "Supprimer", forumParticipants: "participants", forumMessages: "messages", + forumLastMessage: "Dernier message", forumMessageLabel: "Message", forumMessagePlaceholder: "Écrivez votre message...", forumSendButton: "Envoyer", + forumVisitForum: "Visiter le forum", noForums: "Aucun forum disponible pour l’instant.", - forumVisitButton: "Visiter le Forum", - forumTopicLabel: "Sujet", + forumVisitButton: "Visiter le forum", + forumCatGENERAL: "Général", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", + forumCatPOLITICS: "Politique", + forumCatTECH: "Technologie", + forumCatSCIENCE: "Science", + forumCatMUSIC: "Musique", + forumCatART: "Art", + forumCatGAMING: "Jeux", + forumCatBOOKS: "Livres", + forumCatFILMS: "Films", + forumCatPHILOSOPHY: "Philosophie", + forumCatSOCIETY: "Société", + forumCatPRIVACY: "Vie privée", + forumCatCYBERWARFARE: "Cyberguerre", + forumCatSURVIVALISM: "Survivalisme", imageTitle: "Images", imageDescription: "Explorez et gérez du contenu image dans votre réseau.", + imagePluginTitle: "Titre", + imagePluginDescription: "Description", imageMineSectionTitle: "Vos images", imageCreateSectionTitle: "Téléverser une image", imageUpdateSectionTitle: "Mettre à jour l'image", @@ -1480,12 +1612,14 @@ module.exports = { imageTopSectionTitle: "Top images", imageFavoritesSectionTitle: "Favoris", imageGallerySectionTitle: "Galerie", + imageMemeSectionTitle: "Mèmes", imageFilterAll: "TOUS", imageFilterMine: "À MOI", imageFilterRecent: "RÉCENTES", imageFilterTop: "TOP", imageFilterFavorites: "FAVORIS", imageFilterGallery: "GALERIE", + imageFilterMeme: "MÈMES", imageCreateButton: "Téléverser une image", imageUpdateButton: "Mettre à jour", imageDeleteButton: "Supprimer", @@ -1498,6 +1632,7 @@ module.exports = { imageTitlePlaceholder: "Facultatif", imageDescriptionLabel: "Description", imageDescriptionPlaceholder: "Facultatif", + imageMemeLabel: "MÈME ?", imageNoFile: "Aucun fichier image fourni", noImages: "Aucune image disponible.", imageSearchPlaceholder: "Rechercher par titre, tags, description, auteur…", @@ -1505,6 +1640,7 @@ module.exports = { imageSortOldest: "Les plus anciennes", imageSortTop: "Les plus votées", imageSearchButton: "Rechercher", + imageMessageAuthorButton: "Message", imageUpdatedAt: "Mis à jour", imageNoMatch: "Aucune image ne correspond à votre recherche.", feedTitle: "Fil", @@ -1516,6 +1652,7 @@ module.exports = { createFeedButton: "Envoyer le fil !", feedPlaceholder: "Que se passe-t-il ? (maximum 280 caractères)", TODAYButton: "AUJOURD’HUI", + CREATEButton: "Créer un fil", totalOpinions: "Total des opinions", moreVoted: "Le plus voté", noFeedsFound: "Aucun fil trouvé.", @@ -1537,12 +1674,20 @@ module.exports = { concept: "Concept", description: "Description", meme: "Mème", + activityContact: "Contact", + activityBy: "Nom", + activityPixelia: "Nouveau pixel ajouté", + viewImage: "Voir l’image", + playAudio: "Lire l’audio", + playVideo: "Lire la vidéo", typeRecent: "RÉCENT", - typeTop: "TOP", + errorActivity: "Erreur lors de la récupération de l’activité", typePost: "BLOGS", + recentButton: "RÉCENT", typeTribe: "TRIBUS", typeLarp: "L.A.R.P.", typeInhabitants: "HABITANTS", + activityProfileUpdated: "a mis à jour son profil", typeAbout: "HABITANTS", typeCurriculum: "CVS", typeImage: "IMAGES", @@ -1586,6 +1731,8 @@ module.exports = { typeCourtsSettlementAccepted: "Tribunaux · Règlement accepté", typeCourtsNomination: "Tribunaux · Nomination", typeCourtsNominationVote: "Tribunaux · Vote de nomination", + activitySupport: "Nouvelle alliance forgée", + activityJoin: "Nouveau PUB rejoint", question: "Question", deadline: "Échéance", status: "Statut", @@ -1599,8 +1746,12 @@ module.exports = { date: "Date", category: "Catégorie", attendees: "Participants", + activitySpread: "->", + visitLink: "Visiter le lien", + viewDocument: "Voir le document", location: "Lieu", contentWarning: "Sujet", + personName: "Nom de l’habitant", bankWalletConnected: "Portefeuille ECOin", bankUbiReceived: "UBI reçu", bankTx: "Tx", @@ -1609,6 +1760,7 @@ module.exports = { link: "Lien", activityProjectFollow: "%OASIS% est maintenant %ACTION% ce projet %PROJECT%", activityProjectUnfollow: "%OASIS% est maintenant %ACTION% ce projet %PROJECT%", + activityProjectPledged: "%OASIS% a %ACTION% %AMOUNT% au projet %PROJECT%", following: "SUIVRE", unfollowing: "NE PLUS SUIVRE", pledged: "ENGAGÉ", @@ -1654,17 +1806,26 @@ module.exports = { courtsVotesSlashTotal: "OUI/TOTAL", courtsOpenVote: "Vote ouvert", courtsAnswerTitle: "Réponse", + courtsStanceADMIT: "Admettre", + courtsStanceDENY: "Refuser", + courtsStancePARTIAL: "Partiel", + courtsStanceCOUNTERCLAIM: "Demande reconventionnelle", + courtsStanceNEUTRAL: "Neutre", courtsVerdictResult: "Résultat", courtsVerdictOrders: "Ordonnances", courtsSettlementText: "Accord", courtsSettlementAccepted: "Accepté", + courtsSettlementPending: "En attente", courtsJudge: "Juge", courtsThSupports: "Soutiens", courtsFilterOpenCase: 'Ouvrir un dossier', + courtsEvidenceFileLabel: 'Fichier de preuve (image, audio, vidéo ou PDF)', + courtsCaseMediators: 'Médiateurs', courtsCaseMediatorsPh: 'IDs Oasis des médiateurs, séparés par des virgules', courtsMediatorsLabel: 'Médiateurs', courtsThCase: 'Dossier', courtsThCreatedAt: 'Date de début', + courtsThActions: 'Actions', courtsPublicPrefLabel: 'Visibilité après la résolution', courtsPublicPrefYes: "J'accepte que ce dossier soit entièrement public", courtsPublicPrefNo: 'Je préfère garder les détails privés', @@ -1672,11 +1833,8 @@ module.exports = { courtsMethodMEDIATION: 'Médiation', courtsNoCases: 'Aucun dossier.', reportsTitle: "Rapports", - reportsDescription: "Gérez et suivez les signalements de votre réseau.", - reportsSearchPlaceholder: "Rechercher des rapports...", + reportsDescription: "Gérez et suivez les rapports liés aux problèmes, aux erreurs, aux abus et aux avertissements de contenu dans votre réseau.", reportsFilterAll: "TOUS", - reportsFilterRecent: "RÉCENTS", - reportsFilterTop: "TOP", reportsFilterMine: "MIENS", reportsFilterFeatures: "FONCTIONS", reportsFilterBugs: "ERREURS", @@ -1689,13 +1847,18 @@ module.exports = { reportsFilterInvalid: "INVALIDES", reportsCreateButton: "Créer un rapport", reportsTitleLabel: "Titre", + reportsDescriptionLabel: "Description", + reportsDescriptionPlaceholder: "Veuillez fournir une description détaillée.", reportsCategory: "Catégorie", + reportsCategoryLabel: "Catégorie", reportsCategoryFeatures: "Fonctions", reportsCategoryBugs: "Erreurs", reportsCategoryAbuse: "Abus", - reportsCategoryContent: "Contenu", + reportsCategoryContent: "Problèmes de Contenu", reportsUpdateButton: "Mettre à jour", reportsDeleteButton: "Supprimer", + reportsDateLabel: "Date", + reportsUploadFile: "Télécharger un média (max: 50 Mo)", reportsMineSectionTitle: "Vos rapports", reportsFeaturesSectionTitle: "Demandes de fonctions", reportsBugsSectionTitle: "Erreurs", @@ -1703,6 +1866,11 @@ module.exports = { reportsContentSectionTitle: "Problèmes de contenu", reportsAllSectionTitle: "Rapports", reportsNoItems: "Aucun rapport disponible.", + reportsValidationTitle: "Veuillez saisir un titre valide.", + reportsValidationDescription: "La description ne peut pas être vide.", + reportsValidationCategory: "Veuillez sélectionner une catégorie.", + reportsCreatedAt: "Créé le", + reportsCreatedBy: "Par", reportsSeverity: "Gravité", reportsSeverityLow: "Faible", reportsSeverityMedium: "Moyenne", @@ -1713,10 +1881,13 @@ module.exports = { reportsStatusUnderReview: "En révision", reportsStatusResolved: "Résolu", reportsStatusInvalid: "Invalide", + reportsUpdateStatusButton: "Mettre à jour l’état", + reportsAnonymityOption: "Envoyer de façon anonyme", + reportsAnonymousAuthor: "Anonyme", reportsConfirmButton: "CONFIRMER LE RAPPORT !", reportsConfirmations: "Confirmations", reportsConfirmedSectionTitle: "Rapports confirmés", - reportsCreateTaskButton: "Créer une tâche", + reportsCreateTaskButton: "CRÉER UNE TÂCHE", reportsOpenSectionTitle: "Rapports ouverts", reportsUnderReviewSectionTitle: "Rapports en révision", reportsResolvedSectionTitle: "Rapports résolus", @@ -1753,20 +1924,6 @@ module.exports = { reportsRequestedActionLabel: 'Action demandée', reportsRequestedActionPlaceholder: 'Supprimer, masquer, étiqueter, avertir, etc.', tribesTitle: "Tribus", - tribeFilterAll: "TOUTES", - tribeFilterRecent: "RÉCENTES", - tribeFilterMine: "MIENNES", - tribeFilterMembership: "ADHÉSION", - tribeFilterSubtribes: "SOUS-TRIBUS", - tribeFilterTop: "TOP", - tribeFilterGallery: "GALERIE", - tribeFeedFilterTOP: "TOP", - tribeFeedFilterMINE: "MIENS", - tribeFeedFilterALL: "TOUS", - tribeFeedFilterRECENT: "RÉCENTS", - courtsStanceDENY: "Nier", - courtsStanceADMIT: "Admettre", - courtsStancePARTIAL: "Admettre en partie", tribeAllSectionTitle: "Tribus", tribeMineSectionTitle: "Vos tribus", tribeCreateSectionTitle: "Créer une tribu", @@ -1778,6 +1935,13 @@ module.exports = { tribeviewSubTribeButton: "Visiter la sous-tribu", tribeRootLabel: "RACINE", tribeDescription: "Explorez ou créez des tribus dans votre réseau.", + tribeFilterAll: "TOUS", + tribeFilterMine: "MIENS", + tribeFilterMembership: "MEMBRES", + tribeFilterRecent: "RÉCENTS", + tribeFilterTop: "TOP", + tribeFilterSubtribes: "SOUS-TRIBUS", + tribeFilterGallery: "GALERIE", tribeMainTribeLabel: "TRIBU PRINCIPALE", tribeCreateButton: "Créer une tribu", tribeUpdateButton: "Mettre à jour", @@ -1796,8 +1960,13 @@ module.exports = { tribeModeLabel: "MODE", tribeIsAnonymousLabel: "STATUT", tribeMembersCount: "Membres", + tribeInviteCodePlaceholder: "Saisissez le code d’invitation", + tribeJoinByCodeButton: "Rejoindre avec un code", + tribeJoinButton: "REJOINDRE", tribeEnterInvite: "SAISIR LE CODE", tribeLeaveButton: "QUITTER", + tribeYes: "OUI", + tribeNo: "NON", tribePublic: "PUBLIQUE", tribePrivate: "PRIVÉE", tribeGenerateInvite: "INVITATION UNIQUE", @@ -1806,8 +1975,13 @@ module.exports = { tribeRemoveInvitation: "SUPPRIMER L’INVITATION", tribeCreatedAt: "Créé le", tribeAuthor: "Par", + tribeAuthorLabel: "AUTEUR", tribeStrict: "Strict", tribeOpen: "Ouverte", + tribeFeedFilterRECENT: "RÉCENTS", + tribeFeedFilterMINE: "MIENS", + tribeFeedFilterALL: "TOUS", + tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Repartages", tribeFeedRefeed: "Repartager", tribeFeedMessagePlaceholder: "Écrivez un feed…", @@ -1817,12 +1991,17 @@ module.exports = { tribeNotFound: "Tribu introuvable!", createTribeTitle: "Créer une tribu", updateTribeTitle: "Mettre à jour la tribu", + tribeSectionOverview: "Aperçu", tribeSectionInhabitants: "Habitants", tribeSectionVotations: "VOTATIONS", tribeSectionEvents: "ÉVÉNEMENTS", + tribeSectionReports: "Rapports", tribeSectionTasks: "TÂCHES", tribeSectionFeed: "FIL", tribeSectionForum: "FORUM", + tribeSectionMarket: "Marché", + tribeSectionJobs: "Emplois", + tribeSectionProjects: "Projets", tribeSectionMedia: "Média", tribeSectionImages: "IMAGES", tribeSectionAudios: "AUDIOS", @@ -1860,6 +2039,11 @@ module.exports = { tribeTaskStatusClosed: "FERMER", tribeTaskAssign: "ASSIGNER", tribeTaskUnassign: "DÉSASSIGNER", + tribeReportCreate: "Créer un rapport", + tribeReportsEmpty: "Aucun rapport pour l'instant.", + tribeReportTitle: "Titre", + tribeReportDescription: "Description", + tribeReportCategory: "Catégorie", tribeVotationCreate: "Créer une votation", tribeVotationsEmpty: "Aucune votation pour l'instant.", tribeVotationTitle: "Titre", @@ -1877,6 +2061,27 @@ module.exports = { tribeForumCategory: "Catégorie", tribeForumReply: "Répondre", tribeForumReplies: "Réponses", + tribeMarketCreate: "Créer une annonce", + tribeMarketEmpty: "Aucune annonce pour l'instant.", + tribeMarketTitle: "Titre", + tribeMarketDescription: "Description", + tribeMarketPrice: "Prix", + tribeMarketImage: "Image", + tribeMarketCategory: "Catégorie", + tribeJobCreate: "Créer un emploi", + tribeJobsEmpty: "Aucun emploi pour l'instant.", + tribeJobTitle: "Titre", + tribeJobDescription: "Description", + tribeJobLocation: "Lieu", + tribeJobSalary: "Salaire", + tribeJobDeadline: "Date limite", + tribeProjectCreate: "Créer un projet", + tribeProjectsEmpty: "Aucun projet pour l'instant.", + tribeProjectTitle: "Titre", + tribeProjectDescription: "Description", + tribeProjectGoal: "Objectif", + tribeProjectFunded: "Financé", + tribeProjectDeadline: "Date limite", tribeMediaUpload: "Télécharger un média", readDocument: "Lire le Document", tribeCreateImage: "Créer Image", @@ -1887,6 +2092,7 @@ module.exports = { tribeMediaEmpty: "Aucun média pour l'instant.", tribeMediaTitle: "Titre", tribeMediaDescription: "Description", + tribeMediaType: "Type", tribeMediaTypeImage: "Image", tribeMediaTypeVideo: "Vidéo", tribeMediaTypeAudio: "Audio", @@ -1896,6 +2102,11 @@ module.exports = { tribeInviteCodeText: "Code d'invitation : ", oasisVersionLabel: "Version d’Oasis", tribeInviteCodeHint: "Partagez ce code avec la personne que vous souhaitez inviter. Elle peut rejoindre via /invites → Tribes.", + tribeGroupTribe: "Tribu", + tribeGroupOffice: "Bureau", + tribeGroupNetwork: "Réseau", + tribeGroupEconomy: "Économie", + tribeGroupMedia: "Média", tribeStatusOpen: "OUVERT", tribeStatusClosed: "FERMÉ", tribeStatusInProgress: "EN COURS", @@ -1905,17 +2116,33 @@ module.exports = { tribePriorityCritical: "CRITIQUE", tribeTaskFilterAll: "TOUS", tribeMediaFilterAll: "TOUS", + tribeReportCatBug: "BUG", + tribeReportCatAbuse: "ABUS", + tribeReportCatContent: "CONTENU", + tribeReportCatOther: "AUTRE", tribeForumCatGeneral: "GÉNÉRAL", tribeForumCatProposal: "PROPOSITION", tribeForumCatQuestion: "QUESTION", tribeForumCatAnnouncement: "ANNONCE", + tribeMarketCatGoods: "BIENS", + tribeMarketCatServices: "SERVICES", + tribeMarketCatFood: "ALIMENTATION", + tribeMarketCatOther: "AUTRE", tribeStatusLabel: "Statut", tribeSubTribes: "SOUS-TRIBUS", tribeSubTribesCreate: "Créer Sous-Tribu", tribeSubTribesStrictDenied: "Le mode strict de la tribu ne vous permet pas de créer de nouvelles sous-tribus. Veuillez contacter l'administrateur.", + tribeSubTribesEmpty: "Aucune sous-tribu créée, pour l'instant.", + tribeActivityJoined: "REJOINT", + tribeActivityLeft: "QUITTÉ", + tribeActivityFeed: "FIL", tribeActivityRefeed: "REPARTAGE", + tribeGroupAnalytics: "Analytiques", + tribeGroupCreative: "Créatif", tribeSectionActivity: "ACTIVITÉ", tribeSectionTrending: "TENDANCES", + tribeSectionOpinions: "OPINIONS", + tribeSectionPixelia: "PIXELIA", tribeSectionTags: "TAGS", tribeSectionSearch: "RECHERCHE", tribeActivityEmpty: "Aucune activité pour le moment.", @@ -1927,15 +2154,25 @@ module.exports = { tribeTrendingPeriodWeek: "Cette Semaine", tribeTrendingPeriodAll: "Tout", tribeTrendingEngagement: "engagement", + tribeOpinionsEmpty: "Aucune opinion pour le moment.", + tribeOpinionsCast: "Voter", + tribeOpinionsRankings: "Classements", + tribeOpinionsAlreadyVoted: "Déjà voté", tribeTopCategory: "Plus voté", + tribePixeliaTitle: "Pixelia", + tribePixeliaDescription: "Toile collaborative de pixel art.", + tribePixeliaPaint: "Peindre", + tribePixeliaContributors: "contributeurs", + tribePixeliaTotalPixels: "pixels peints", tribeTagsEmpty: "Aucun tag trouvé.", + tribeTagsCloud: "Nuage de Tags", + tribeTagsContentWith: "Contenu avec le tag", tribeSearchPlaceholder: "Rechercher dans la tribu...", tribeSearchEmpty: "Aucun résultat.", tribeSearchResults: "Résultats", tribeSearchMinChars: "Entrez au moins 2 caractères pour rechercher.", agendaTitle: "Agenda", agendaDescription: "Ici vous pouvez trouver tous vos éléments assignés.", - agendaSearchPlaceholder: "Rechercher dans l’agenda...", agendaFilterAll: "TOUS", agendaFilterToday: "AUJOURD’HUI", agendaFilterUpcoming: "À VENIR", @@ -1949,14 +2186,23 @@ module.exports = { agendaFilterReports: "RAPPORTS", agendaFilterTransfers: "TRANSFERTS", agendaFilterJobs: "EMPLOIS", - agendaFilterIndustry: "INDUSTRIE", - agendaFilterHousing: "LOGEMENT", agendaFilterProjects: "PROJETS", agendaFilterCalendars: "CALENDRIERS", agendaNoItems: "Aucune affectation trouvée.", agendaDiscardButton: "Écarter", agendaRestoreButton: "Restaurer", + agendaAuthor: "Par", + agendaCreatedAt: "Créé le", + agendaTitleLabel: "Titre", agendaMembersCount: "Membres", + agendaDescriptionLabel: "Description", + agendaStatus: "État", + agendaVisibility: "Visibilité", + agendaEventDate: "Date de l’événement", + agendaEventLocation: "Localisation", + agendaEventPrice: "Prix", + agendaEventUrl: "URL", + agendaTaskStart: "Heure de début", agendaLocationLabel: "Localisation", agendaYes: "OUI", agendaNo: "NON", @@ -1966,6 +2212,11 @@ module.exports = { agendareportCategory: "Catégorie", agendareportSeverity: "Gravité", agendareportStatus: "État", + agendareportDescription: "Description", + agendaTaskEnd: "Heure de fin", + agendaTaskPriority: "Priorité", + agendaTransferFrom: "De", + agendaTransferTo: "À", agendaTransferConcept: "Concept", agendaTransferAmount: "Montant", agendaTransferDeadline: "Date limite", @@ -1975,7 +2226,47 @@ module.exports = { voteNow: "Votez maintenant", alreadyVoted: "Vous avez déjà donné votre avis.", noOpinionsFound: "Aucune opinion trouvée.", + RECENTButton: "RÉCENT", TOPButton: "TOP", + interestingButton: "INTÉRESSANT", + necessaryButton: "NÉCESSAIRE", + funnyButton: "AMUSANT", + disgustingButton: "DÉGOUTANT", + sensibleButton: "SENSIBLE", + propagandaButton: "PROPAGANDE", + adultOnlyButton: "ADULTE SEULEMENT", + boringButton: "ENNUYEUX", + confusingButton: "CONFUS", + usefulButton: "UTILE", + informativeButton: "INFORMATIF", + wellResearchedButton: "BIEN RECHERCHÉ", + accurateButton: "PRÉCIS", + needsSourcesButton: "BESOIN DE SOURCES", + wrongButton: "FAUX", + lowQualityButton: "FAIBLE QUALITÉ", + creativeButton: "CRÉATIF", + insightfulButton: "PERSPECTIF", + actionableButton: "RÉALISABLE", + inspiringButton: "INSPIRANT", + loveButton: "AMOUR", + clearButton: "CLAIR", + upliftingButton: "ÉLÉVANT", + unnecessaryButton: "INUTILE", + rejectedButton: "REJETÉ", + misleadingButton: "TROMPEUR", + offTopicButton: "HORS SUJET", + duplicateButton: "DUPE", + clickbaitButton: "APPÂT À CLIC", + spamButton: "SPAM", + trollButton: "TROLL", + nsfwButton: "NSFW", + violentButton: "VIOLENT", + toxicButton: "TOXIQUE", + harassmentButton: "HARCÈLEMENT", + hateButton: "HAINE", + scamButton: "ESCROQUERIE", + triggeringButton: "PROVOCANT", + opinionsCreatedAt: "Créé à", opinionsTotalCount: "Total des opinions", voteInteresting: "Intéressant", voteNecessary: "Nécessaire", @@ -2012,39 +2303,27 @@ module.exports = { voteCreative: "Créatif", voteSpam: "Spam", voteAdultOnly: "Adultes uniquement", + publishBlog: "Publier un blog", privateMessage: "MP", pmSendTitle: "Messages privés", pmSend: "Envoyer !", - pmCancel: "Annuler", - pmReset: "Réinitialiser", - pmSharedKeyHint: "Partagez cette clé avec le destinataire par un autre canal. Elle est nécessaire pour déchiffrer.", - pmDescription: "Utilisez ce formulaire pour envoyer un message/fichier chiffré à d'autres habitants.", - pmComposeTitle: "Envoyer un message", + pmDescription: "Utilisez ce formulaire pour envoyer un message chiffré à d’autres habitants.", pmRecipients: "Destinataires", pmRecipientsHint: "Saisissez les IDs Oasis séparés par des virgules", - pmLimitsHint: "Jusqu'à 7 destinataires par message.", - pmTextPlaceholder: "Corps limité à 7000 caractères.", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "Sujet", pmSubjectHint: "Saisissez l’objet du message", pmText: "Message", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Déchiffrer", - fileShareTitle: "Partager un fichier", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "Fichier", - fileShareDownload: "Télécharger", - fileShareMutualError: "Vous ne pouvez partager des fichiers qu'avec des habitants en soutien mutuel.", - fileShareNoFile: "Aucun fichier sélectionné.", - fileShareTooLarge: "Le fichier dépasse la taille autorisée.", - fileShareFailed: "Impossible de préparer le fichier.", - fileShareSendError: "Impossible d'envoyer le fichier.", - fileShareUnavailable: "Ce fichier n'est pas disponible pour le moment. Réessayez plus tard.", pmCrypterBadKey: "La clé partagée est incorrecte !", pmCrypterTooLong: "Le message est trop long pour être chiffré avec le Crypter. Raccourcissez-le et réessayez.", pmCrypterCipherLabel: "Message rechiffré", pmCrypterCharsLabel: "caractères", pmInvalidRecipients: "Identifiant Oasis invalide (devrait ressembler à @…=.ed25519).", pmEncryptionLabel: "Chiffrement", + pmFile: "Fichier joint", private: "Privés", privateDescription: "Vos messages chiffrés.", privateInbox: "BOÎTE DE RÉCEPTION", @@ -2054,33 +2333,27 @@ module.exports = { noPrivateMessages: "Aucun message privé.", pmReply: "Répondre", pmReplies: "réponses", + pmNew: "nouvelles", + pmMarkRead: "Marquer comme lu", inReplyTo: "EN RÉPONSE À", pmPreview: "Aperçu", pmPreviewTitle: "Aperçu", performed: "→", pmFromLabel: "De :", pmToLabel: "À :", + pmInvalidMessage: "Message non valide", pmNoSubject: "(sans objet)", pmSubjectLabel: "Objet :", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "La maison dirigeante du L.A.R.P a changé.", - politicalBotParliamentTitle: "Un nouveau cycle de gouvernement a commencé au Parlement.", - politicalBotTribeTitle: "Un nouveau cycle de gouvernement a commencé dans l'une de tes Tribus.", - politicalBotLarpIntro: "Maintenant {house} gouverne durant ce cycle : {cycle}", - politicalBotParliamentIntro: "Le gouvernement actuel est {gov}", - politicalBotParliamentIntroLeader: "Le gouvernement actuel est {gov}, exercé par {leader}", - politicalBotTribeIntro: "Le gouvernement actuel dans la Tribu {tribe} est {gov}", - politicalBotTribeIntroLeader: "Le gouvernement actuel dans la Tribu {tribe} est {gov}, exercé par {leader}", - politicalBotLeaderLabel: "Chef", + pmBodyLabel: "Corps", + pmBotJobs: "42-EmploisBOT", + pmBotProjects: "42-ProjetsBOT", + pmBotMarket: "42-MarchéBOT", inboxJobSubscribedTitle: "Nouvelle inscription à votre offre d’emploi", pmInhabitantWithId: "Habitat avec ID OASIS :", pmHasSubscribedToYourJobOffer: "s’est inscrit à votre offre d’emploi", + inboxProjectCreatedTitle: "Nouveau projet créé", + pmHasCreatedAProject: "a créé un projet", inboxMarketItemSoldTitle: "Article vendu", - inboxShopSoldTitle: "Produit vendu", pmYourItem: "Votre article", pmHasBeenSoldTo: "a été vendu à", pmFor: "pour", @@ -2088,6 +2361,7 @@ module.exports = { pmHasPledged: "a contribué", pmToYourProject: "à votre projet", blockchain: 'Explorateur de blocs', + blockchainTitle: 'Explorateur de blocs', blockchainDescription: 'Explorez et visualisez les blocs de la chaîne.', blockchainNoBlocks: 'Aucun bloc trouvé sur la chaîne.', blockchainStatsTitle: 'Statistiques', @@ -2099,30 +2373,40 @@ module.exports = { blockchainBlockCarbon: 'Empreinte carbone', blockchainBlockType: 'Type', blockchainBlockTimestamp: 'Horodatage', + blockchainBlockContent: 'Bloc', + blockchainBlockURL: 'URL :', + blockchainContent: 'Bloc', + blockchainContentPreview: 'Aperçu du contenu du bloc', blockchainLatestDatagram: 'Dernier Datagramme', - blockchainDatagram: "Datagramme", - blockchainDetails: "Voir les Détails du Bloc", - blockchainViewBlockexplorer: "Voir dans le Blockexplorer", + blockchainDatagram: 'Datagramme', + blockchainDetails: 'Voir les détails du bloc', + blockchainViewBlockexplorer: "Voir dans le blockexplorer", + blockchainBlockInfo: 'Informations du bloc', + blockchainBlockDetails: 'Détails du bloc sélectionné', blockchainBack: 'Retour à l’explorateur', banking: 'Banque', bankingTitle: 'Banque', - bankingDescription: "L'économie ECOin : solde, revenu de base, taxes et valeur de l'industrie.", + bankingDescription: 'Explorez la valeur actuelle d’ECOin et l’allocation RBU correspondante, distribuée chaque semaine en fonction de la participation et de la confiance.', bankOverview: 'Aperçu', bankEpochs: 'Époques', bankRules: 'Règles', pending: 'En attente', closed: 'Fermées', + bankBack: 'Retour à la Banque', bankViewTx: 'Voir la Tx', bankClaimNow: 'Réclamer', bankClaimUBI: 'Réclamer RBU !', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: 'Aucune allocation RBU en attente pour cette époque.', bankEpoch: 'Époque', bankPool: 'Fonds (cette époque)', bankWeightsSum: 'Somme des poids', + bankAllocations: 'Attributions', bankNoAllocations: 'Aucune attribution trouvée.', bankNoEpochs: 'Aucune époque trouvée.', bankEpochAllocations: 'Attributions de l’époque', @@ -2136,11 +2420,10 @@ module.exports = { bankEpochId: 'ID de l’époque', bankRuleHash: 'Hash de l’instantané des règles', bankViewEpoch: 'Voir l’époque', - bankYourIndustryBalance: "Votre part de l'industrie", - bankYourUbiMonth: "Votre RBU (ce mois-ci)", - bankYourFundsMonth: "Vos fonds (ce mois-ci)", - bankIndustryBalance: "Production industrielle (estimée)", bankUserBalance: 'Votre solde', + ecoWalletNotConfigured: 'Portefeuille ECOin non configuré', + editWallet: 'Modifier le portefeuille', + addWallet: 'Ajouter un portefeuille', bankAddresses: 'Adresses', bankNoAddresses: 'Aucune adresse trouvée.', bankUser: 'ID d’Oasis', @@ -2165,11 +2448,19 @@ module.exports = { search: 'Rechercher !', bankLocal: 'Local', bankFromOasis: 'Oasis', + bankMyAddress: 'Votre adresse', + bankRemoveMyAddress: 'Supprimer mon adresse', + bankNotRemovableOasis: 'Les adresses ne peuvent pas être supprimées localement', bankingUserEngagementScore: "Score KARMA", + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "PAS DE FONDS!", bankUbiAvailableOk: "DISPONIBLE!", - bankUbiAvailability: "RBU (réseau)", + bankUbiAvailability: "Disponibilité UBI", bankAlreadyClaimedThisMonth: "Déjà réclamé ce mois-ci", bankUbiThisMonth: "RBU (ce mois)", bankUbiLastClaimed: "RBU (dernière réclamation)", @@ -2183,6 +2474,13 @@ module.exports = { shopsTitle: "Boutiques", shopDescription: "Découvrez et gérez les boutiques du réseau.", shopTitle: "Boutique", + shopFilterAll: "TOUTES", + shopFilterMine: "MIENNES", + shopFilterRecent: "RÉCENTES", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUITS", + shopFilterPrices: "PRIX", + shopFilterFavorites: "FAVORIS", shopUpload: "Créer Boutique", shopAllSectionTitle: "Toutes les Boutiques", shopMineSectionTitle: "Mes Boutiques", @@ -2199,10 +2497,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "Publier sur le Clearnet", + shopClearnetUnpublish: "Retirer du Clearnet", + shopClearnetUrlLabel: "URL Clearnet", + shopClearnetWarning: "Publier sur le Clearnet rend cette boutique indexable par les moteurs de recherche et robots externes.", shopAddFavorite: "Ajouter Favori", shopRemoveFavorite: "Retirer Favori", shopOpen: "OUVERTE", shopClosed: "FERMÉE", + shopOpenShop: "Ouvrir Boutique", + shopCloseShop: "Fermer Boutique", shopMakePrivate: "RENDRE PRIVÉE (E2E)", shopMakePublic: "RENDRE PUBLIQUE", shopProducts: "Produits", @@ -2230,6 +2534,8 @@ module.exports = { shopOrderPrice: "Prix", shopOrderBuyer: "Acheteur", shopBackToShop: "Retour à la Boutique", + shopShareUrl: "URL de partage", + shopVisitShop: "VISITER BOUTIQUE", marketVisitItem: "VOIR L’ARTICLE", shopStatus: "STATUT", shopCreatedAt: "CRÉÉ", @@ -2238,10 +2544,12 @@ module.exports = { shopLocation: "Localisation", shopTags: "Étiquettes", shopVisibility: "Visibilité", + shopImage: "Image", shopShortDescription: "Description Courte", shopShortDescriptionPlaceholder: "Description breve pour les cartes (max 160 caracteres)", shopTitlePlaceholder: "Nom de votre boutique", shopDescriptionPlaceholder: "Description détaillée de votre boutique", + shopUrlPlaceholder: "https://url-de-votre-boutique.com", shopLocationPlaceholder: "Ville, Pays", shopTagsPlaceholder: "tag1, tag2, tag3", mapTitlePlaceholder: "Titre de la carte", @@ -2258,6 +2566,7 @@ module.exports = { bankUnitMinutes: "minutes", bankUnitDays: "jours", bankExchangeNoData: 'Aucune donnée disponible', + bankExchangeIndex: 'Valeur d’ECOin (1h)', bankInflation: 'Inflation d’ECOin (anual)', bankInflationMonthly: 'Inflation d’ECOin (mensual)', bankExchangeChartTitle: 'Progression de la valeur d’ECOin au fil du temps', @@ -2271,17 +2580,38 @@ module.exports = { bankingSyncStatusOutdated: 'Obsolète', statsTitle: 'Statistiques', statistics: "Statistiques", + statsInhabitant: "Statistiques de l'habitant", statsDescription: "Découvrez des statistiques sur votre réseau.", ALLButton: "TOUT", MINEButton: "MIENS", TOMBSTONEButton: "SUPPRESSIONS", + statsYou: "Vous", + statsUserId: "ID Oasis", statsCreatedAt: "Créé le", statsYourContent: "Contenu", statsYourOpinions: "Avis", + statsYourTombstone: "Suppressions", + statsNetwork: "Réseau", + statsTotalInhabitants: "Habitants", + statsDiscoveredTribes: "Tribus (publiques)", + statsPrivateDiscoveredTribes: "Tribus (privées)", statsNetworkContent: "Contenu", + statsYourMarket: "Marché", + statsYourJob: "Emplois", + statsYourProject: "Projets", + statsYourTransfer: "Transferts", + statsYourForum: "Forums", statsNetworkOpinions: "Avis", + statsDiscoveredMarket: "Marché", + statsDiscoveredJob: "Emplois", + statsDiscoveredProject: "Projets", + statsBankingTitle: "Banque", statsEcoWalletLabel: "Portefeuille ECOIN", statsEcoWalletNotConfigured: "Non configuré !", + statsTotalEcoAddresses: "Adresses totales", + statsDiscoveredTransfer: "Transferts", + statsDiscoveredForum: "Forums", + statsNetworkTombstone: "Suppressions", statsBookmark: "Signets", statsEvent: "Événements", statsTask: "Tâches", @@ -2303,12 +2633,16 @@ module.exports = { statsAiExchange: "IA", statsPUBs: 'PUBs', statsPost: "Publications", + statsOasisID: "ID Oasis", statsSize: "Total (taille)", statsBlockchainSize: "Blockchain (taille)", statsBlobsSize: "Blobs (taille)", statsActivity7d: "Activité (7 derniers jours)", statsActivity7dTotal: "Total 7 jours", statsActivity30dTotal: "Total 30 jours", + statsKarmaScore: "Score KARMA", + statsPublic: "Public", + statsPrivate: "Privé", day: "Jour", messages: "Messages", statsProject: "Projets", @@ -2320,14 +2654,30 @@ module.exports = { statsProjectsCancelled: "Annulés", statsProjectsGoalTotal: "Objectif total", statsProjectsPledgedTotal: "Montant promis total", + statsProjectsSuccessRate: "Taux de réussite", + statsProjectsAvgProgress: "Progression moyenne", + statsProjectsMedianProgress: "Progression médiane", + statsProjectsActiveFundingAvg: "Financement actif moyen", + statsJobsTitle: "Emplois", + statsJobsTotal: "Emplois au total", + statsJobsOpen: "Ouverts", + statsJobsClosed: "Fermés", + statsJobsOpenVacants: "Postes ouverts", + statsJobsSubscribersTotal: "Abonnés au total", + statsJobsAvgSalary: "Salaire moyen", + statsJobsMedianSalary: "Salaire médian", statsMarketTitle: "Marché", statsMarketTotal: "Articles au total", statsMarketForSale: "À vendre", statsMarketReserved: "Réservés", statsMarketClosed: "Fermés", statsMarketSold: "Vendus", + statsMarketRevenue: "Chiffre d'affaires", + statsMarketAvgSoldPrice: "Prix de vente moyen", statsUsersTitle: "Habitants", user: "Habitant", + statsTombstoneTitle: "Suppressions", + statsNetworkTombstones: "Suppressions du réseau", statsTombstoneRatio: "Taux de suppressions (%)", statsParliamentCandidature: "Candidatures au parlement", statsParliamentTerm: "Mandats parlementaires", @@ -2354,21 +2704,30 @@ module.exports = { aiConfiguration: "Configurer le prompt", aiPromptUsed: "Invite", aiClearHistory: "Effacer l’historique du chat", + aiSharePrompt: "Ajouter cette réponse à l’entraînement collectif ?", + aiShareYes: "Oui", + aiShareNo: "Non", + aiSharedLabel: "Ajoutée à l’entraînement", + aiRejectedLabel: "Non ajoutée à l’entraînement", aiServerError: "L’IA n’a pas pu répondre. Réessayez.", aiInputPlaceholder: "Qu'est-ce qu'Oasis ?", typeAiExchange: "IA", aiApproveTrain: "Ajouter à l’entraînement collectif", aiRejectTrain: "Ne pas entraîner", + aiTrainPending: "En attente d’approbation", aiTrainApproved: "Approuvé pour l’entraînement", aiTrainRejected: "Rejeté pour l’entraînement", aiSnippetsUsed: "Fragments utilisés", aiSnippetsLearned: "Fragments appris", statsAITraining: "Entraînement de l’IA", + aiApproveCustomTrain: "Entraîner avec cette réponse personnalisée", aiCustomAnswerPlaceholder: "Écrivez votre réponse personnalisée…", + statsAIExchanges: "Échanges de modèles", marketMineSectionTitle: "Vos articles", marketCreateSectionTitle: "Créer un article", marketUpdateSectionTitle: "Mettre à jour", marketAllSectionTitle: "Marché", + marketRecentSectionTitle: "Marché récent", marketTitle: "Marché", marketDescription: "Un marché pour échanger des biens ou des services dans votre réseau.", marketFilterAll: "TOUS", @@ -2398,11 +2757,14 @@ module.exports = { marketItemDeadline: "Date limite", marketItemIncludesShipping: "Livraison incluse ?", marketItemHighestBid: "Meilleure offre", + marketItemHighestBidder: "Meilleur enchérisseur", marketItemStock: "Réserve", marketOutOfStock: "Rupture de stock", + marketItemBidTime: "Date de l'enchère", marketActionsUpdate: "Mettre à jour", marketUpdateButton: "Mettre à jour", marketActionsDelete: "Supprimer", + marketActionsSold: "Marquer comme vendu", marketActionsChangeStatus: "CHANGER LE STATUT", marketActionsBuy: "ACHETER !", marketAuctionBids: "Offres en cours", @@ -2411,82 +2773,19 @@ module.exports = { marketNoItems: "Aucun article disponible pour le moment.", marketYourBid: "Votre offre", marketCreateFormImageLabel: "Télécharger un média (max: 50 Mo)", + marketSearchLabel: "Rechercher", marketSearchPlaceholder: "Rechercher par titre ou tags", marketMinPriceLabel: "Prix minimum", marketMaxPriceLabel: "Prix maximum", + marketSortLabel: "Trier par", marketSortRecent: "Les plus récents", marketSortPrice: "Prix", marketSortDeadline: "Date limite", marketSearchButton: "Rechercher", marketAuctionEndsIn: "Se termine", marketAuctionEnded: "Terminé", + marketMyBidBadge: "Vous avez enchéri", marketNoItemsMatch: "Aucun article ne correspond à votre recherche.", - housingTitle: "Logement", - housingDescriptionText: "Découvrez et gérez des lieux dans votre réseau.", - modulesHousingLabel: "Logement", - modulesHousingDescription: "Module pour découvrir et gérer des lieux.", - typeHousing: "LOGEMENT", - housingFilterAll: "TOUT", - housingFilterMine: "À MOI", - housingFilterRequested: "DEMANDÉS", - housingFilterSale: "VENTE", - housingFilterRent: "LOCATION", - housingFilterCouchsurfing: "COUCHSURFING", - housingFilterOpen: "DISPONIBLES", - housingFilterClosed: "INDISPONIBLES", - housingFilterRecent: "RÉCENTS", - housingFilterTop: "TOP", - housingType: "Offre", - housingTypeSALE: "À vendre", - housingTypeRENT: "À louer", - housingTypeCOUCHSURFING: "Couchsurfing", - housingProperty: "Bien", - housingPropertyAPARTMENT: "Appartement", - housingPropertyHOUSE: "Maison", - housingPropertyROOM: "Chambre", - housingPropertyLAND: "Terrain", - housingPropertyOTHER: "Autre", - housingTitleLabel: "Titre", - housingTitlePlaceholder: "Nom du lieu", - housingDescription: "Description", - housingDescriptionPlaceholder: "Décrivez le lieu, le quartier, ce qui est inclus : salles de bain, jardin, lits…", - housingRules: "Règles", - housingRulesPlaceholder: "Animaux, tabac, heures calmes, séjour minimum…", - housingPlace: "Lieu", - housingPlacePlaceholder: "Pays, ville, adresse", - housingPrice: "Prix", - housingPriceLabel: "Prix (ECO)", - housingSizeLabel: "Surface (m²)", - housingFree: "GRATUIT", - housingPerMonth: "/mois", - housingRooms: "Pièces", - housingSize: "Surface", - housingCapacity: "Voyageurs", - housingAvailableFrom: "Disponible à partir du", - housingAvailableTo: "Disponible jusqu'au", - housingTags: "Étiquettes", - spreadEditWarning: "Ce contenu perdra ses partages après modification", - housingRequests: "Demandes", - housingRequestButton: "Demander", - housingCancelButton: "Annuler la demande", - housingRequestedBadge: "DEMANDÉ", - housingStatus: "Statut", - housingStatusOPEN: "DISPONIBLE", - housingStatusCLOSED: "INDISPONIBLE", - housingSetOpen: "Rendre disponible", - housingSetClosed: "Rendre indisponible", - housingCreateButton: "Créer un lieu", - housingUpdateButton: "Mettre à jour", - housingDeleteButton: "Supprimer", - housingSearchPlaceholder: "Rechercher des lieux...", - housingSearchButton: "Rechercher", - housingMinPrice: "Prix min", - housingMaxPrice: "Prix max", - housingSortRecent: "Plus récents", - housingSortPrice: "Prix le plus bas", - housingSortRequests: "Plus demandés", - housingSortRating: "Mieux notés", - housingNoItems: "Aucun lieu trouvé.", jobsTitle: "Offres d’emploi", jobsCandidatesTitle: "Candidats suggérés", jobsCandidatesDescription: "Habitants dont les compétences correspondent à cet emploi. Envoyez-leur un message privé.", @@ -2534,6 +2833,7 @@ module.exports = { jobLocationPresencial: "Présentiel", jobLocationRemote: "À distance", jobVacantsPlaceholder: "Nombre de postes vacants", + jobSalaryPlaceholder: "Salaire en ECO pour 1 heure", jobImage: "Télécharger un média (max: 50 Mo)", jobTasks: "Tâches", jobType: "Type d’emploi", @@ -2543,6 +2843,7 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Emplois les mieux payés", createJobButton: "Publier un emploi", + viewDetailsButton: "Voir les détails", noJobsFound: "Aucune offre d’emploi trouvée.", jobAuthor: "Par", jobTypeFreelance: "Autonome", @@ -2554,6 +2855,10 @@ module.exports = { jobsHoursOffered: "Heures offertes", jobsHoursRequested: "Heures demandées en échange", jobsExchangeSkill: "Compétence demandée en échange", + jobsHoursOfferedPlaceholder: "ex. 4", + jobsHoursRequestedPlaceholder: "ex. 4", + jobsExchangeSkillPlaceholder: "ex. menuiserie, design", + jobExchangeHint: "Pour les emplois en échange d’heures, remplissez les champs ci-dessous au lieu du salaire.", jobTimePartial: "Partiel", jobTimeComplete: "Complet", jobsDeleteButton: "SUPPRIMER", @@ -2561,201 +2866,31 @@ module.exports = { jobsFilterApplied: "POSTULÉ", jobsAppliedTitle: "Mes candidatures", jobsAppliedBadge: "Candidaté", + jobsFilterFavs: "Favoris", + jobsFavsTitle: "Favoris", + jobsFilterNeeds: "Besoin d'aide", + jobsNeedsTitle: "Besoin d'aide", + jobsSearchLabel: "Recherche", jobsSearchPlaceholder: "Rechercher titre, tags, description...", jobsMinSalaryLabel: "Salaire min.", jobsMaxSalaryLabel: "Salaire max.", + jobsSortLabel: "Trier par", jobsSortRecent: "Plus récents", jobsSortSalary: "Salaire le plus élevé", jobsSortSubscribers: "Plus de candidats", jobsSearchButton: "Rechercher", + jobsFavoriteButton: "Favori", + jobsUnfavoriteButton: "Retirer favori", + jobsMessageAuthorButton: "MP", jobsApplicants: "Candidats", jobsUpdatedAt: "Mis à jour", jobSetOpen: "Marquer ouvert", + jobNewBadge: "NOUVEAU", jobsTagsLabel: "Étiquettes", + jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "Aucune offre ne correspond à votre recherche.", - industrySearchPlaceholder: "Rechercher des usines…", - industryAllSectors: "Tous les secteurs", - industryVisitButton: "Visiter l’industrie", - industryAdvanceTo: "Passer à", - industryAmount: "Montant", - industryApproveBuild: "Approuver", - industryBlueprint: "Blueprint", - industryBlueprints: "Blueprints", - industryBuild: "Build", - industryBuildNotes: "Notes", - industryBuildStart: "Date de début", - industryVoteUpdateButton: "Voter la mise à jour", - industryVoteDeleteButton: "Voter la suppression", - industryRevokeVote: "Révoquer", - industryTimeLeft: "Temps restant", - industryBuildEnd: "Date de fin", - industryBuilds: "Builds", - industryBuildTitle: "Titre", - industryContribute: "Contribuer", - industryContributeButton: "Contribuer", - industryContributions: "Contributions", - industryCreateBlueprintButton: "Créer un blueprint", - industryDistribute: "Distribuer", - industryDistributeButton: "Distribuer par parts", - industryDistribution: "Distribution", - industryEcoAmount: "Montant (ECO)", - industryEditBlueprint: "Modifier le blueprint", - industryFacility: "Usine", - industryKindLabel: "Type", - industryKind_labor: "Main-d'œuvre", - industryKind_material: "Matériau", - industryKind_eco: "Fonds", - industryLaborHours: "Main-d'œuvre (heures)", - industryHours: "Heures", - industryMaterialItem: "Matériel", - industryMaterials: "Matériaux", - industryEstMaterials: "Total matériaux", - industryEstLabor: "Total main-d'œuvre", - industryEstTotal: "Prix estimé", - industryBuildingPrice: "Prix de fabrication", - industryFinalPrice: "Prix final", - industryBlueprintDescPlaceholder: "Spécifications, dimensions, poids, documentation…", - industryMaterialValue: "Valeur (ECO)", - industryMember: "Membre", - industryNewBlueprint: "Nouveau blueprint", - industryNewBuild: "Proposer un build", - industryEditBuild: "Modifier la production", - industryNeedBlueprint: "Créez d'abord un plan : chaque production en fabrique un.", - industryNoBlueprints: "Aucun blueprint pour l'instant.", - industryNoBuilds: "Aucun build pour l'instant.", - industryNote: "Note", - industryOutput: "Sortie", - industryOutputKind: "Type de produit", - industryOutputValue: "Valeur de sortie (ECO, p. ex. de la vente)", - industryPostJob: "Créer un emploi", - industryPot: "Cagnotte", - industryProposeBuildButton: "Proposer le build", - industryProposer: "Proposant", - industryAuthor: "Auteur", - industrySellOnMarket: "Envoyer au marché", - industrySendToProjects: "Créer un projet", - industrySkills: "Compétences", - industryTreasury: "Trésorerie", - industryKind_physical: "Physique", - industryKind_digital: "Numérique", - industryBuildStatus_PROPOSED: "PROPOSÉ", - industryBuildStatus_REJECTED: "REJETÉ", - industryBuildStatus_APPROVED: "APPROUVÉ", - industryBuildStatus_STOCKING: "APPROVISIONNEMENT", - industryBuildStatus_IN_PRODUCTION: "EN PRODUCTION", - industryBuildStatus_COMPLETED: "TERMINÉ", - industryBuildStatus_FAILED: "ÉCHOUÉ", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "Une offre correspond à votre CV", - jobsBotMatchIntro: "Une offre correspond à votre CV.", - jobsBotMatchJob: "Emploi", - jobsBotMatchHint: "Vous recevez ceci parce que votre CV est géré par l'IA. Vous pouvez le désactiver dans votre CV.", - cvAiManaged: "Géré par l'IA", - switchOn: "ACTIVÉ", - switchOff: "DÉSACTIVÉ", - cvAiManagedHint: "Si activé, JobsBot vous prévient par message privé quand une offre correspond à votre CV.", - cvMatchThreshold: "Me prévenir à partir de ce niveau de correspondance", - housingBotRequestedTitle: "Quelqu’un a demandé l’un de vos lieux.", - housingBotCancelledTitle: "Une demande sur l’un de vos lieux a été annulée.", - housingBotYouRequestedTitle: "Vous avez demandé un lieu.", - housingBotYouCancelledTitle: "Vous avez annulé une demande.", - housingBotUnavailableTitle: "Un lieu que vous aviez demandé n’est plus disponible.", - housingBotUnavailableSentTitle: "Vous avez prévenu les personnes qui avaient demandé ce lieu.", - industryBotAdmittedTitle: "Vous avez été admis dans une usine.", - industryBotApplicationTitle: "Nouvelle demande d'adhésion dans votre usine.", - industryBotInvitedTitle: "Vous avez été invité dans une usine.", - industryBotDissolvedTitle: "Une usine dont vous faites partie a été dissoute.", - industryBotBuildApprovedTitle: "Un build a été approuvé.", - industryBotDistributedTitle: "La sortie d'un build a été distribuée.", - industryFilterRules: "RÈGLES", - industryRulesTitle: "Règles", - industryRulesIntro: "Industry permet aux habitants de posséder collectivement les moyens de production : une Usine est un atelier du réseau que personne ne possède à titre privé.", - industryRulesSteward: "Le fondateur est l'intendant — un gardien, pas un propriétaire : il ne peut ni vendre, ni privatiser, ni dissoudre l'Usine seul.", - industryRulesMembership: "La politique d'adhésion définit comment on rejoint : Ouverte (adhésion immédiate), Par vote (admis par le vote des membres) ou Sur invitation (un membre doit inviter d'abord).", - industryRulesQuorum: "Le quorum est le nombre minimum de membres qui doivent voter pour qu'une décision compte, afin qu'une petite Usine ne soit pas décidée par une seule personne.", - industryRulesMajority: "La majorité est la fraction de votes nécessaire (0,5 = moitié, 1 = unanimité) ; une décision passe quand les OUI atteignent au moins max(quorum, membres × majorité).", - industryRulesDecisions: "Les membres votent pour admettre de nouveaux venus, approuver les builds et dissoudre l'Usine ; l'intendant ne peut pas passer outre ces décisions collectives.", - industryRulesBlueprints: "Les Blueprints sont des recettes libres (COPYLEFT) — les intrants (matériaux, heures de travail, compétences) et la sortie (un bien physique ou numérique) — et chacun peut les forker.", - industryRulesBuilds: "Un Build est un ordre de production : PROPOSÉ → APPROUVÉ (par vote) → APPROVISIONNEMENT → EN PRODUCTION → TERMINÉ, et il n'accepte des contributions qu'une fois approuvé.", - industryRulesContributions: "Les membres contribuent en travail (heures), matériaux (valeur ECO déclarée) ou ECO à un build ; chaque contribution rapporte du karma.", - industryRulesLaborRate: "Le taux de travail est la valeur ECO d'une heure de travail dans cette Usine ; il convertit les heures en karma pour comparer équitablement effort et capital : karma = heures × taux + valeur matériaux + ECO.", - industryRulesShares: "Votre part d'un build est votre karma ÷ karma total, et fixe la part de la valeur de sortie que vous recevez.", - industryRulesDistribution: "Lorsqu'une production est terminée, le steward répartit le pot (les fonds de la production plus la valeur de vente) entre les contributeurs, au prorata de leurs parts.", - industryRulesTreasury: "Les contributions en ECO sont détenues par l'intendant comme dépositaire de la trésorerie du build jusqu'à la distribution ; tous les mouvements sont enregistrés sur le réseau et les litiges peuvent aller aux Courts.", - industryJobs: "Emplois", - industryReportButton: "Signaler", - industryCreateTask: "Créer une tâche", - industryOpenDispute: "Ouvrir un litige", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "Générer une invitation", - industryPauseVotes: "Votes de statut", - industryTitle: "Industrie", - industryDescription: "Module pour gérer collectivement les moyens de production.", - typeIndustryBuild: "BUILD", - typeIndustryBlueprint: "BLUEPRINT", - typeIndustryAllocation: "DISTRIBUTION", - typeIndustry: "INDUSTRIE", - modulesIndustryLabel: "Industrie", - modulesIndustryDescription: "Module pour gérer collectivement les moyens de production.", - industryFilterAll: "TOUTES", - industryFilterMember: "MEMBRE", - industryFilterBlueprints: "PLANS", - industryFilterBuilds: "PRODUCTIONS", - industryFilterMine: "MIENNES", - industryFilterActive: "EN MARCHE", - industryFilterPaused: "EN PAUSE", - industryFilterDissolved: "DISSOUTES", - industryAllTitle: "Industrie", - industryMemberTitle: "Usines dont je fais partie", - industryMineTitle: "Mes usines", - industryActiveTitle: "En marche", - industryPausedTitle: "Usines en pause", - industryDissolvedTitle: "Usines dissoutes", - industryNoFacilitiesFound: "Aucune usine trouvée.", - industryCreateFacility: "Créer une usine", - industryMembers: "Membres", - industryMemberBadge: "MEMBRE", - industryName: "Nom", - industryNamePlaceholder: "Nom de l'usine", - industryDescriptionLabel: "Description", - industryDescriptionPlaceholder: "Que produit cette usine ?", - industrySector: "Secteur", - industryMembershipPolicy: "Politique d'adhésion", - industryQuorum: "Quorum", - industryMajority: "Majorité", - industryLaborRate: "Taux de travail", - industryCreateButton: "Créer une usine", - industryUpdateButton: "Mettre à jour", - industrySteward: "Intendant", - industryStatusActive: "EN MARCHE", - industryStatusPaused: "EN PAUSE", - industryStatusDissolved: "DISSOUTE", - industryStatusLabel: "État", - industryJoinButton: "Rejoindre", - industryApplyButton: "Demander à rejoindre", - industryLeaveButton: "Quitter", - industryInviteButton: "Inviter", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "Vous avez besoin d'une invitation pour rejoindre.", - industryPauseButton: "Voter la pause", - industryResumeButton: "Voter la reprise", - industryDeleteButton: "Supprimer", - industryPendingApplicants: "Candidatures en attente", - industryVotesYes: "oui", - industryVotesNo: "non", - industryAlreadyVoted: "voté", - industryAdmit: "Admettre", - industryReject: "Rejeter", - industryDissolveTitle: "Dissoudre l'usine", - industryDissolveVote: "Voter la dissolution", - industryPolicy_open: "Ouverte", - industryPolicy_vote: "Par vote", - industryPolicy_invite: "Sur invitation", projectsTitle: "Projets", projectsDescription: "Créez, financez et suivez des projets communautaires dans votre réseau.", - projectSearchPlaceholder: "Rechercher des projets...", projectCreateProject: "Créer un projet", projectCreateButton: "Créer un projet", projectUpdateButton: "METTRE À JOUR", @@ -2799,8 +2934,14 @@ module.exports = { projectPledgeTitle: "Soutenez ce projet", projectPledgePlaceholder: "Montant en ECO", projectBounties: "Récompenses", + projectBountiesInputLabel: "Récompenses (une par ligne : Titre|Montant [ECO]|Description)", + projectBountiesPlaceholder: "Réparer un bug UI|100|Lien vers le problème\nRédiger de la documentation|250|Exemples d'utilisation", projectNoBounties: "Aucune récompense trouvée.", projectTitle: "Titre", + projectAddBountyTitle: "Ajouter une nouvelle récompense", + projectBountyTitle: "Titre de la récompense", + projectBountyAmount: "Montant (ECO)", + projectBountyDescription: "Description", projectMilestoneSelect: "Sélectionner un jalon", projectBountyCreateButton: "Créer une récompense", projectBountyStatus: "Statut de la récompense", @@ -2815,15 +2956,19 @@ module.exports = { projectMilestoneTitle: "Titre du jalon", projectMilestoneTargetPercent: "Pourcentage (%)", projectMilestoneDueDate: "Date", + projectMilestoneCreateButton: "Créer un jalon", projectMilestoneStatus: "Statut du jalon", projectMilestoneOpen: "ouvert", projectMilestoneDone: "complété", projectMilestoneDue: "échéance", + projectNoMilestones: "Aucun jalon trouvé.", projectMilestoneMarkDone: "Marquer comme fait", projectMilestoneTitlePlaceholder: "Entrez le titre du jalon", projectMilestoneDescriptionPlaceholder: "Entrez la description de ce jalon", projectMilestoneDescription: "Description du jalon", + projectBudgetGoal: "Budget (Objectif)", projectBudgetAssigned: "Assigné aux récompenses", + projectBudgetRemaining: "Restant", projectBudgetOver: "⚠ Budget dépassé : ce qui est assigné dépasse l’objectif", projectFollowers: "Suiveurs", projectFollowersTitle: "Suiveurs", @@ -2836,6 +2981,7 @@ module.exports = { projectBackersTotalPledged: "Total engagé", projectBackersYourPledge: "Votre engagement", projectBackersNone: "Pas encore d'engagement.", + projectNoRemainingBudget: "Aucun budget restant.", projectFilterApplied: "POSTULÉ", projectAppliedTitle: "POSTULÉ", projectFilterBackers: "MÉCÈNES", @@ -2844,15 +2990,30 @@ module.exports = { projectBackerAmount: "Total engagé", projectBackerPledges: "Engagements", projectBackerProjects: "Projets", + projectPledgeAmount: "Montant", projectSelectMilestoneOrBounty: "Sélectionner un jalon ou une récompense", projectPledgeButton: "Soutenir", footerLicense: "GPLv3", + footerPackage: "Paquet", + footerVersion: "Version", modulesModuleName: "Nom", modulesModuleDescription: "Description", modulesModuleStatus: "Statut", modulesTotalModulesLabel: "Modules chargés", modulesEnabledModulesLabel: "Activés", modulesDisabledModulesLabel: "Désactivés", + modulesPopularLabel: "Populaire", + modulesPopularDescription: "Module pour recevoir les publications tendance, les plus vues ou les plus commentées.", + modulesTopicsLabel: "Sujets", + modulesTopicsDescription: "Module pour recevoir des catégories de discussion basées sur des intérêts partagés.", + modulesSummariesLabel: "Résumé", + modulesSummariesDescription: "Module pour recevoir des résumés de discussions ou publications longues.", + modulesLatestLabel: "Derniers", + modulesLatestDescription: "Module pour recevoir les publications et discussions les plus récentes.", + modulesThreadsLabel: "Fils", + modulesThreadsDescription: "Module pour recevoir des conversations groupées par sujet ou question.", + modulesMultiverseLabel: "Multivers", + modulesMultiverseDescription: "Module pour recevoir du contenu d’autres pairs fédérés.", modulesInvitesLabel: "Invitations", modulesInvitesDescription: "Module pour gérer et appliquer des codes d'invitation.", modulesWalletLabel: "Portefeuille", @@ -2897,12 +3058,8 @@ module.exports = { modulesOpinionsDescription: "Module pour découvrir et voter des opinions.", modulesTransfersLabel: "Transferts", modulesTransfersDescription: "Module pour découvrir et gérer les contrats intelligents (transferts).", - modulesFediverseLabel: "Multivers", - modulesBlogsLabel: "Blogs", - modulesBlogsDescription: "Module pour découvrir et gérer les blogs.", - modulesPollsLabel: "Sondages", - modulesPollsDescription: "Module pour interroger le réseau et compter les réponses.", - modulesFediverseDescription: "Gérez vos autres comptes du multiverse, y compris l'envoi et la réception de contenu.", + modulesFediverseLabel: "Fediverse", + modulesFediverseDescription: "Gérez vos autres comptes du fédiverse, y compris l'envoi et la réception de contenu.", modulesFeedLabel: "Fil", modulesFeedDescription: "Module pour découvrir et partager des textes courts (flux).", modulesPixeliaLabel: "Pixelia", @@ -2934,44 +3091,44 @@ module.exports = { visibilityMakeHidden: "Cacher", invitesInhabitantsTitle: "Habitants", invitesInhabitantsFollow: "Suivre", - aiNavPlaceholder: "Décrivez ce qu'il vous faut...", + aiNavPlaceholder: "Où veux-tu aller ?", aiNavDisabled: "La navigation par IA est désactivée.", - aiNavResultsTitle: "Suggestions AINav", + aiNavResultsTitle: "Suggestions de navigation IA", aiNavQueryLabel: "Requête", + aiNavResultsDescription: "Choisissez l'itinéraire qui correspond le mieux à ce que vous cherchez.", + aiNavResultPath: "Route", + aiNavResultDescription: "Ce que vous pouvez y faire", aiNavResultMatch: "Correspondance", aiNavResultsEmpty: "Aucune route correspondante. Essayez /search.", + aiNavSearchInstead: "Rechercher plutôt", modulesAINavLabel: "AINav", modulesAINavDescription: "Module pour des requêtes en langage naturel sur le contenu du réseau.", + directConnect: "Connexion Directe", directConnectDescription: "Connectez-vous directement à un pair en saisissant son adresse IP, port et clé publique. Le pair sera ajouté comme connexion suivie.", peerHost: "IP", peerPort: "Port (par défaut : 8008)", peerPublicKey: "Clé publique (@...ed25519)", connectAndFollow: "Connecter", + deviceSourceLabel: "Appareil", modulesPresetTitle: "Configurations Communes", - workflowsTitle: "Workflows", - workflowsDescription: "Un workflow définit le thème et les modules actifs.", - workflowsSet: "Appliquer le workflow", - workflow_default: "Par défaut", - workflow_jobs: "Emploi", - workflow_ruling: "Gouvernance", - workflow_politics: "Politique", - workflow_social: "Social", - workflow_business: "Affaires", - workflow_night: "Nuit", - workflow_mobile: "Mobile", + modulesPreset_minimal: "Minimal", + modulesPreset_basic: "Basique", + modulesPreset_social: "Social", + modulesPreset_economy: "Économie", + modulesPreset_full: "Complet", statsCarbonFootprintTitle: "Empreinte Carbone", + statsCarbonFootprintNetwork: "Empreinte carbone du réseau", + statsCarbonFootprintYours: "Votre empreinte carbone", statsCarbonTombstone: "Empreinte du tombstoning", + feedSuccessMsg: "Feed publié avec succès !", + dominantOpinionLabel: "Opinion dominante", uploadMedia: "Télécharger un média (max: 50 Mo)", invitesUnfollow: "Ne plus suivre", invitesFollow: "Suivre", invitesUnfollowedInvites: "Non fédérés", invitesNoUnfollowed: "Aucun pub non fédéré.", blockchainContentDeleted: "Ce contenu a été supprimé", - visitContent: "Visiter le Contenu", - favoriteAdd: "Épingler aux Favoris", - pmContentTooltip: "Envoyer un Message Privé", - favoriteRemove: "Retirer des Favoris", - reportContent: "Signaler le Contenu", + visitContent: "Voir le contenu", bankEcoinHours: "Équivalence ECOin en temps", mapsLabel: "Cartes", mapTitle: "Cartes", @@ -2999,13 +3156,14 @@ module.exports = { mapDescriptionLabel: "Description", mapDescriptionPlaceholder: "Décrivez la carte ou l'emplacement...", mapTypeLabel: "Type de carte", - mapInviteMode: "Invitation", - mapTypeSingle: "UNIQUE", - mapTypeOpen: "OUVERT", - mapTypeClosed: "FERMÉ", + mapTypeSingle: "UNIQUE (emplacement initial uniquement)", + mapTypeOpen: "OUVERT (tout le monde peut ajouter des marqueurs)", + mapTypeClosed: "FERMÉ (seul le créateur ajoute des marqueurs)", mapTagsLabel: "Étiquettes", mapTagsPlaceholder: "Entrez des étiquettes séparées par des virgules", mapUrlLabel: "URL de la carte", + mapPickCoordLabel: "Ou sélectionnez un emplacement sur la grille :", + mapMarkersLabel: "marqueurs", mapMarkersTitle: "Marqueurs", mapMarkerDefault: "Marqueur", mapMarkerLatLabel: "Latitude du marqueur", @@ -3032,9 +3190,14 @@ module.exports = { padTitle: "Pad", modulesPadsLabel: "Pads", modulesPadsDescription: "Module pour gérer les éditeurs de texte collaboratifs.", + padFilterAll: "TOUS", + padFilterMine: "MES PADS", + padFilterRecent: "RÉCENT", + padFilterOpen: "OUVERT", + padFilterClosed: "FERMÉ", padCreate: "Créer Pad", - padUpdate: "Modifier", - padDelete: "Supprimer", + padUpdate: "Modifier Pad", + padDelete: "Supprimer Pad", padTitleLabel: "Titre", padTitlePlaceholder: "Entrez le titre du pad...", padStatusLabel: "Statut", @@ -3045,7 +3208,14 @@ module.exports = { padTagsLabel: "Tags", padTagsPlaceholder: "tag1, tag2, ...", padMembersLabel: "Membres", + padVisitPad: "Visiter le Pad", + padShareUrl: "Partager URL", padCreated: "Créé", + padAuthor: "Auteur", + padGenerateCode: "Générer un Code", + padInviteCodeLabel: "Code d'Invitation", + padInviteCodePlaceholder: "Entrez le code d'invitation...", + padValidateInvite: "Valider", padStartEditing: "COMMENCER À ÉDITER !", padEditorPlaceholder: "Commencez à écrire...", padSubmitEntry: "Envoyer", @@ -3058,11 +3228,12 @@ module.exports = { padClosedSectionTitle: "Pads Fermés", padCreateSectionTitle: "Créer un Nouveau Pad", padUpdateSectionTitle: "Modifier le Pad", + padInviteGenerated: "Code d'Invitation Généré", typePad: "PADS", padNew: "NOUVEAU", padAddFavorite: "Ajouter aux Favoris", padRemoveFavorite: "Retirer des Favoris", - padClose: "Fermer", + padClose: "Fermer le pad", padBackToEditor: "Retour à l'éditeur", padSearchPlaceholder: "Rechercher des pads...", @@ -3070,6 +3241,8 @@ module.exports = { modulesChatsDescription: "Module pour découvrir et gérer les chats chiffrés.", typeChat: "CHATS", typeChatMessage: "MESSAGE CHAT", + chatLabel: "CHATS", + chatMessageLabel: "MESSAGES CHAT", chatsTitle: "Chats", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3079,50 +3252,52 @@ module.exports = { chatDescription: "Description", chatCategory: "Catégorie", chatStatus: "STATUT", + chatFilterAll: "TOUS", + chatFilterMine: "MIEN", + chatFilterRecent: "RÉCENT", + chatFilterFavorites: "FAVORIS", + chatFilterOpen: "OUVERT", + chatFilterClosed: "FERMÉ", chatCreate: "Créer Chat", - chatUpdate: "Mettre à jour", - chatDelete: "Supprimer", + chatUpdate: "Modifier Chat", + chatDelete: "Supprimer Chat", chatClose: "Fermer Chat", chatVisitChat: "VOIR CHAT", chatUntitled: "Chat sans titre", chatNoItems: "Aucun chat trouvé.", chatParticipants: "Participants", - chatMessagesLabel: "Messages", - chatThreadsLabel: "Fils", + chatStartChatting: "COMMENCER À CHATTER!", + chatGenerateCode: "Générer Code", + chatShareUrl: "Partager URL", chatCreatedAt: "CRÉÉ", chatSearchPlaceholder: "Rechercher chats...", chatStatusOpen: "OUVERT", chatStatusInviteOnly: "SUR INVITATION", chatStatusClosed: "FERMÉ", chatSendMessage: "Envoyer", - chatJumpLatest: "Dernier message", - chatPickChat: "Choisissez un chat pour l'ouvrir", - chatNoneYet: "Aucun chat disponible pour l'instant.", - activitySearchPlaceholder: "Chercher dans l'activité...", - trendingSearchPlaceholder: "Chercher dans les tendances...", - opinionsSearchPlaceholder: "Chercher dans les opinions...", - votesSearchPlaceholder: "Chercher dans les votations...", - welcomeStepUxTitle: "Choisissez votre interface", - welcomeStepUxText: "Choisissez l'apparence d'Oasis. Modifiable ensuite dans les réglages.", - welcomeStepUxAction: "Utiliser cette vue", - chatRateLimitTitle: "Trop de messages", - chatRateLimitMessage: "Vous avez atteint la limite de messages que ce salon accepte en une heure. Réessayez plus tard.", chatMessagePlaceholder: "Tapez votre message...", chatNoMessages: "Pas encore de messages.", + chatLeave: "Quitter Chat", + chatInviteCodeLabel: "Entrez le code d'invitation", + chatJoinByInvite: "Rejoindre", chatTitlePlaceholder: "Titre du chat", chatDescriptionPlaceholder: "Description du chat", chatTagsPlaceholder: "tag1, tag2, tag3", chatImageLabel: "Sélectionnez un fichier image (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "Code d'Invitation", chatAuthor: "Auteur", + chatCreated: "Créé", chatAddFavorite: "Ajouter aux Favoris", chatRemoveFavorite: "Retirer des Favoris", chatPM: "MP", chatStatusLabel: "Statut", chatCategoryLabel: "Catégorie", + chatParticipantsLabel: "Participants", gamesTitle: "Jeux", gamesDescription: "Découvrez et jouez à des jeux.", gamesFilterAll: "TOUS", gamesPlayButton: "JOUER!", + gamesBackToGames: "Retour aux Jeux", modulesGamesLabel: "Jeux", modulesGamesDescription: "Module pour découvrir et jouer à des jeux.", modulesGraphosLabel: "Graphos", @@ -3139,6 +3314,8 @@ module.exports = { gamesArkanoidDesc: "Cassez toutes les briques avec votre raquette et votre balle. Un défi arcade classique.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Ping-pong classique contre une IA. Le premier à 5 points gagne.", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "Course contre la montre! Évitez les obstacles et atteignez l'arrivée à temps.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Pilotez votre vaisseau dans un champ d'astéroïdes. Détruisez-les avant qu'ils ne vous touchent.", gamesRockPaperScissorsTitle: "Pierre Feuille Ciseaux", @@ -3159,6 +3336,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3169,6 +3348,12 @@ module.exports = { modulesCalendarsLabel: "Calendriers", modulesCalendarsDescription: "Module pour découvrir et gérer les calendriers.", typeCalendar: "CALENDRIERS", + calendarFilterAll: "TOUS", + calendarFilterMine: "LES MIENS", + calendarFilterOpen: "OUVERT", + calendarFilterClosed: "FERMÉ", + calendarFilterRecent: "RÉCENTS", + calendarFilterFavorites: "FAVORIS", calendarCreate: "Créer un calendrier", calendarUpdate: "Mettre à jour", calendarDelete: "Supprimer", @@ -3181,17 +3366,24 @@ module.exports = { calendarTagsLabel: "Étiquettes", calendarTagsPlaceholder: "tag1, tag2...", calendarParticipantsLabel: "Participants", + calendarParticipantsCount: "Participants", + calendarVisitCalendar: "Visiter le calendrier", calendarCreated: "Créé", + calendarAuthor: "Auteur", calendarJoin: "Rejoindre", calendarGenerateInvite: "Générer une invitation", + calendarInviteCodePlaceholder: "Entrez le code d'invitation...", + calendarValidateInvite: "Valider le code", + calendarJoined: "Rejoint", + calendarAddDate: "Ajouter une date", + calendarAddNote: "Ajouter une note", calendarDateLabel: "Date", calendarDatePlaceholder: "Décrivez cette date...", - calendarNoteLabel: "Notes", + calendarNoteLabel: "Note", calendarNotePlaceholder: "Ajouter une note...", calendarFirstDateLabel: "Date", calendarFirstNoteLabel: "Notes", calendarIntervalLabel: "Interval", - calendarIntervalNone: "Sans répétition", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3202,6 +3394,8 @@ module.exports = { calendarsDescription: "Découvrez et gérez les calendriers de votre réseau.", calendarMonthPrev: "← Précédent", calendarMonthNext: "Suivant →", + calendarMonthLabel: "Dates", + calendarsShareUrl: "URL de partage", calendarAllSectionTitle: "Calendriers", calendarRecentSectionTitle: "Calendriers Récents", calendarFavoritesSectionTitle: "Favoris", @@ -3215,6 +3409,7 @@ module.exports = { calendarRemoveFavorite: "Retirer des favoris", calendarSearchPlaceholder: "Rechercher des calendriers...", calendarAddEntry: "Ajouter une entrée", + calendarLeave: "Quitter le calendrier", statsCalendar: "Calendriers", statsCalendarDate: "Dates de calendrier", statsCalendarNote: "Notes de calendrier", @@ -3261,6 +3456,7 @@ module.exports = { torrentSortOldest: "Plus anciens", torrentSortTop: "Plus votés", torrentNoMatch: "Aucun torrent correspondant.", + torrentMessageAuthorButton: "Envoyer un Message à l'Auteur", torrentUpdatedAt: "Mis à jour", statsTorrent: "Torrents", typeTorrent: "TORRENTS", @@ -3268,18 +3464,10 @@ module.exports = { modulesTorrentsDescription: "Module pour découvrir et gérer les torrents.", favoritesFilterTorrents: "TORRENTS", favoritesFilterMarket: "MARCHÉ", - favoritesFilterEvents: "ÉVÉNEMENTS", - favoritesFilterForum: "FORUM", - favoritesFilterHousing: "LOGEMENT", - favoritesFilterJobs: "EMPLOIS", - favoritesFilterProjects: "PROJETS", - favoritesFilterReports: "RAPPORTS", - favoritesFilterTasks: "TÂCHES", - favoritesFilterTransfers: "TRANSFERTS", - favoritesFilterVotes: "VOTES", favoritesFilterShopProducts: "ARTICLES BOUTIQUE", tribeSectionTorrents: "TORRENTS", tribeCreateTorrent: "Téléverser un Torrent", + tribeMediaTypeTorrent: "Torrent", settingsWishTitle: "Souhait", settingsWishDesc: "Configurez le souhait de votre Avatar.", settingsWishWhole: "Multiverse", @@ -3290,10 +3478,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "L'envoi est un garde-fou UX. La réception est un filtre d'attention.", + pmBlockedNonMutual: "Vous ne pouvez envoyer des MP qu'aux habitants en soutien mutuel.", inhabitantsPendingFollowsTitle: "Demandes de soutien en attente", inhabitantsPendingAccept: "Accepter", inhabitantsPendingReject: "Refuser", + bxEncrypted: "CHIFFRÉ", + bxEncryptedHexLabel: "Texte chiffré (aperçu)", tribeSectionGovernance: "GOUVERNANCE", + tribeSubStatusPublic: "PUBLIQUE", + tribeSubStatusPrivate: "PRIVÉE", + tribeSubInheritedPrivate: "PRIVÉE (héritée de la tribu principale)", tribePadInviteRequired: "Vous n'avez pas accès au pad. Demandez une invitation.", tribeChatInviteRequired: "Vous n'avez pas accès au chat. Demandez une invitation.", tribeGovernanceDesc: "Gouvernance interne de cette tribu.", @@ -3333,31 +3527,44 @@ module.exports = { tribeGovNoHistorical: "Aucun enregistrement pour l’instant", logsTitle: "Journal", logsDescription: "Enregistrez votre expérience sur le réseau.", + logsReadMore: "Lire la suite", logsViewTitle: "Journal", logsColumnType: "Type", logsView: "Voir", + logsManualPrompt: "Écrivez votre journal", logsUpdateButton: "Mettre à jour", - logsEditTitle: "Mettre à jour le journal", + logsEditTitle: "Éditer une entrée", + logsCreateDescription: "Enregistrez votre expérience...", logsCreateTitle: "Créer une entrée", logsWriteButton: "Écrire", logsTextPlaceholder: "Décrivez vos expériences...", + logsTextField: "Texte", + logsLabelPlaceholder: "Libellé...", + logsLabelField: "Écrivez votre journal (libellé)", + logsAiDisabledWarn: "Activez le module IA dans /modules pour utiliser les entrées écrites par l'IA.", + logsAiContextValue: "Jour de blockchain", + logsAiContext: "Contexte", + logsAiModStatus: "AImod", + logsModeApply: "Appliquer !", logsModeAIWritten: "AI-Assistant", logsModeManual: "Manuel", logsModeAI: "IA", + logsColumnDelete: "Supprimer", + logsColumnEdit: "Éditer", logsColumnLog: "Journal", logsColumnDate: "Date", logsDelete: "Supprimer", - logsEdit: "Mettre à jour", + logsEdit: "Éditer", logsFilterAlways: "TOUJOURS", logsFilterYear: "ANNÉE", logsFilterMonth: "MOIS", logsFilterWeek: "SEMAINE", logsFilterToday: "AUJOURD'HUI", - logsComments: "Commentaires", - logsAuthorEntries: "Entrées", + logsFilterRecent: "RÉCENTS", + logsFilterAll: "TOUS", logsCreate: "Créer une entrée", logsExport: "Exporter le journal", - logsExportOne: "Exporter le journal", + logsExportOne: "Exporter", logsViewDetails: "Voir les détails", logsGenerateButton: "Générer le texte", logsSearchText: "Chercher dans les journaux...", @@ -3367,25 +3574,31 @@ module.exports = { modulesLogsLabel: "Journal", modulesLogsDescription: "Module pour enregistrer (via un assistant IA) vos expériences.", statsLogsTitle: "Journal", + statsLogsEntries: "Entrées", typeLog: "JOURNAL", + blockchainCycle: "Cycle", larpTitle: "L.A.R.P.", larpDescription: "Une couche de jeu de rôle grandeur nature pour l'expérimentation collaborative.", - larpNoHousesMatch: "Aucune maison ne correspond à votre recherche.", - larpSearchPlaceholder: "Rechercher des maisons...", larpAllHouses: "Maisons :", larpFilterRuling: "RÈGNE", larpFilterHouses: "MAISONS", larpFilterRules: "FAQ", larpRulesTitle: "FAQ du L.A.R.P.", + larpRulesEntryTitle: "Maison de départ", larpRulesEntryText: "Chaque habitant commence dans ACADEMIA par défaut. Depuis ACADEMIA, choisis une maison dans le panneau \"Rejoindre une maison\" : cela ouvre son test d'entrée. Réponds aux 10 questions et soumets. Si tu dépasses le seuil (7/10), tu es admis automatiquement dans cette maison ; sinon, tu es rejeté et tu restes dans ACADEMIA. Dans tous les cas, le système enregistre ton OasisID et le cycle de la tentative et empêche un nouvel essai jusqu'à la fin du délai de 30 cycles.", + larpRulesLeaveText: "Les membres de toute maison peuvent retourner à ACADEMIA à tout moment depuis leur page de maison ; cela réinitialise leur appartenance mais ne lève pas le délai du test.", + larpRulesGovernanceTitle: "Mur de gouvernance", larpRulesGovernanceText: "Pendant son cycle, la maison régnante arbore l'insigne \"Règne\" et est la seule dont le Mur est exposé publiquement sous l'onglet RÈGNE.", - larpRulesWallText: "Chaque maison a un Mur où écrivent ses membres. Le Mur d'une maison est privé, sauf celui de la maison au pouvoir et celui de l'ACADEMIA, que tout le monde peut lire.", + larpRulesSignTitle: "Emblème L.A.R.P.", larpRulesSignText: "Les habitants peuvent l'activer (dans /profile/edit > Capteurs) pour afficher l'image de leur maison actuelle comme un sceau sur leur profil, leur fiche d'auteur et d'habitant. Le sceau renvoie à la page de la maison.", larpPostsTitle: "Mur", larpPostsEmpty: "Aucune publication pour le moment.", + larpPostLabel: "Nouvelle publication", larpPostPlaceholder: "Qu'a cette maison à dire ?", + larpPostPublic: "Public (visible par tous, sinon réservé aux membres)", larpPostSubmit: "Publier", + larpPostMembersOnly: "Membres seulement", larpCycleLabel: "Cycle :", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", @@ -3431,9 +3644,12 @@ module.exports = { bankTaxesLookupNote: "Entre un ID de bloc pour le rechercher dans ton flux local.", bankTaxesUserNoteChipsClick: "Clique sur n'importe quel chip pour inspecter son bloc dans le blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntry: "Code d'invitation", larpRulesInviteEntryText: "Les membres des maisons peuvent générer des codes d'invitation qui donnent un accès direct à la maison.", larpRulesOneHouseText: "Tu ne peux appartenir qu'à une seule maison à la fois. Si tu n'appartiens à aucune, tu es dans ACADEMIA. Chaque page de maison (sauf ACADEMIA) affiche un bouton « Quitter la Maison » à ses membres qui remet l'appartenance à ACADEMIA. Quitter n'annule PAS le délai du test.", - larpRulesTestEntryText: "Dans le test WILL, chaque option pondère une ou plusieurs maisons ; à l'envoi, le système additionne les scores et vous admet automatiquement dans la maison la mieux notée.", + larpRulesOneHouseTitle: "Une maison à la fois", + larpRulesTestEntry: "Test WILL", + larpRulesTestEntryText: "Chaque option pondère une ou plusieurs maisons ; lors de l'envoi, le système additionne les scores et t'admet automatiquement dans la maison ayant le score le plus élevé.", larpWillTestHint: "Une fois terminé, on t'assignera la maison qui correspond le mieux à tes réponses. Tes réponses individuelles sont traitées localement et ne sont jamais stockées — seule l'attribution de maison résultante est publiée dans ton flux.", larpWillTestTitle: "Test WILL", settingsLanDesc: "Annoncer périodiquement ce pair aux autres instances d'Oasis sur le même réseau local. Désactiver pour arrêter les diffusions UDP.", @@ -3452,30 +3668,36 @@ module.exports = { aiExchangeMarkHelpful: "+1 utile", larpCyclesUntilRuling: "Prochain cycle de gouvernance", larpVisitTribe: "Visiter la tribu", - larpStartJourney: "Commencer le voyage", - larpGoverning: "Gouverne :", - larpStartHere: "Commencez ici :", + larpGoverning: "Règne :", larpMyHouse: "Ma Maison :", larpMembersCount: "Membres", + larpMembersTitle: "Membres", + larpNoMembers: "Aucun membre pour le moment.", larpRolesLabel: "Rôles", larpFunctionLabel: "Fonction", larpMonthLabel: "Cycle de gouvernance", larpLeaveToAcademia: "Retour à ACADEMIA", - larpAcademiaJoinTitle: "Maisons L.A.R.P.", + larpAcademiaJoinTitle: "Rejoindre une maison", + larpAcademiaJoinHint: "Passe le test psychologique de profil pour être affecté à la maison qui te correspond le mieux, ou utilise un code d'invitation partagé par un membre d'une maison.", + larpTakeTestButton: "Passer le test de profil", larpInviteRedeem: "Rejoindre la Maison", larpInviteCreate: "Générer un code d'invitation", larpInvitePlaceholder: "Code d'invitation", larpInviteBannerTitle: "Nouveau code d'invitation", larpInviteBannerHint: "Partage ce code avec une personne en ACADEMIA. Il expire dans 30 cycles ou après une utilisation.", + larpTestAssignedHouse: "Maison attribuée", larpTestRankingTitle: "Score par maison", invitesHousesTitle: "L.A.R.P.", invitesHouseInviteCodePlaceholder: "Code d'invitation de maison", invitesHouseJoinButton: "Rejoindre la Maison", ecoTaxLabel: "ECO Tax", + ecoinTaxLabel: "ECOin Tax", bankTaxes: "Impôts", bankOverviewYourTaxes: "Tes impôts", + bankTaxesNetworkTitle: "Impôts du réseau", bankTaxesEcoTaxTitle: "ECO Tax", bankTaxesArchTaxTitle: "ARCH Tax", + bankTaxesUserTitle: "Tes impôts", bankTaxesUserAmount: "Ton ECO Tax (prix à rendre)", bankTaxesArchTaxUserAmount: "Ton ARCH Tax (prix à rendre)", bankTaxesArchTaxFirstBlock: "Âge de ton premier bloc", @@ -3526,13 +3748,16 @@ module.exports = { bankRulesEcoTaxRate: "Taux", bankRulesArchTaxRate: "Taux", bankRulesCarbonFactor: "Facteur carbone", + statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "Aucun tombstone dans le réseau pour le moment.", blockchainBlockNotAvailable: "Ce bloc n'est pas disponible dans ton feed local. Il appartient peut-être à un peer dont tu ne réplique pas encore les données.", blockchainBackToBlockexplorer: "Retour au blockexplorer", audioFilterBcs: "BCS", audioTranscodeButton: "TRANSCODE", + audioTranscodeTitle: "BCS Transcode", audioTranscodeDetailTitle: "Transcode", audioTranscodeDetailDescription: "Décode la charge utile incrustée et la carte de composition d'origine de la blockchain.", + audioTranscodeStegoTitle: "Incrusté dans la forme d'onde", audioTranscodeStegoOasisId: "Par", audioTranscodeStegoTimestamp: "Généré le", audioTranscodeStegoMessage: "TEXT", @@ -3540,7 +3765,9 @@ module.exports = { audioTranscodeStegoUnknown: "—", audioTranscodeStegoNotFound: "Aucune charge stéganographique n'a pu être décodée depuis cet audio.", audioTranscodeCompositionEmpty: "Cet audio ne contient pas de composition blockchain enregistrée.", + audioBackToAudio: "Retour à l'audio", audioBackToBcs: "Retour à BCS", + audioAuthorLabel: "Auteur", melodyCompositionTitle: "Carte de composition blockchain", melodyFilterMine: "MIEN", melodyByLabel: "Par", @@ -3554,6 +3781,7 @@ module.exports = { melodyUploadBcsNameNote: "L'audio sera publié sous le nom auto-généré", larpLastAttemptTitle: "Ta dernière tentative", larpLastAttemptHouse: "Maison", + larpLastAttemptResult: "Résultat", larpLastAttemptWhen: "Quand", larpLastAttemptCooldown: "Prochaine tentative dans", larpTestTitle: "Test d'entrée", @@ -3562,14 +3790,29 @@ module.exports = { larpTestCooldownActive: "Prochain test disponible dans", larpTestCooldownDays: "cycles", larpTestResultTitle: "Résultat du test", + larpTestPassed: "Test réussi — bienvenue !", + larpTestFailed: "Test non réussi", larpTestScore: "Score", larpTestNextAttempt: "Tu pourras tenter un autre test dans 30 cycles.", larpGoToHouse: "Aller à ta maison", larpBackToAcademia: "Retour à ACADEMIA", + larpBackToHouses: "◀ Maisons", larpBadgeYou: "Toi", larpBadgeRuling: "Règne", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Module pour la couche de jeu de rôle grandeur nature d'Oasis (les 9 Maisons).", + melodyUploadTitlePlaceholder: "Composition title", + melodyUploadDescPlaceholder: "Optional description", + bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", + bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", + bankTaxesExample: "Example", + bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", + audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + mapClickToCreate: "Click on the map to select a location", + mapClickToAddMarker: "Click on the map to add a marker", + gameScoreLabel: "GAME SCORES", larpProfileQ1: "Face à un problème complexe, que fais-tu en premier ?", larpProfileQ1O1: "Parler aux autres pour trouver un consensus", larpProfileQ1O2: "Construire un prototype à tester", diff --git a/src/client/assets/translations/oasis_hi.js b/src/client/assets/translations/oasis_hi.js index 062769c..4b66a12 100644 --- a/src/client/assets/translations/oasis_hi.js +++ b/src/client/assets/translations/oasis_hi.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { hi: { + bbAdd: "जोड़ें", + bbQuickActions: "त्वरित क्रियाएँ", + bbPickTitle: "एक मॉड्यूल पिन करें", + bbPickDesc: "नीचे की बार में + स्लॉट के लिए एक मॉड्यूल चुनें।", + bbPickNone: "कोई नहीं", + bbManage: "संपादित करें", + bbYourBar: "आपकी बार", + bbRemove: "हटाएँ", + bbFull: "अधिकतम पहुँच गया", + bbDone: "हो गया", + activityChangeFilter: "फ़िल्टर बदलें", + emptyConnectHint: "नेटवर्क से सिंक करने के लिए किसी pub से जुड़ें।", + emptyConnectCta: "pub से जुड़ें", + menuCommunity: "समुदाय", + activityGroupCommunity: "समुदाय और शासन", + activityGroupOrg: "संगठन", + activityGroupComm: "संचार", + activityGroupEconomy: "अर्थव्यवस्था", + activityGroupMedia: "सामग्री और मीडिया", + activityGroupOther: "अन्य", + languageName: "हिन्दी", shopOrderStatusPaid: "भुगतान प्राप्त", shopOrderStatusReceived: "प्राप्त", shopOrderMarkPaid: "भुगतान प्राप्त चिह्नित करें", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "आपके पास अभी कोई खरीद नहीं है।", shopOrderPmSeller: "PM Seller", shopOrderSeller: "विक्रेता", + shopOrderPaymentConcept: "भुगतान", shopOrderInvoice: "चालान", shopOrderInvoiceConcept: "चालान", shopOrderStatusPending: "लंबित", shopOrderStatusAccepted: "स्वीकृत", shopOrderStatusRejected: "अस्वीकृत", shopOrderStatusShipped: "भेजा गया", + shopOrderStatusDelivered: "वितरित", shopOrderAccept: "स्वीकार करें", shopOrderReject: "अस्वीकार करें", shopOrderMarkShipped: "भेजा गया चिह्नित करें", + shopOrderMarkDelivered: "वितरित चिह्नित करें", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "स्मार्ट अनुबंध", shopOrderContractConcept: "दुकान ऑर्डर", shopOrdersPending: "लंबित ऑर्डर", all: "सभी", @@ -50,13 +75,14 @@ module.exports = { viewJson: "JSON देखें", matchScore: "मिलान स्कोर", preferencesLabel: "प्राथमिकताएं", - inhabitantProfileTitle: "निवासी", + inhabitantProfileTitle: "निवासी प्रोफ़ाइल", contentWarningLabel: "सामग्री चेतावनी", invalidPost: "अमान्य सामग्री", invalidPostHint: "इस संदेश में अमान्य/खाली पाठ है।", spreadBy: "द्वारा", spreadChron: "प्रसार", spreaded: "प्रसारित", + spreadMore: "और", spreadContentUnavailable: "सामग्री अभी उपलब्ध नहीं (प्रतिकृति लंबित)", filteredByTag: "टैग द्वारा फ़िल्टर", filteredByTagTitle: "टैग द्वारा फ़िल्टर", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "कोई गंभीरता नहीं", calendarDeleteDate: "तारीख हटाएं", calendarIntervalUntil: "तक", + bookmarkCategory: "श्रेणी", bookmarkLastVisit: "अंतिम विज़िट", profileClearnetBookmarksLabel: "बुकमार्क", + forumFilterHot: "लोकप्रिय", invitesPort: "पोर्ट", currentlyUnrecheable: "त्रुटि!", peerHostValidation: "मान्य IPv4 (जैसे 192.168.1.100) या होस्टनाम (जैसे pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "वार्षिक अधिकतम अनुमान", statsCarbonNetwork: "नेटवर्क कुल", statsCarbonOfEstMax: "अनुमानित अधिकतम क्षमता का", - statsCarbonYearProgress: "वर्ष बीत चुका", - statsNetworkTitle: "नेटवर्क", - statsStorageTitle: "भंडारण", - statsEcoTaxTitle: "ECO कर", - statsAccountTitle: "खाता", - statsAveragesTitle: "औसत", statsCarbonOfNetwork: "नेटवर्क कुल का", statsCarbonUser: "आपका फुटप्रिंट", statsContentCountColumn: "गिनती", @@ -154,6 +176,10 @@ module.exports = { statsTopTagsTitle: "शीर्ष टैग", statsTopTypesTitle: "शीर्ष सामग्री प्रकार", statsTotalMsgs: "कुल संदेश", + feedViewDetails: "विवरण देखें", + eventFileLabel: "छवि संलग्न करें", + taskFileLabel: "छवि संलग्न करें", + courtsRespondentRequired: "प्रतिवादी आईडी आवश्यक है", extended: "मल्टीवर्स", extendedDescription: [ "जब आप किसी का समर्थन करते हैं तो आप उनके द्वारा समर्थित निवासियों की पोस्ट डाउनलोड कर सकते हैं, और वे पोस्ट यहाँ नवीनता के अनुसार क्रमबद्ध दिखाई देती हैं।", @@ -197,27 +223,36 @@ module.exports = { graphosShowing: "दिखा रहे", graphosYou: "आप", graphosTotalNodes: "कुल नोड्स", + manualMode: "मैनुअल मोड", mentions: "उल्लेख", mentionsDescription: [ - strong("हर संदेश जो आपको @उल्लेख करता है"), - ".", + strong("वे पोस्ट जो आपको @उल्लेख करती हैं"), + ", नवीनता के अनुसार क्रमबद्ध।", ], + nextPage: "अगला", + previousPage: "पिछला", noMentions: "आपको अभी तक कोई @उल्लेख नहीं मिला है।", - mentionsSearchPlaceholder: "उल्लेख खोजें...", privateReminders: "रिमाइंडर", inboxFiltersLabel: "फ़िल्टर:", inboxToggleToMutuals: "पारस्परिक पर स्विच करें", inboxToggleToWhole: "सभी पर स्विच करें", + privateFrom: "से", + privateTo: "को", privateDate: "दिनांक", pmReply: "उत्तर दें", pmReplies: "उत्तर", + pmNew: "नया", + pmMarkRead: "पढ़ा हुआ चिह्नित करें", inReplyTo: "के उत्तर में", pmPreview: "पूर्वावलोकन", pmPreviewTitle: "संदेश पूर्वावलोकन", peers: "पीयर", searchDescription: "विवरण", + imageSearch: "छवि खोज", searchFromLabel: "से", searchToLabel: "तक", + searchMinOpinionsLabel: "न्यून. राय", + searchInhabitantLabel: "निवासी (Oasis ID)", searchQueryLabel: "क्वेरी", searchPerPageLabel: "प्रति पृष्ठ परिणाम", settings: "सेटिंग्स", @@ -230,39 +265,68 @@ module.exports = { melodyDescription: 'अपनी ब्लॉकचेन की धुन बजाएँ — हर ब्लॉक एक स्वर बन जाता है।', melodyEmpty: 'अभी कोई स्वर नहीं — आपकी ब्लॉकचेन ने अभी कोई ब्लॉक नहीं बनाया है।', melodyCompositionTitle: 'रचना', + melodyCompositionHelp: 'आपकी ब्लॉकचेन का हर ब्लॉक उसके प्रकार और आकार के अनुसार एक संगीत स्वर बन जाता है।', + melodyStatsTitle: 'प्रकार विश्लेषण', + melodyInhabitantLabel: 'निवासी', melodyTotalBlocks: 'स्वर', + melodyType: 'प्रकार', + melodyCount: 'ब्लॉक', melodyFilterMine: 'मेरी', melodyFilterAll: 'सभी', + melodyFilterShare: 'धुन अपलोड करें', + melodyShareTitle: 'अपनी धुन साझा करें', + melodyShareHelp: 'अपनी मौजूदा धुन का स्नैपशॉट प्रकाशित करें ताकि अन्य सुन और रीमिक्स कर सकें।', + melodyShareTitleLabel: 'शीर्षक (वैकल्पिक)', + melodyShareTitlePlaceholder: 'एक ब्लॉकचेन भूत', + melodyShareSubmit: 'धुन प्रकाशित करें', + melodyNoShared: 'अभी तक कोई ब्लॉकचेन धुन नहीं।', melodyRegenerate: 'पुनः उत्पन्न करें', melodyDownload: "BCS डाउनलोड करें", + profileActivityTitle: 'गतिविधि', + profileActivityMore: 'सारी गतिविधि देखें', profileContentSectionTitle: 'अवतार सामग्री', profileContentHelp: 'चुनें कि आपके अवतार पर कौन से मॉड्यूल दिखाए जाएँगे।', profileSensorsHelp: 'आपके अवतार पर दिखाई जाने वाली वैकल्पिक मेट्रिक्स।', profileClearnetHelp: 'Oasis के बाहर से एक्सेस किए जा सकने वाले मॉड्यूल।', profileHubAll: 'सभी', + profileHubTitle: 'सार्वजनिक सामग्री', + footerPulseTitle: 'पल्स', + footerPulsePeers: 'पीयर', + footerPulseInbox: 'इनबॉक्स', + footerPulseSync: 'अंतिम सिंक', + footerPulseEcoValue: 'ECO/घंटा', + footerPulseLastActivity: 'अंतिम गतिविधि', footerLicenseLabel: 'लाइसेंस', profileSwitchToOasis: 'Oasis पर वापस जाएँ', - profileVisibilityFediverse: "मल्टीवर्स", + profileSwitchToClearnet: 'Clearnet में गोता लगाएँ', + profileVisibilityFediverse: "फ़ेडिवर्स", + profileSwitchToFediverse: "फ़ेडिवर्स में गोता लगाएँ", + coordLabel: 'निर्देशांक (जैसे, A3)', + coordPlaceholder: 'निर्देशांक दर्ज करें', contributorsTitle: "योगदानकर्ता", + pixeliaBy: "द्वारा", colorLabel: 'रंग चुनें', paintButton: 'रंग भरें!', + invalidCoordinate: 'गलत निर्देशांक', + goToMuralButton: "भित्तिचित्र देखें", totalPixels: 'कुल पिक्सेल', modules: "मॉड्यूल", modulesViewTitle: "मॉड्यूल", modulesViewDescription: "मॉड्यूल सक्षम या अक्षम करके अपना वातावरण सेट करें।", inbox: "इनबॉक्स", multiverse: "मल्टीवर्स", - fediverse: "मल्टीवर्स", - fediverseDescription: "अपने अन्य मल्टीवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।", + fediverse: "फ़ेडिवर्स", + fediverseDescription: "अपने अन्य फ़ेडिवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।", fediverseStatus: "स्थिति", - fediverseDisconnected: "मल्टीवर्स डिस्कनेक्ट है। %LINK% या कनेक्शन स्थिति जाँचें।", - fediverseSettingsTitle: "मल्टीवर्स", + fediverseDisconnected: "फ़ेडिवर्स डिस्कनेक्ट है। %LINK% या कनेक्शन स्थिति जाँचें।", + fediverseSettingsTitle: "फ़ेडिवर्स", fediverseInstanceLabel: "पता", fediverseTokenLabel: "एक्सेस टोकन", fediverseTokenHelp: "अपना एक्सेस टोकन सेट करें। इसका उपयोग Oasis के भीतर से आपके Mastodon खाते को प्रबंधित करने के लिए किया जाएगा।", fediverseConnect: "कनेक्ट करें", fediverseConnectedAs: "इस रूप में कनेक्टेड", fediverseDisconnect: "डिस्कनेक्ट करें", + fediverseEmpty: "जब आप अपनी %LINK% से अन्य फ़ेडिवर्स खाते कनेक्ट करेंगे, तो आप उन्हें यहाँ से प्रबंधित कर सकते हैं।", fediverseEmptyLink: "सेटिंग्स", fediverseComposePlaceholder: "आप क्या सोच रहे हैं?", fediverseReplyPlaceholder: "अपना जवाब लिखें…", @@ -271,15 +335,21 @@ module.exports = { fediverseReply: "जवाब दें", fediverseBoosted: "ने बूस्ट किया", fediverseNoPosts: "आपकी टाइमलाइन खाली है।", + fediverseThread: "थ्रेड", fediverseTimeline: "टाइमलाइन्स", + fediverseManageTimeline: "टाइमलाइन प्रबंधित करें", fediverseFollowers: "फ़ॉलोअर्स", fediverseFollowing: "फ़ॉलोइंग", fediversePosts: "पोस्ट", fediverseJoined: "शामिल हुए", + fediverseOverview: "अवलोकन", fediverseRefresh: "रिफ़्रेश", fediverseWrite: "लिखें", fediversePreview: "पूर्वावलोकन", + fediverseEdit: "संपादित करें", + fediverseOpenFeed: "फ़ीड देखें", fediverseManage: "प्रबंधित करें", + fediverseStatusNotConnected: "कनेक्ट नहीं है", fediverseError: "Mastodon से संपर्क नहीं हो सका।", fediverseErrInstance: "अमान्य इंस्टेंस URL।", fediverseErrAuth: "अमान्य या समाप्त टोकन।", @@ -290,6 +360,17 @@ module.exports = { fediverseErrPost: "प्रकाशित नहीं हो सका।", fediverseErrMedia: "फ़ाइल अपलोड नहीं हो सकी।", fediverseErrEmpty: "कुछ लिखें या फ़ाइल जोड़ें।", + popularLabel: "हाइलाइट्स", + topicsLabel: "विषय", + latestLabel: "नवीनतम", + summariesLabel: "सारांश", + threadsLabel: "थ्रेड्स", + multiverseLabel: "मल्टीवर्स", + inboxLabel: "इनबॉक्स", + invitesLabel: "आमंत्रण", + walletLabel: "वॉलेट", + legacyLabel: "कुंजियाँ", + cipherLabel: "क्रिप्टर", bookmarksLabel: "बुकमार्क", videosLabel: "वीडियो", torrentsLabel: "टोरेंट", @@ -300,14 +381,18 @@ module.exports = { inhabitantsLabel: "निवासी", trendingLabel: "ट्रेंडिंग", eventsLabel: "कार्यक्रम", + tasksLabel: "कार्य", apply: "लागू करें", menuPersonal: "व्यक्तिगत", + menuContent: "सामग्री", + menuBlogs: "ब्लॉग", menuGovernance: "शासन", menuOffice: "कार्यालय", - menuNetwork: "समुदाय", + menuMultiverse: "मल्टीवर्स", + menuNetwork: "नेटवर्क", menuCreative: "रचनात्मक", menuEconomy: "अर्थव्यवस्था", - menuMedia: "पुस्तकालय", + menuMedia: "मीडिया", menuTools: "उपकरण", comment: "टिप्पणी", subtopic: "उपविषय", @@ -317,6 +402,13 @@ module.exports = { follow: "समर्थन करें", block: "ब्लॉक करें", unblock: "अनब्लॉक करें", + newerPosts: "नई पोस्ट", + olderPosts: "पुरानी पोस्ट", + feedRangeEmpty: "इस फ़ीड के लिए दी गई सीमा खाली है। देखने का प्रयास करें ", + seeFullFeed: "पूरी फ़ीड", + feedEmpty: "Oasis नेटवर्क ने इस खाते से कभी कोई पोस्ट नहीं देखी है।", + beginningOfFeed: "यह फ़ीड की शुरुआत है", + noNewerPosts: "अभी तक कोई नई पोस्ट प्राप्त नहीं हुई है।", relationshipNotFollowing: "आपका समर्थन नहीं किया गया है", relationshipTheyFollow: "आपका समर्थन करते हैं", relationshipMutuals: "पारस्परिक समर्थन", @@ -326,20 +418,25 @@ module.exports = { relationshipBlockedBy: "आप ब्लॉक हैं", relationshipMutualBlock: "पारस्परिक ब्लॉक", relationshipNone: "आप समर्थन नहीं कर रहे हैं", + relationshipConflict: "विवाद", relationshipBlockingPost: "ब्लॉक की गई पोस्ट", viewLikes: "प्रसार देखें", spreadedDescription: "निवासी द्वारा प्रसारित पोस्ट की सूची।", + totalspreads: "कुल प्रसार", + attachFiles: "फ़ाइलें संलग्न करें", preview: "पूर्वावलोकन", publish: "लिखें", contentWarningPlaceholder: "पोस्ट में विषय जोड़ें (वैकल्पिक)", - publishWarningPlaceholder: "मुख्य पाठ 7000 वर्णों तक सीमित।", - publishTooLong: "आपकी पोस्ट बहुत लंबी है। कृपया इसे छोटा करें।", + privateWarningPlaceholder: "निजी पोस्ट भेजने के लिए निवासी जोड़ें (वैकल्पिक)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "याद रखें: ब्लॉकचेन तकनीक के कारण, एक बार पोस्ट प्रकाशित होने के बाद इसे संपादित या हटाया नहीं जा सकता।", ], commentWarning: [ "याद रखें: ब्लॉकचेन तकनीक के कारण, एक बार पोस्ट प्रकाशित होने के बाद इसे संपादित या हटाया नहीं जा सकता।", ], + commentPublic: "सार्वजनिक", + commentPrivate: "निजी", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -359,6 +456,7 @@ module.exports = { "।", ], publishCustom: "उन्नत पोस्ट लिखें", + subtopicLabel: "इस पोस्ट का उपविषय बनाएँ", messagePreview: "पोस्ट पूर्वावलोकन", mentionsMatching: "मिलते-जुलते उल्लेख", mentionsName: "नाम", @@ -381,93 +479,21 @@ module.exports = { ssbLogStream: "ब्लॉकचेन", ssbLogStreamDescription: "ब्लॉकचेन स्ट्रीम के लिए संदेश सीमा कॉन्फ़िगर करें।", saveSettings: "सेटिंग्स सहेजें", + exportTitle: "डेटा निर्यात करें", exportDescription: "अपनी कुंजी एन्क्रिप्ट करने के लिए पासवर्ड सेट करें (न्यूनतम 32 अक्षर)", exportDataTitle: "बैकअप", exportDataDescription: "अपना डेटा डाउनलोड करें (गुप्त कुंजी शामिल नहीं!)", exportDataButton: "डेटाबेस डाउनलोड करें", + pubWallet: "PUB वॉलेट", + pubWalletDescription: "PUB वॉलेट URL सेट करें। इसका उपयोग PUB लेनदेन (UBI सहित) के लिए किया जाएगा।", + pubWalletConfiguration: "कॉन्फ़िगरेशन सहेजें", + importTitle: "डेटा आयात करें", importDescription: "अपना अवतार सक्रिय करने के लिए अपनी एन्क्रिप्टेड गुप्त कुंजी (निजी कुंजी) आयात करें", + importAttach: "एन्क्रिप्टेड फ़ाइल संलग्न करें (.enc)", + passwordLengthInfo: "पासवर्ड कम से कम 32 अक्षर लंबा होना चाहिए।", passwordImport: "डेटा को डिक्रिप्ट करने के लिए अपना पासवर्ड लिखें जो आपके सिस्टम होम पर सहेजा जाएगा (नाम: secret)", exportPasswordPlaceholder: "लोअरकेस, अपरकेस, संख्याएँ और प्रतीक उपयोग करें", - fileInfo: "आपकी एन्क्रिप्टेड गुप्त कुंजी आपके डिवाइस पर डाउनलोड की जाएगी (नाम: oasis.enc)", - developer: "विकास", - devTitle: "विकास", - welcomeBannerText: "OASIS में स्वागत है। अपना अवतार सेट करने के लिए इन आसान चरणों का पालन करें।", - aiSuggestionBanner: "सुझाव:", - aiSuggestionsEnable: "AI सुझाव", - welcomeBannerAction: "शुरू करें!", - welcomeTitle: "स्वागत है", - welcomeStepGreetingAction: "भेजें", - welcomeJoinPubButton: "pub से जुड़ें", - welcomeStepLarpAction: "\"अकादमी\" में शामिल हों", - welcomeStepLarpText: "हमारे लाइव एक्शन रोल प्लेइंग खेल में शामिल हों।", - welcomeStepLarpTitle: "L.A.R.P. में शामिल हों", - welcomeStepGreetingTitle: "एक \"Hello world!\" भेजें", - welcomeStepGreetingText: "एक संदेश भेजें ताकि दूसरे निवासी आपको खोज सकें।", - welcomeJoinPub: "जुड़ें:", - welcomeDescription: "शुरू करने से पहले कुछ ज़रूरी बातें।", - welcomeProgress: "पूर्ण", - welcomeStepDone: "हो गया", - welcomeStepPending: "बाकी", - welcomeSkip: "अभी नहीं", - welcomeStepLanguageTitle: "अपनी भाषा चुनें", - welcomeStepLanguageText: "Oasis कई भाषाएँ बोलता है। आप इसे कभी भी बदल सकते हैं।", - welcomeStepProfileTitle: "अपनी प्रोफ़ाइल संपादित करें", - welcomeStepProfileText: "एक नाम चुनें, विवरण जोड़ें और तस्वीर लगाएँ ताकि दूसरे निवासी आपको पहचान सकें।", - welcomeStepProfileAction: "प्रोफ़ाइल सहेजें", - welcomeStepFederationTitle: "मुख्य नेटवर्क से जुड़ें", - welcomeStepFederationText: "दूसरे निवासियों से मिलने और सिंक शुरू करने के लिए हमारे pub से जुड़ें।", - welcomeStepFederationAction: "आमंत्रण देखें", - welcomeStepBackupTitle: "अपनी ID का बैकअप लें", - welcomeStepBackupText: "आपकी पहचान इस डिवाइस पर एक कुंजी फ़ाइल है।", - welcomeStepBackupWarning: "यदि यह खो गई, तो कोई भी इसे वापस नहीं ला सकता।", - welcomeStepBackupAction: "बैकअप!", - welcomeCompleteTitle: "सब तैयार है।", - welcomeCompleteText: "आपका नोड तैयार है। यहाँ से नेटवर्क उन लोगों के साथ बढ़ता है जिन्हें आप फ़ॉलो करते हैं।", - welcomeFindPeople: "निवासी खोजें", - devFilterFiles: "फ़ाइलें", - devFilterSearch: "खोज", - devFilterModules: "मॉड्यूल", - devFilterTranslations: "अनुवाद", - devFilterStyles: "शैलियाँ", - devFilterTests: "टेस्ट", - devVersion: "संस्करण", - devNodeVersion: "Node", - devStatsTests: "टेस्ट सूट", - devParentFolder: "मूल फ़ोल्डर", - devOpenFolder: "खोलें", - devDescription: "इस नोड पर चल रहे स्रोत कोड का अन्वेषण करें।", - devRoot: "मूल", - devSearchPlaceholder: "कोड में खोजने के लिए पाठ", - devAnyExtension: "कोई भी फ़ाइल", - devStatsFiles: "फ़ाइलें", - devStatsLines: "पंक्तियाँ", - devStatsModules: "मॉड्यूल", - devStatsLanguages: "भाषाएँ", - devNotViewable: "देखने योग्य नहीं", - devEmptyFolder: "यह फ़ोल्डर खाली है।", - devSize: "आकार", - devShowing: "दिखा रहे हैं", - devLine: "पंक्ति", - devReportBug: "बग रिपोर्ट करें", - devCreateTask: "कार्य बनाएँ", - devRaw: "RAW", - devReportContext: "Oasis स्रोत कोड पढ़ते समय मिला", - devTaskPrefix: "समीक्षा", - devPrevPage: "पिछली पंक्तियाँ", - devNextPage: "अगली पंक्तियाँ", - devMatches: "मिलान", - devFilesScanned: "फ़ाइलें जाँची गईं", - devNoResults: "अभी तक कोई मिलान नहीं।", - devColModule: "मॉड्यूल", - devColState: "स्थिति", - devColModel: "मॉडल", - devColView: "दृश्य", - devColRoutes: "रूट", - devColTests: "टेस्ट", - devOn: "चालू", - devOff: "बंद", - modulesDevLabel: "विकास", - modulesDevDescription: "Oasis स्रोत कोड को प्रबंधित करने का मॉड्यूल।", + fileInfo: "आपकी एन्क्रिप्टेड गुप्त कुंजी आपके सिस्टम होम पर सहेजी जाएगी (नाम: oasis.enc)", themeIntro: "एक थीम चुनें।", setTheme: "थीम सेट करें", @@ -475,13 +501,16 @@ module.exports = { languageDescription: "यदि आप दूसरी भाषा उपयोग करना चाहते हैं, तो यहाँ चुनें।", setLanguage: "भाषा सेट करें", + peerConnections: "पीयर", peerConnectionsIntro: "अन्य पीयर के साथ अपने सभी कनेक्शन प्रबंधित करें।", peerConnectionsTitle: "कनेक्शन", online: "ऑनलाइन", offline: "ऑफ़लाइन", discovered: 'खोजे गए', unknown: 'अज्ञात', + lanBroadcastLabel: "LAN प्रसारण", lanBroadcastActive: "सक्रिय (LAN पर UDP मल्टीकास्ट)", + lanBroadcastDisabled: "अक्षम (pub मोड)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -494,6 +523,8 @@ module.exports = { noConnections: "कोई पीयर कनेक्ट नहीं है।", noDiscovered: "कोई पीयर नहीं मिला।", noUnknownPeers: "मित्र-ग्राफ में कोई अज्ञात पीयर नहीं।", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -503,6 +534,9 @@ module.exports = { peerImport: "आयात", peerImportTitle: "पीयर सूची आयात करें", peerImportPlaceholder: "प्रति पंक्ति एक multiserver पता पेस्ट करें, या एक .txt फ़ाइल अपलोड करें…", + noSupportedConnections: "कोई समर्थित पीयर नहीं।", + noBlockedConnections: "कोई ब्लॉक किया गया पीयर नहीं।", + noRecommendedConnections: "कोई अनुशंसित पीयर नहीं।", connectionActionIntro: "", startNetworking: "नेटवर्किंग शुरू करें", @@ -516,19 +550,9 @@ module.exports = { homePageDescription: "चुनें कि कौन सा मॉड्यूल आपका होम पेज हो।", saveHomePage: "होम सेट करें", invites: "आमंत्रण", + invitesTitle: "आमंत्रण", + invitesInvites: "आमंत्रण", invitesDescription: "अपने नेटवर्क में आमंत्रण कोड प्रबंधित करें और लागू करें।", - invitesPubsHint: "किसी पब का आमंत्रण कोड चिपकाएँ ताकि उससे संघ बने और उसके निवासियों की प्रतिकृति शुरू हो।", - invitesFederationsHint: "जिन पब से आप जुड़े हैं: उन्हें ताज़ा करें, अनुपलब्ध हटाएँ या सूची निर्यात करें।", - invitesHousesHint: "LARP के किसी घर में शामिल होने और उसकी अर्थव्यवस्था में भाग लेने के लिए घर का कोड डालें।", - invitesTribesHint: "सदस्य बनने और निजी सामग्री तक पहुँचने के लिए ट्राइब का कोड डालें।", - invitesChatsHint: "बातचीत और उसके थ्रेड में शामिल होने के लिए चैट का कोड डालें।", - invitesPadsHint: "साझा दस्तावेज़ पर साथ लिखने के लिए पैड का कोड डालें।", - invitesCalendarsHint: "तारीख़ें और अनुस्मारक साझा करने के लिए कैलेंडर का कोड डालें।", - invitesEventsHint: "निजी आयोजन में शामिल होने के लिए इवेंट का कोड डालें।", - invitesForumsHint: "निजी फ़ोरम में पढ़ने और उत्तर देने के लिए फ़ोरम का कोड डालें।", - invitesMapsHint: "साझा नक़्शे पर जगहें देखने और जोड़ने के लिए मानचित्र का कोड डालें।", - invitesShopsHint: "किसी दुकान और उसके कैटलॉग में शामिल होने के लिए दुकान का कोड डालें।", - invitesInhabitantsHint: "एक-दूसरे को फ़ॉलो करने और सामग्री साझा करने के लिए निवासी का कोड डालें।", invitesTribesTitle: "जनजातियाँ", invitesTribeInviteCodePlaceholder: "जनजाति आमंत्रण कोड दर्ज करें", invitesTribeJoinButton: "जनजाति में शामिल हों", @@ -559,6 +583,7 @@ module.exports = { invitesAlreadyFederated: "आप पहले से ही इस पब के साथ फेडरेटेड हैं।", invitesFederationsTitle: "संघ", invitesAcceptedInvites: "संघीय", + invitesNoInvites: "अभी तक कोई आमंत्रण स्वीकार नहीं किया गया।", invitesUnfollow: "अनफ़ॉलो करें", invitesFollow: "फ़ॉलो करें", invitesUnfollowedInvites: "असंघीय", @@ -578,24 +603,39 @@ module.exports = { panicMode: "आपातकालीन मोड!", encryptData: "अपनी ब्लॉकचेन एन्क्रिप्ट करने के लिए पासवर्ड सेट करें (न्यूनतम 32 अक्षर)", decryptData: "अपनी ब्लॉकचेन डिक्रिप्ट करने के लिए पासवर्ड दर्ज करें", + panicModeDescription: "अपनी ब्लॉकचेन एन्क्रिप्ट/डिक्रिप्ट या हटाएँ", removeDataDescription: "चेतावनी: यह प्रक्रिया पूर्ववत नहीं की जा सकती।", + encryptPanicButton: "ब्लॉकचेन एन्क्रिप्ट करें", + decryptPanicButton: "ब्लॉकचेन डिक्रिप्ट करें", removePanicButton: "अपना सारा डेटा हटाएँ!", searchTitle: "खोज", searchDescriptionLabel: "अपने नेटवर्क में सामग्री खोजें।", + searchLanguagesLabel: "भाषाएँ", + searchSkillsLabel: "कौशल", searchPlaceholder:"सामग्री खोजें...", + searchDateLabel:"दिनांक", searchLocationLabel:"स्थान", searchPriceLabel:"मूल्य", + searchUrlLabel:"URL", searchCategoryLabel:"श्रेणी", + searchStartLabel:"प्रारंभ समय", + searchEndLabel:"समाप्ति समय", searchPriorityLabel:"प्राथमिकता", searchStatusLabel:"स्थिति", statusLabel:"स्थिति", + totalVotesLabel:"कुल मत", noResultsFound:"कोई परिणाम नहीं मिला।", votesOption:"मत विकल्प", voteYesLabel:"हाँ", voteNoLabel:"नहीं", allTypesLabel: "असीमित", + ABSTENTIONLabel:"तटस्थता", YESLabel:"हाँ", NOLabel:"नहीं", + FOLLOW_MAJORITYLabel: "बहुमत का पालन करें", + CONFUSEDLabel: "भ्रमित", + NOT_INTERESTEDLabel: "रुचि नहीं", + StatusLabel:"स्थिति", votesOptionYesLabel:"हाँ", votesOptionNoLabel:"नहीं", votesOptionAbstentionLabel:"तटस्थता", @@ -603,6 +643,7 @@ module.exports = { votesCreatedByLabel:"द्वारा बनाया गया", voteStatusOpen:"खुला", voteStatusClosed:"बंद", + imageSearchLabel: "छवियों को खोजने के लिए शब्द दर्ज करें।", commentDescription: ({ parentUrl }) => [ " ने टिप्पणी की ", a({ href: parentUrl }, " थ्रेड पर"), @@ -613,20 +654,53 @@ module.exports = { a({ href: parentUrl }, " एक पोस्ट से"), ], subtopicTitle: ({ authorName }) => [`@${authorName} की पोस्ट पर उपविषय`], + mysteryDescription: "एक रहस्यमय पोस्ट प्रकाशित की", oasisDescription: "OASIS परियोजना नेटवर्क", searchSubmit: "खोजें...", + postLabel: "पोस्ट", + aboutLabel: "निवासी", + feedLabel: "फ़ीड", votesLabel: "मतदान", + reportLabel: "रिपोर्ट", + imageLabel: "छवियाँ", + videoLabel: "वीडियो", + audioLabel: "ऑडियो", + documentLabel: "दस्तावेज़", + torrentLabel: "टॉरेंट", pdfFallbackLabel: "PDF दस्तावेज़", + eventLabel: "कार्यक्रम", + taskLabel: "कार्य", + transferLabel: "स्थानांतरण", + curriculumLabel: "पाठ्यक्रम", + bookmarkLabel: "बुकमार्क", + tribeLabel: "जनजातियाँ", + marketLabel: "बाज़ार", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "वॉलेट", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "जमा करें", + subjectLabel: "विषय", editProfile: "अवतार संपादित करें", + profileQrAlt: "प्रोफ़ाइल QR कोड", + profileQrHint: "इस Oasis ID को कॉपी करने के लिए स्कैन करें।", oasisIdLabel: "OasisID", - spreadContent: "प्रतिकृति बनाएँ", + spreadLabel: "फैलाएँ", + spreadHint: "अपने समर्थकों को फैलाएँ (फ़ीड से प्रतिकृति)।", welcomePmSubject: "Hello World!", - welcomePmBody: "Oasis में आपका स्वागत है — एक स्वतंत्र, पीयर-टू-पीयर, फ़ेडरेटेड और एन्क्रिप्टेड सोशल नेटवर्क, जिसकी वास्तुकला वितरित और केवल-आमंत्रण आधारित है।\n\nशुरुआत के लिए कुछ रोचक स्थान:\n\n- /profile — अपना अवतार, नाम, विवरण और कौन से मॉड्यूल आपके सार्वजनिक पक्ष पर दिखेंगे, संपादित करें।\n- /invites — व्यापक नेटवर्क के साथ फेडरेट करने के लिए एक pub जोड़ें।\n- /peers — देखें कौन जुड़ा है, अपनी LAN फिर से स्कैन करें, पीयर सूचियाँ निर्यात या आयात करें।\n- /inhabitants — दूसरों के अवतार खोजें और देखें।\n- /tribes — एंड-टू-एंड एन्क्रिप्शन वाले निजी समूह बनाएँ या उनमें शामिल हों।\n- /inbox — निजी संदेश पढ़ें और भेजें।\n- /activity — हाल की गतिविधि देखें — आपकी और आपके नेटवर्क की।\n- /publish — पोस्ट लिखें, या चित्र, ऑडियो, वीडियो, दस्तावेज़ साझा करें।\n- /search — नेटवर्क की सामग्री को प्रकार के अनुसार खोजें।\n\nकनेक्ट करने के लिए कुछ दिलचस्प जगहें:\n\n- /multiverse — अपने अन्य मल्टीवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।\n\nयह संदेश आपके द्वारा निजी रूप से स्वयं को भेजा गया था, इसलिए इसे प्राप्त करने के लिए किसी कनेक्शन की आवश्यकता नहीं थी।", + welcomePmBody: "Oasis में आपका स्वागत है — एक स्वतंत्र, पीयर-टू-पीयर, फ़ेडरेटेड और एन्क्रिप्टेड सोशल नेटवर्क, जिसकी वास्तुकला वितरित और केवल-आमंत्रण आधारित है।\n\nशुरुआत के लिए कुछ रोचक स्थान:\n\n- /profile — अपना अवतार, नाम, विवरण और कौन से मॉड्यूल आपके सार्वजनिक पक्ष पर दिखेंगे, संपादित करें।\n- /invites — व्यापक नेटवर्क के साथ फेडरेट करने के लिए एक pub जोड़ें।\n- /peers — देखें कौन जुड़ा है, अपनी LAN फिर से स्कैन करें, पीयर सूचियाँ निर्यात या आयात करें।\n- /inhabitants — दूसरों के अवतार खोजें और देखें।\n- /tribes — एंड-टू-एंड एन्क्रिप्शन वाले निजी समूह बनाएँ या उनमें शामिल हों।\n- /inbox — निजी संदेश पढ़ें और भेजें।\n- /activity — हाल की गतिविधि देखें — आपकी और आपके नेटवर्क की।\n- /publish — पोस्ट लिखें, या चित्र, ऑडियो, वीडियो, दस्तावेज़ साझा करें।\n\nकनेक्ट करने के लिए कुछ दिलचस्प जगहें:\n\n- /fediverse — अपने अन्य फ़ेडिवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।\n\nयह संदेश आपके द्वारा निजी रूप से स्वयं को भेजा गया था, इसलिए इसे प्राप्त करने के लिए किसी कनेक्शन की आवश्यकता नहीं थी।", + profileVisibilityTitle: "सार्वजनिक प्रोफ़ाइल दृश्यता", + profileVisibilityHint: "तय करें कि आपकी प्रोफ़ाइल में अन्य निवासी कौन से क्षेत्र देखें। डिफ़ॉल्ट रूप से केवल Karma दिखाया जाता है।", profileSensorsSectionTitle: "सेंसर", profileVisibilityActivity: "गतिविधि स्तर", + profileVisibilityDevice: "डिवाइस", profileDeviceLockedHint: "यह सेंसर हमेशा चालू रहता है: यह दूसरों को दिखाता है कि आप किस डिवाइस से कनेक्ट होते हैं और इसे अक्षम नहीं किया जा सकता।", profileVisibilityKarma: "KARMA स्कोर", profileVisibilityUbi: "UBI", @@ -634,6 +708,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "GPG कुंजी", + profileVisibilityClearnet: "मेरी प्रोफ़ाइल प्रकाशित करें", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "दुकानें", profileClearnetJobsLabel: "नौकरियाँ", @@ -687,6 +762,7 @@ module.exports = { " या कनेक्शन स्थिति।", ], walletSentToLine: ({ destination, amount }) => `ECO ${amount} भेजा गया ${destination} को`, + walletSettingsTitle: "वॉलेट", walletSettingsDescription: "Oasis को अपने ECOin वॉलेट के साथ एकीकृत करें।", walletSettingsDocLink: "ECOin स्थापना मार्गदर्शिका", walletStatusMessages: { @@ -708,19 +784,37 @@ module.exports = { cipherTitle: "क्रिप्टर", cipherDescription: "अपने टेक्स्ट को सममित रूप से एन्क्रिप्ट और डिक्रिप्ट करें (साझा पासवर्ड का उपयोग करके)।", randomPassword: "यादृच्छिक पासवर्ड", + cipherEncryptTitle: "टेक्स्ट एन्क्रिप्ट करें", + cipherEncryptDescription: "एन्क्रिप्ट करने के लिए टेक्स्ट दर्ज करें", + cipherTextLabel: "एन्क्रिप्ट करने के लिए टेक्स्ट", cipherTextPlaceholder: "एन्क्रिप्ट करने के लिए टेक्स्ट दर्ज करें...", cipherPasswordLabel: "अपना टेक्स्ट एन्क्रिप्ट करने के लिए पासवर्ड सेट करें (न्यूनतम 32 अक्षर)", cipherPasswordDecryptLabel: "अपना टेक्स्ट डिक्रिप्ट करने के लिए पासवर्ड सेट करें (न्यूनतम 32 अक्षर)", cipherPasswordPlaceholder: "पासवर्ड दर्ज करें...", cipherEncryptButton: "एन्क्रिप्ट करें", + cipherDecryptTitle: "टेक्स्ट डिक्रिप्ट करें", + cipherDecryptDescription: "डिक्रिप्ट करने के लिए टेक्स्ट दर्ज करें", cipherEncryptedMessageLabel: "एन्क्रिप्टेड टेक्स्ट", cipherDecryptedMessageLabel: "डिक्रिप्टेड टेक्स्ट", + cipherPasswordUsedLabel: "एन्क्रिप्ट करने के लिए उपयोग किया गया पासवर्ड (इसे रखें!)", cipherEncryptedTextPlaceholder: "एन्क्रिप्टेड टेक्स्ट दर्ज करें...", + cipherIvLabel: "IV", + cipherIvPlaceholder: "इनिशियलाइज़ेशन वेक्टर दर्ज करें...", cipherDecryptButton: "डिक्रिप्ट करें", password: "पासवर्ड", text: "टेक्स्ट", encryptedText: "एन्क्रिप्टेड टेक्स्ट", iv: "इनिशियलाइज़ेशन वेक्टर (IV)", + encryptTitle: "अपना टेक्स्ट एन्क्रिप्ट करें", + encryptDescription: "वह टेक्स्ट दर्ज करें जिसे आप एन्क्रिप्ट करना चाहते हैं और पासवर्ड प्रदान करें।", + encryptButton: "एन्क्रिप्ट करें", + decryptTitle: "अपना टेक्स्ट डिक्रिप्ट करें", + decryptDescription: "एन्क्रिप्टेड टेक्स्ट दर्ज करें और एन्क्रिप्शन के लिए उपयोग किया गया वही पासवर्ड प्रदान करें।", + decryptButton: "डिक्रिप्ट करें", + passwordLengthError: "पासवर्ड कम से कम 32 अक्षर लंबा होना चाहिए।", + missingFieldsError: "टेक्स्ट, पासवर्ड या IV प्रदान नहीं किया गया।", + encryptionError: "टेक्स्ट एन्क्रिप्ट करने में त्रुटि।", + decryptionError: "टेक्स्ट डिक्रिप्ट करने में त्रुटि।", bookmarkTitle: "बुकमार्क", bookmarkDescription: "अपने नेटवर्क में बुकमार्क खोजें और प्रबंधित करें।", bookmarkAllSectionTitle: "बुकमार्क", @@ -746,6 +840,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "वैकल्पिक", bookmarkTagsLabel: "टैग", bookmarkTagsPlaceholder: "अल्पविराम से अलग करके टैग दर्ज करें", + bookmarkCategoryLabel: "श्रेणी", + bookmarkCategoryPlaceholder: "वैकल्पिक", bookmarkLastVisitLabel: "अंतिम विज़िट", bookmarkSearchPlaceholder: "URL, टैग, श्रेणी, लेखक खोजें...", bookmarkSortRecent: "सबसे हाल का", @@ -760,6 +856,8 @@ module.exports = { noLastVisit: "कोई अंतिम विज़िट नहीं", videoTitle: "वीडियो", videoDescription: "अपने नेटवर्क में वीडियो सामग्री खोजें और प्रबंधित करें।", + videoPluginTitle: "शीर्षक", + videoPluginDescription: "विवरण", videoMineSectionTitle: "आपके वीडियो", videoCreateSectionTitle: "वीडियो अपलोड करें", videoUpdateSectionTitle: "वीडियो अपडेट करें", @@ -791,6 +889,7 @@ module.exports = { videoSortOldest: "सबसे पुराना", videoSortTop: "सर्वाधिक मतदान", videoSearchButton: "खोजें", + videoMessageAuthorButton: "PM", videoUpdatedAt: "अपडेट किया गया", videoNoMatch: "आपकी खोज से कोई वीडियो मेल नहीं खाता।", documentTitle: "दस्तावेज़", @@ -831,6 +930,8 @@ module.exports = { documentUpdatedAt: "अपडेट किया गया", audioTitle: "ऑडियो", audioDescription: "अपने नेटवर्क में ऑडियो सामग्री खोजें और प्रबंधित करें।", + audioPluginTitle: "शीर्षक", + audioPluginDescription: "विवरण", audioMineSectionTitle: "आपके ऑडियो", audioCreateSectionTitle: "ऑडियो अपलोड करें", audioUpdateSectionTitle: "ऑडियो अपडेट करें", @@ -841,6 +942,7 @@ module.exports = { audioFilterMine: "मेरे", audioFilterRecent: "हाल के", audioFilterTop: "शीर्ष", + audioFilterBlockchain: "ब्लॉकचेन", audioCreateButton: "ऑडियो अपलोड करें", audioUpdateButton: "अपडेट", audioDeleteButton: "हटाएँ", @@ -867,7 +969,6 @@ module.exports = { audioFilterFavorites: "पसंदीदा", favoritesTitle: "पसंदीदा", favoritesDescription: "आपकी सभी पसंदीदा सामग्री एक ही स्थान पर।", - favoritesSearchPlaceholder: "पसंदीदा खोजें...", favoritesFilterAll: "सभी", favoritesFilterRecent: "हाल के", favoritesFilterAudios: "ऑडियो", @@ -883,13 +984,18 @@ module.exports = { favoritesNoItems: "अभी तक कोई पसंदीदा नहीं।", yourContacts: "आपके संपर्क", allInhabitants: "निवासी", - allCVs: "CVs", + allCVs: "सभी CVs", discoverPeople: "अपने नेटवर्क में निवासियों को खोजें।", + allInhabitantsButton: "सभी", + contactsButton: "समर्थन", + CVsButton: "बायोडाटा", + matchSkills: "कौशल मिलान", + matchSkillsButton: "कौशल मिलान", + suggestedButton: "सुझाए गए", searchInhabitantsPlaceholder: "नाम से निवासी फ़िल्टर करें …", filterLocation: "स्थान से निवासी फ़िल्टर करें …", filterLanguage: "भाषा से निवासी फ़िल्टर करें …", filterSkills: "कौशल से निवासी फ़िल्टर करें …", - cvSkillsCount: "कौशल", applyFilters: "फ़िल्टर लागू करें", locationLabel: "स्थान", languagesLabel: "भाषाएँ", @@ -898,18 +1004,17 @@ module.exports = { mutualFollowers: "पारस्परिक अनुयायी", suggestedFollowsYou: "आपको फ़ॉलो करता है", latestInteractions: "नवीनतम बातचीत", + viewAvatar: "अवतार देखें", + viewCV: "CV देखें", suggestedSectionTitle: "सुझाए गए", topkarmaSectionTitle: "शीर्ष कर्मा", topecoSectionTitle: "शीर्ष इको", topactivitySectionTitle: "शीर्ष गतिविधि", - "TOP INACTIVITYButton": "सबसे निष्क्रिय", - topinactivitySectionTitle: "सबसे कम सक्रिय निवासी", blockedSectionTitle: "ब्लॉक किए गए", gallerySectionTitle: "गैलरी", - topSectionTitle: "शीर्ष", + blockedButton: "ब्लॉक किए गए", blockedLabel: "ब्लॉक किया गया उपयोगकर्ता", inhabitantviewDetails: "विवरण देखें", - cvVisitButton: "CV देखें", keepReading: "पढ़ते रहें...", oasisId: "ID", noInhabitantsFound: "अभी तक कोई निवासी नहीं मिला।", @@ -919,11 +1024,10 @@ module.exports = { parliamentTitle: "संसद", parliamentDescription: "शासन के रूपों और सामूहिक प्रबंधन कानूनों का अन्वेषण करें।", parliamentFilterGovernment: "सरकार", - parliamentFilterCandidatures: "उम्मीदवारियाँ", + parliamentFilterCandidatures: "उम्मीदवारी", parliamentFilterProposals: "प्रस्ताव", parliamentFilterLaws: "कानून", - parliamentFilterRevocations: "निरसन", - parliamentFilterHistorical: "इतिहास", + parliamentFilterHistorical: "ऐतिहासिक", parliamentFilterLeaders: "नेता", parliamentFilterRules: "नियम", parliamentGovernmentCard: "वर्तमान सरकार", @@ -948,8 +1052,10 @@ module.exports = { parliamentPoliciesDeclined: "अस्वीकृत कानून", parliamentPoliciesDiscarded: "निरस्त कानून", parliamentEfficiency: "% दक्षता", + parliamentFilterRevocations: 'निरसन', parliamentRevocationFormTitle: 'कानून निरसन', parliamentRevocationLaw: 'कानून', + parliamentRevocationTitle: 'शीर्षक', parliamentRevocationReasons: 'कारण', parliamentRevocationPublish: 'निरसन प्रकाशित करें', parliamentCurrentRevocationsTitle: 'वर्तमान निरसन', @@ -962,12 +1068,23 @@ module.exports = { parliamentNoGovernments: "अभी तक कोई सरकार नहीं है।", parliamentCandidatureFormTitle: "उम्मीदवारी प्रस्तावित करें", parliamentCandidatureIdPh: "Oasis ID (@...) या जनजाति का नाम", + parliamentCandidatureSlogan: "नारा (अधिकतम 140 अक्षर)", + parliamentCandidatureSloganPh: "एक छोटा आदर्श वाक्य", parliamentCandidatureMethod: "पद्धति", parliamentCandidatureProposeBtn: "उम्मीदवारी प्रकाशित करें", + parliamentThDate: "प्रस्ताव तिथि", + parliamentThSlogan: "नारा", + parliamentThSince: "प्रोफ़ाइल से", + parliamentThVotes: "प्राप्त मत", + parliamentThVoteAction: "मत दें", + parliamentTypeUser: "निवासी", + parliamentTypeTribe: "जनजाति", parliamentVoteBtn: "मत दें", parliamentProposalFormTitle: "कानून प्रस्तावित करें", parliamentProposalDescription: "विवरण (≤1000)", parliamentProposalPublish: "प्रस्ताव प्रकाशित करें", + parliamentFinalize: "अंतिम रूप दें", + parliamentDeadline: "समय सीमा", parliamentNoProposals: "अभी तक कोई कानून प्रस्ताव नहीं है।", parliamentNoLaws: "अभी तक कोई स्वीकृत कानून नहीं है।", parliamentMethodDEMOCRACY: "लोकतंत्र", @@ -985,21 +1102,29 @@ module.exports = { parliamentVotesSlashTotal: "मत/कुल", parliamentVotesNeeded: "आवश्यक मत", parliamentFutureLawsTitle: "भविष्य के कानून", + parliamentNoFutureLaws: "अभी तक कोई भविष्य के कानून नहीं।", parliamentVoteAction: "मत दें", + parliamentLeadersTitle: "नेता", parliamentThLeader: "अवतार", parliamentPopulation: "जनसंख्या", + parliamentThType: "प्रकार", + parliamentThInPower: "सत्ता में", parliamentThPresented: "उम्मीदवारी", parliamentNoLeaders: "अभी तक कोई नेता नहीं।", parliamentCandidaturesListTitle: "उम्मीदवारी सूची", parliamentTimeRemaining: "शेष समय", + parliamentNoLeader: "अभी तक कोई विजयी उम्मीदवारी नहीं।", parliamentElectionsStatusTitle: "अगली सरकार", parliamentProposalDeadlineLabel: "समय सीमा", parliamentProposalTimeLeft: "शेष समय", + parliamentProposalOnTrack: "पारित होने की राह पर", + parliamentProposalOffTrack: "अभी तक पर्याप्त समर्थन नहीं", parliamentRulesTitle: "संसद कैसे काम करती है", parliamentRulesIntro: "चुनाव हर 2 महीने में हल होते हैं; उम्मीदवारी निरंतर होती है और सरकार चुने जाने पर रीसेट हो जाती है।", parliamentRulesCandidates: "कोई भी निवासी अपने आप को, किसी अन्य निवासी को, या किसी जनजाति को प्रस्तावित कर सकता है। प्रत्येक निवासी प्रति चक्र अधिकतम 3 उम्मीदवारी प्रस्तावित कर सकता है; एक ही चक्र में दोहराव अस्वीकार किए जाते हैं।", parliamentRulesElection: "विजेता वह उम्मीदवारी है जिसे समाधान के समय सबसे अधिक मत प्राप्त हों।", parliamentRulesTies: "टाई-ब्रेक क्रम: सर्वोच्च निवासी कर्म; यदि बराबर, सबसे पुरानी प्रोफ़ाइल; यदि अभी भी बराबर, सबसे पहला प्रस्ताव; फिर शब्दकोषीय ID द्वारा।", + parliamentRulesFallback: "यदि कोई मत नहीं देता, तो नवीनतम प्रस्तावित उम्मीदवारी जीतती है। यदि कोई उम्मीदवारी नहीं है, तो एक यादृच्छिक जनजाति चुनी जाती है।", parliamentRulesTerm: "सरकार टैब वर्तमान सरकार और आँकड़े दिखाता है।", parliamentRulesMethods: "रूप: अराजकता (साधारण बहुमत), लोकतंत्र (50%+1), बहुमत (80%), अल्पमत (20%), कर्मतंत्र (सर्वोच्च-कर्म प्रस्ताव), तानाशाही (तत्काल अनुमोदन)।", parliamentRulesAnarchy: "अराजकता डिफ़ॉल्ट मोड है: यदि समाधान पर कोई उम्मीदवारी नहीं चुनी जाती, तो अराजकता घोषित होती है। अराजकता के तहत, कोई भी निवासी कानून प्रस्तावित कर सकता है।", @@ -1021,7 +1146,11 @@ module.exports = { courtsCaseFormTitle: "मामला खोलें", courtsCaseRespondent: "आरोपी / प्रतिवादी", courtsCaseRespondentPh: "Oasis ID (@...) या जनजाति का नाम", + courtsCaseMediatorsAccuser: "मध्यस्थ (वादी)", courtsCaseMethod: "समाधान पद्धति", + courtsCaseDescription: "विवरण (अधिकतम 1000 अक्षर)", + courtsCaseEvidenceTitle: "मामले के साक्ष्य", + courtsCaseEvidenceHelp: "अपने मामले का समर्थन करने वाली छवियाँ, ऑडियो, दस्तावेज़ (PDF) या वीडियो संलग्न करें।", courtsCaseSubmit: "मामला दायर करें", courtsNominateJudge: "न्यायाधीश नामांकित करें", courtsJudgeId: "न्यायाधीश", @@ -1066,6 +1195,22 @@ module.exports = { courtsRulesMisconduct: "उत्पीड़न, हेरफेर, या मनगढ़ंत साक्ष्य तत्काल नकारात्मक समाधान का कारण बन सकते हैं।", courtsRulesGlossary: "मामला: विवाद का रिकॉर्ड। साक्ष्य: दावों का समर्थन करने वाली सामग्री। फैसला: उपाय के साथ निर्णय। अपील: फैसले की समीक्षा का अनुरोध।", courtsFilterActions: "कार्रवाई", + courtsNoActions: "आपकी भूमिका के लिए कोई लंबित कार्रवाई नहीं।", + courtsCaseTitlePlaceholder: "विवाद का संक्षिप्त विवरण", + courtsCaseSeverity: "गंभीरता", + courtsCaseSeverityNone: "कोई गंभीरता टैग नहीं", + courtsCaseSeverityLOW: "कम", + courtsCaseSeverityMEDIUM: "मध्यम", + courtsCaseSeverityHIGH: "उच्च", + courtsCaseSeverityCRITICAL: "गंभीर", + courtsCaseSubject: "विषय", + courtsCaseSubjectNone: "कोई विषय टैग नहीं", + courtsCaseSubjectBEHAVIOUR: "व्यवहार", + courtsCaseSubjectCONTENT: "सामग्री", + courtsCaseSubjectGOVERNANCE: "शासन / नियम", + courtsCaseSubjectFINANCIAL: "वित्तीय / संसाधन", + courtsCaseSubjectOTHER: "अन्य", + courtsHiddenRespondent: "छुपा हुआ (केवल संबंधित भूमिकाओं को दिखाई देता है)।", courtsThRole: "भूमिका", courtsRoleAccuser: "वादी", courtsRoleDefence: "बचाव", @@ -1076,19 +1221,51 @@ module.exports = { courtsAssignJudgeBtn: "न्यायाधीश चुनें", trendingTitle: "ट्रेंडिंग", exploreTrending: "अपने नेटवर्क में सबसे लोकप्रिय सामग्री खोजें।", + bookmarkButton: "बुकमार्क", + transferButton: "स्थानांतरण", + eventButton: "कार्यक्रम", + taskButton: "कार्य", votesButton: "मतदान", - shopProductButton: "दुकान", + reportButton: "रिपोर्ट", + feedButton: "फ़ीड", + marketButton: "बाज़ार", + imageButton: "छवियाँ", + audioButton: "ऑडियो", + videoButton: "वीडियो", + documentButton: "दस्तावेज़", + torrentButton: "टॉरेंट", + noTrendingFound: "कोई ट्रेंडिंग सामग्री नहीं मिली।", + noContentMessage: "अभी तक कोई ट्रेंडिंग सामग्री उपलब्ध नहीं।", + trendingDescription: "विवरण", + trendingDate: "दिनांक", + trendingLocation: "स्थान", + trendingPrice: "मूल्य", + trendingUrl: "URL", + trendingCategory: "श्रेणी", + trendingStart: "प्रारंभ", + trendingEnd: "समाप्ति", + trendingPriority: "प्राथमिकता", + trendingStatus: "स्थिति", + trendingFrom: "से", + trendingTo: "को", + trendingConcept: "अवधारणा", + trendingAmount: "राशि", + trendingDeadline: "समय सीमा", + trendingItemStatus: "आइटम स्थिति", + trendingTotalVotes: "कुल मत", trendingTotalOpinions: "कुल राय", - trendingNoContentMessage: "अभी तक कोई ट्रेंडिंग नहीं है।", + trendingNoContentMessage: "अभी तक कोई ट्रेंडिंग सामग्री उपलब्ध नहीं।", + trendingAuthor: "द्वारा", + trendingCreatedAtLabel: "बनाया गया", trendingTotalCount: "कुल संख्या", tasksTitle: "कार्य", tasksDescription: "अपने नेटवर्क में कार्य खोजें और प्रबंधित करें।", - taskSearchPlaceholder: "कार्य खोजें...", taskTitleLabel: "शीर्षक", taskDescriptionLabel: "विवरण", taskStartTimeLabel: "प्रारंभ समय", taskEndTimeLabel: "समाप्ति समय", taskPriorityLabel: "प्राथमिकता", + taskPrioritySelect: "प्राथमिकता चुनें", taskPriorityUrgent: "अत्यावश्यक", taskPriorityHigh: "उच्च", taskPriorityMedium: "मध्यम", @@ -1098,13 +1275,13 @@ module.exports = { taskVisibilityLabel: "दृश्यता", taskPublic: "सार्वजनिक", taskPrivate: "निजी", + taskCreatedAt: "बनाया गया", + taskBy: "द्वारा", taskStatus: "स्थिति", taskStatusOpen: "खुला", taskStatusInProgress: "प्रगति में", taskStatusClosed: "बंद", taskAssignedTo: "को सौंपा गया", - taskAssignedChip: "सौंपा गया", - taskUnassignedChip: "असौंपा", taskAssignees: "सौंपे गए", taskAssignButton: "मुझे सौंपें", taskUnassignButton: "सौंपना रद्द करें", @@ -1117,6 +1294,7 @@ module.exports = { taskFilterInProgress: "प्रगति में", taskFilterClosed: "बंद", taskFilterAssigned: "सौंपे गए", + taskFilterArchived: "संग्रहीत", taskFilterUrgent: "अत्यावश्यक", taskFilterHigh: "उच्च", taskFilterMedium: "मध्यम", @@ -1129,21 +1307,23 @@ module.exports = { taskInProgressTitle: "प्रगति में कार्य", taskClosedTitle: "बंद कार्य", taskAssignedTitle: "सौंपे गए कार्य", + taskArchivedTitle: "संग्रहीत कार्य", + taskPublicTitle: "सार्वजनिक कार्य", + taskPrivateTitle: "निजी कार्य", notasks: "कोई कार्य उपलब्ध नहीं।", noLocation: "कोई स्थान निर्दिष्ट नहीं", taskSetStatus: "स्थिति सेट करें", + eventTitle: "कार्यक्रम", eventDateLabel: "दिनांक", - eventRecurrenceLabel: "पुनरावृत्ति", - eventRecurrenceUntil: "इस तिथि तक दोहराएँ", - eventRecurrenceHint: "एकल इवेंट के लिए अंतिम तिथि खाली छोड़ें।", - eventUpcomingDates: "आगामी तिथियाँ", eventsTitle: "कार्यक्रम", eventsDescription: "अपने नेटवर्क में कार्यक्रम खोजें और प्रबंधित करें।", - eventSearchPlaceholder: "कार्यक्रम खोजें...", + eventDescription: "विवरण", eventPrice: "मूल्य", eventStatus: "स्थिति", + eventOrganizer: "आयोजक", eventAllSectionTitle: "कार्यक्रम", eventMineSectionTitle: "आपके कार्यक्रम", + eventArchivedTitle: "संग्रहीत कार्यक्रम", eventCreateSectionTitle: "कार्यक्रम बनाएँ", eventUpdateSectionTitle: "कार्यक्रम अपडेट करें", eventDeleteButton: "हटाएँ", @@ -1151,26 +1331,11 @@ module.exports = { eventAddToCalendar: "कैलेंडर में जोड़ें", eventVisitCalendar: "कैलेंडर देखें", viewTask: "कार्य देखें", - viewEvent: "इवेंट देखें", - viewPad: "पैड देखें", - viewMap: "मानचित्र देखें", - viewCalendar: "कैलेंडर देखें", viewReport: "रिपोर्ट देखें", viewTransfer: "स्थानांतरण देखें", viewItem: "आइटम देखें", viewShop: "दुकान देखें", viewProject: "परियोजना देखें", - viewJob: "नौकरी देखें", - viewPlace: "स्थान देखें", - generatePdf: "PDF बनाएँ", - sharePm: "PM से साझा करें", - galleryImages: "छवियाँ (अधिकतम आकार: 50MB प्रत्येक)", - galleryPhotos: "छवियाँ", - galleryAddPhoto: "छवि जोड़ें", - galleryRemovePhoto: "हटाएँ", - galleryVideo: "वीडियो (अधिकतम आकार: 50MB)", - galleryAddVideo: "वीडियो जोड़ें", - galleryRemoveVideo: "वीडियो हटाएँ", viewVotation: "मतदान देखें", voteCastTitle: "मतदान करें", voteResults: "परिणाम", @@ -1178,15 +1343,29 @@ module.exports = { eventDescriptionLabel: "विवरण", eventDescriptionPlaceholder: "कार्यक्रम विवरण दर्ज करें...", eventUpdateButton: "अपडेट", + eventAttendeesLabel: "उपस्थित", + eventAttendeesPlaceholder: "अल्पविराम से अलग करके उपस्थित दर्ज करें...", eventTagsLabel: "टैग", + eventTagsPlaceholder: "अल्पविराम से अलग करके कार्यक्रम टैग दर्ज करें...", + eventTags: "टैग", eventPriceLabel: "मूल्य", eventUrlLabel: "URL", eventAttendees: "उपस्थित", + noAttendees: "अभी तक कोई उपस्थित नहीं", + eventCreatedAt: "बनाया गया", eventLocation: "स्थान", eventLocationLabel: "स्थान", + eventNoLocation: "कोई स्थान निर्दिष्ट नहीं", + eventNoURL: "कोई URL निर्दिष्ट नहीं", + eventBy: "द्वारा", noevents: "कोई कार्यक्रम उपलब्ध नहीं।", eventDate: "दिनांक", + eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "कार्यक्रम में शामिल हों", + eventUnattendButton: "कार्यक्रम छोड़ें", + eventCreatedBy: "द्वारा बनाया गया", + eventAttendeesCount: "उपस्थित संख्या", + eventCreatedByYou: "आपने यह कार्यक्रम बनाया", eventFilterAll: "सभी", eventFilterMine: "मेरे", eventFilterToday: "आज", @@ -1194,9 +1373,18 @@ module.exports = { eventFilterMonth: "इस महीने", eventFilterYear: "इस वर्ष", eventFilterArchived: "संग्रहीत", + eventTodayTitle: "आज के कार्यक्रम", + eventThisWeekTitle: "इस सप्ताह के कार्यक्रम", + eventThisMonthTitle: "इस महीने के कार्यक्रम", + eventThisYearTitle: "इस वर्ष के कार्यक्रम", eventPrivacyLabel: "दृश्यता", eventPublic: "सार्वजनिक", eventPrivate: "निजी", + eventPublicTitle: "सार्वजनिक कार्यक्रम", + eventNoPrice: "निःशुल्क कार्यक्रम", + eventNoImage: "कोई छवि अपलोड नहीं की गई", + eventAttendConfirmation: "अब आप इस कार्यक्रम में शामिल हैं", + eventUnattendConfirmation: "अब आप इस कार्यक्रम में शामिल नहीं हैं", eventAttended: "शामिल हुए", eventUnattended: "शामिल नहीं", eventStatusOpen: "खुला", @@ -1207,14 +1395,9 @@ module.exports = { tagsTopSectionTitle: "शीर्ष टैग", tagsCloudSectionTitle: "टैग क्लाउड", tagsFilterAll: "सभी", - tagsFilterMine: "मेरे", - tagsFilterRecent: "हाल के", - tagsMineSectionTitle: "मेरे टैग", - tagsRecentSectionTitle: "हाल के टैग", tagsFilterTop: "शीर्ष", tagsFilterCloud: "क्लाउड", tagsNoItems: "कोई टैग उपलब्ध नहीं।", - tagsSearchPlaceholder: "टैग खोजें...", tagsTableHeaderTag: "टैग/लिंक:", tagsTableHeaderCount: "गणना:", transfersTitle: "स्थानांतरण", @@ -1254,12 +1437,18 @@ module.exports = { transfersDeadline: "समय सीमा", transfersTags: "टैग", transfersStatus: "स्थिति", + transfersStatusUnconfirmed: "अपुष्ट", + transfersStatusClosed: "बंद", + transfersStatusDiscarded: "निरस्त", + transfersCreatedAt: "बनाया गया", transfersConfirmations: "पुष्टि", - transfersExportContract: "बिल बनाएँ", + transfersContainingBlock: "धारक ब्लॉक", + transfersExportContract: "अनुबंध बनाएँ", transfersConfirmButton: "स्थानांतरण पुष्टि करें", transfersNoItems: "कोई स्थानांतरण नहीं मिला।", transfersMineSectionTitle: "आपके स्थानांतरण", transfersUBISectionTitle: "UBI स्थानांतरण", + transfersMarketSectionTitle: "बाज़ार स्थानांतरण", transfersTopSectionTitle: "शीर्ष स्थानांतरण", transfersPendingSectionTitle: "लंबित स्थानांतरण", transfersUnconfirmedSectionTitle: "अपुष्ट स्थानांतरण", @@ -1267,13 +1456,21 @@ module.exports = { transfersDiscardedSectionTitle: "निरस्त स्थानांतरण", transfersCreateSectionTitle: "स्थानांतरण बनाएँ", transfersAllSectionTitle: "स्थानांतरण", + transfersFilterFavs: "पसंदीदा", + transfersFavsSectionTitle: "पसंदीदा स्थानांतरण", + transfersSearchLabel: "खोज", transfersSearchPlaceholder: "अवधारणा, टैग, उपयोगकर्ता खोजें...", transfersMinAmountLabel: "न्यूनतम राशि", transfersMaxAmountLabel: "अधिकतम राशि", + transfersSortLabel: "क्रमबद्ध करें", transfersSortRecent: "सबसे हाल का", transfersSortAmount: "सबसे अधिक राशि", transfersSortDeadline: "निकटतम समय सीमा", transfersSearchButton: "खोजें", + transfersFavoriteButton: "पसंदीदा", + transfersUnfavoriteButton: "पसंदीदा हटाएँ", + transfersMessageUserButton: "संदेश", + transfersExpiringSoonBadge: "समाप्त हो रहा है", transfersExpiredBadge: "समाप्त", transfersUpdatedAt: "अपडेट किया गया", transfersNoMatch: "आपकी खोज से कोई स्थानांतरण मेल नहीं खाता।", @@ -1286,9 +1483,6 @@ module.exports = { voteClosedTitle: "बंद मतदान", voteAllSectionTitle: "मतदान", voteCreateButton: "मतदान बनाएँ", - voteErrorTitle: "फ़ॉर्म जाँचें", - voteErrorDeadlineMin: "समय-सीमा अभी से कम से कम {days} दिन आगे होनी चाहिए।", - voteErrorGeneric: "वोट सहेजा नहीं जा सका।", voteUpdateButton: "अपडेट", voteDeleteButton: "हटाएँ", voteOptionYes: "हाँ", @@ -1318,6 +1512,16 @@ module.exports = { voteNoQuestion: "कोई प्रश्न प्रदान नहीं किया गया", voteUnknownCreator: "अज्ञात निर्माता", voteUnknownDate: "अज्ञात दिनांक", + errorVoteNotFound: "मत नहीं मिला", + errorAlreadyVoted: "आपने पहले ही राय दे दी है।", + errorVoteClosedCannotEdit: "आप बंद मतदान को संपादित नहीं कर सकते", + errorVoteDeadlinePassed: "इस मतदान की समय सीमा बीत चुकी है", + errorRetrievingVote: "मत प्राप्त करने में त्रुटि", + errorCreatingVote: "मत बनाने में त्रुटि", + errorVoteAlreadyVoted: "राय दिए जाने के बाद संपादित नहीं किया जा सकता", + errorDeletingOldVote: "पुरानी राय हटाने में त्रुटि", + errorCreatingUpdatedVote: "अपडेटेड राय बनाने में त्रुटि", + errorCreatingTombstone: "टॉम्बस्टोन बनाने में त्रुटि", voteDetailSectionTitle: 'मत विवरण', voteCommentsLabel: 'टिप्पणियाँ', voteCommentsForumButton: 'चर्चा खोलें', @@ -1327,6 +1531,7 @@ module.exports = { voteNewCommentButton: 'टिप्पणी पोस्ट करें', voteNewCommentLabel: 'टिप्पणी जोड़ें', cvTitle: "CV", + cvLabel: "बायोडाटा (CV)", cvEditSectionTitle: "CV संपादित करें", cvCreateSectionTitle: "CV बनाएँ", cvDescription: "अपने पेशेवर कौशल और जानकारी प्रबंधित करें और साझा करें।", @@ -1345,121 +1550,29 @@ module.exports = { cvLocationLabel: "स्थान", cvStatusLabel: "स्थिति", cvPreferencesLabel: "प्राथमिकताएँ", + cvOasisContributorLabel: "Oasis योगदानकर्ता", cvPersonal: "व्यक्तिगत", cvOasis: "Oasis योगदानकर्ता (वैकल्पिक)", - cvOasisContributorView: "योगदान", + cvOasisContributorView: "Oasis योगदान", cvEducational: "शैक्षिक (वैकल्पिक)", cvEducationalView: "शैक्षिक", cvProfessional: "पेशेवर (वैकल्पिक)", cvProfessionalView: "पेशेवर", cvAvailability: "उपलब्धता (वैकल्पिक)", + cvAvailabilityView: "उपलब्धता", cvUpdateButton: "अपडेट", cvCreateButton: "CV बनाएँ", + cvContactLabel: "संपर्क", cvCreatedAt: "बनाया गया", cvUpdatedAt: "अपडेट किया गया", cvEditButton: "अपडेट", cvDeleteButton: "हटाएँ", cvNoCV: "कोई CV नहीं मिला।", blogSubject: "विषय", - blogMessage: "संदेश", + blogMessage: "मुख्य भाग 8096 वर्णों तक सीमित।", blogImage: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", - blogMedia: "अनुलग्नक (8 चित्र और एक वीडियो तक)", blogPublish: "पूर्वावलोकन", - pollsTitle: "सर्वेक्षण", - dataTitle: "मिलान", - dataDescription: "अपने नेटवर्क के मेल खोजें और देखें।", - dataFilterAll: "सभी", - dataFilterMine: "मेरे", - dataFilterRecent: "हाल के", - dataFilterTop: "शीर्ष", - dataKindInhabitants: "निवासी", - dataKindJobs: "नौकरियाँ", - dataKindProjects: "परियोजनाएँ", - dataKindEvents: "इवेंट्स", - dataKindTribes: "जनजातियाँ", - dataKindMarket: "मार्केट", - dataKindHousing: "आवास", - dataKindIndustry: "उद्योग", - dataKindTasks: "कार्य", - dataKindReports: "रिपोर्ट्स", - dataKindVotes: "मतदान", - dataSearchPlaceholder: "क्रॉस-डेटा में खोजें...", - dataCohesionTitle: "संसक्ति गुणांक (CC)", - dataCcShort: "CC", - dataCohesionHint: "नेटवर्क ने जो कुछ प्रकाशित किया है, उसके बीच औसत समानता।", - dataAffinity: "समानता", - dataCommonTerms: "कीवर्ड", - dataStatPeople: "CVs", - dataStatConnected: "जुड़े हुए", - dataStatSkills: "कौशल", - dataBestMatch: "सर्वश्रेष्ठ मैच", - dataStatIsolated: "अलग-थलग", - dataStatEntities: "तुलना की गई इकाइयाँ", - dataStatTerms: "विशिष्ट शब्द", - dataStatPairs: "जुड़े हुए जोड़े", - dataMatchesTitle: "मेल", - dataMatchesHint: "एल्गोरिद्म के व्यक्तिगत सुझाव।", - dataTermsTitle: "मुख्य शब्द", - dataTermsHint: "वे शब्द जो सबसे अधिक सामग्री में आते हैं।", - dataConnections: "जुड़े मिलान", - dataTopicTitle: "इससे संबंधित सब कुछ:", - dataTopicHint: "एल्गोरिद्म इस विषय से क्या जोड़ता है", - dataNoMatches: "दिखाने के लिए क्रॉस-डेटा नहीं।", - dataNoProfile: "सामग्री प्रकाशित करें या अपना CV लिखें ताकि एल्गोरिद्म जान सके कि आपकी रुचि किसमें है।", - pollsDescription: "नेटवर्क से पूछने और उत्तर गिनने का मॉड्यूल।", - pollFilterAll: "सभी", - pollFilterMine: "मेरे", - pollFilterRecent: "हाल के", - pollFilterTop: "शीर्ष", - pollFilterVoted: "मतदान किया", - pollFilterOpen: "खुले", - pollFilterClosed: "बंद", - pollCreateButton: "सर्वेक्षण बनाएँ", - pollCreateTitle: "सर्वेक्षण बनाएँ", - pollEditTitle: "सर्वेक्षण संपादित करें", - pollQuestion: "प्रश्न", - pollQuestionPlaceholder: "आप क्या पूछना चाहते हैं?", - pollOptions: "विकल्प (प्रति पंक्ति एक)", - pollOutcome: "परिणाम", - pollNoVotesYet: "अभी कोई वोट नहीं", - pollTie: "बराबरी", - pollOptionsPlaceholder: "चौक\nपार्क\nबार", - pollAnonymous: "गुमनाम", - pollAnonymousLabel: "गुमनाम सर्वेक्षण", - pollMultiple: "बहुविकल्प", - pollMultipleLabel: "कई उत्तरों की अनुमति दें", - pollDeadline: "समय सीमा", - pollTags: "टैग", - pollPublishButton: "सर्वेक्षण प्रकाशित करें", - pollUpdateButton: "अपडेट करें", - pollCloseButton: "बंद करें", - pollDeleteButton: "हटाएं", - pollVoteButton: "मत दें", - pollChangeVote: "वोट बदलें", - pollVoters: "मतदाता", - pollComments: "टिप्पणियाँ", - pollStatusLabel: "स्थिति", - pollStatusOpen: "खुला", - pollStatusClosed: "बंद", - pollSearchPlaceholder: "सर्वेक्षण खोजें...", - pollsNoItems: "कोई सर्वेक्षण नहीं मिला।", - viewPoll: "सर्वेक्षण देखें", - pollChatCreate: "सर्वेक्षण बनाएँ", - pollInChat: "सर्वेक्षण", - blogTitle: "ब्लॉग", - blogDescription: "ब्लॉग खोजने और प्रबंधित करने का मॉड्यूल।", - blogFilterAll: "सभी", - blogFilterMine: "मेरे", - blogFilterRecent: "हाल के", - blogFilterFavorites: "पसंदीदा", - blogFilterTop: "शीर्ष", - blogCreateButton: "ब्लॉग बनाएँ", - blogSearchPlaceholder: "ब्लॉग खोजें...", - blogComments: "टिप्पणियाँ", - blogAllowComments: "टिप्पणियाँ अनुमति दें", - blogCommentsClosed: "लेखक ने इस ब्लॉग की टिप्पणियाँ बंद कर दी हैं।", - blogNoItems: "कोई ब्लॉग नहीं मिला।", - viewBlog: "ब्लॉग देखें", + noPopularMessages: "अभी तक कोई लोकप्रिय संदेश प्रकाशित नहीं हुआ", forumTitle: "मंच", forumCategoryLabel: "श्रेणी", forumTitleLabel: "शीर्षक", @@ -1467,22 +1580,43 @@ module.exports = { forumCreateButton: "मंच बनाएँ", forumCreateSectionTitle: "मंच बनाएँ", forumDescription: "अपने नेटवर्क के अन्य निवासियों से खुलकर बात करें।", - forumSearchPlaceholder: "फ़ोरम खोजें...", forumFilterAll: "सभी", forumFilterMine: "मेरे", forumFilterRecent: "हाल के", forumFilterTop: "शीर्ष", + forumMineSectionTitle: "आपके मंच", + forumRecentSectionTitle: "हाल के मंच", + forumAllSectionTitle: "मंच", forumDeleteButton: "हटाएँ", forumParticipants: "प्रतिभागी", forumMessages: "संदेश", + forumLastMessage: "अंतिम संदेश", forumMessageLabel: "संदेश", forumMessagePlaceholder: "अपना संदेश लिखें...", forumSendButton: "भेजें", + forumVisitForum: "मंच देखें", noForums: "कोई मंच नहीं मिला।", - forumVisitButton: "फ़ोरम देखें", - forumTopicLabel: "विषय", + forumVisitButton: "मंच देखें", + forumCatGENERAL: "सामान्य", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", + forumCatPOLITICS: "राजनीति", + forumCatTECH: "प्रौद्योगिकी", + forumCatSCIENCE: "विज्ञान", + forumCatMUSIC: "संगीत", + forumCatART: "कला", + forumCatGAMING: "गेमिंग", + forumCatBOOKS: "पुस्तकें", + forumCatFILMS: "फ़िल्में", + forumCatPHILOSOPHY: "दर्शन", + forumCatSOCIETY: "समाज", + forumCatPRIVACY: "गोपनीयता", + forumCatCYBERWARFARE: "साइबरयुद्ध", + forumCatSURVIVALISM: "जीवनवाद", imageTitle: "छवियाँ", imageDescription: "अपने नेटवर्क में छवि सामग्री खोजें और प्रबंधित करें।", + imagePluginTitle: "शीर्षक", + imagePluginDescription: "विवरण", imageMineSectionTitle: "आपकी छवियाँ", imageCreateSectionTitle: "छवि अपलोड करें", imageUpdateSectionTitle: "छवि अपडेट करें", @@ -1491,12 +1625,14 @@ module.exports = { imageTopSectionTitle: "शीर्ष छवियाँ", imageFavoritesSectionTitle: "पसंदीदा", imageGallerySectionTitle: "गैलरी", + imageMemeSectionTitle: "मीम्स", imageFilterAll: "सभी", imageFilterMine: "मेरे", imageFilterRecent: "हाल के", imageFilterTop: "शीर्ष", imageFilterFavorites: "पसंदीदा", imageFilterGallery: "गैलरी", + imageFilterMeme: "मीम्स", imageCreateButton: "छवि अपलोड करें", imageUpdateButton: "अपडेट", imageDeleteButton: "हटाएँ", @@ -1509,6 +1645,7 @@ module.exports = { imageTitlePlaceholder: "वैकल्पिक", imageDescriptionLabel: "विवरण", imageDescriptionPlaceholder: "वैकल्पिक", + imageMemeLabel: "मीम के रूप में चिह्नित करें", imageNoFile: "कोई छवि फ़ाइल प्रदान नहीं की गई", noImages: "कोई छवि उपलब्ध नहीं।", imageSearchPlaceholder: "शीर्षक, टैग, विवरण, लेखक खोजें...", @@ -1516,6 +1653,7 @@ module.exports = { imageSortOldest: "सबसे पुराना", imageSortTop: "सर्वाधिक मतदान", imageSearchButton: "खोजें", + imageMessageAuthorButton: "संदेश", imageUpdatedAt: "अपडेट किया गया", imageNoMatch: "आपकी खोज से कोई छवि मेल नहीं खाती।", feedTitle: "फ़ीड", @@ -1523,6 +1661,7 @@ module.exports = { createFeedButton: "फ़ीड भेजें!", feedPlaceholder: "क्या हो रहा है? (अधिकतम 280 अक्षर)", TODAYButton: "आज", + CREATEButton: "फ़ीड बनाएँ", totalOpinions: "कुल राय", moreVoted: "अधिक मतदान", noFeedsFound: "कोई फ़ीड नहीं मिली।", @@ -1545,12 +1684,20 @@ module.exports = { concept: "अवधारणा", description: "विवरण", meme: "मीम", + activityContact: "संपर्क", + activityBy: "नाम", + activityPixelia: "नया पिक्सेल जोड़ा गया", + viewImage: "छवि देखें", + playAudio: "ऑडियो चलाएँ", + playVideo: "वीडियो चलाएँ", typeRecent: "हाल के", - typeTop: "शीर्ष", + errorActivity: "गतिविधि प्राप्त करने में त्रुटि", typePost: "ब्लॉग्स", + recentButton: "हाल का", typeTribe: "जनजातियाँ", typeLarp: "L.A.R.P.", typeInhabitants: "निवासी", + activityProfileUpdated: "ने अपनी प्रोफ़ाइल अपडेट की", typeAbout: "निवासी", typeCurriculum: "CVS", typeImage: "छवियाँ", @@ -1594,6 +1741,8 @@ module.exports = { typeCourtsSettlementAccepted: "न्यायालय · समझौता स्वीकृत", typeCourtsNomination: "न्यायालय · नामांकन", typeCourtsNominationVote: "न्यायालय · नामांकन मत", + activitySupport: "नया गठबंधन बना", + activityJoin: "नए PUB में शामिल हुए", question: "प्रश्न", deadline: "समय सीमा", status: "स्थिति", @@ -1607,8 +1756,12 @@ module.exports = { date: "दिनांक", category: "श्रेणी", attendees: "उपस्थित", + activitySpread: "->", + visitLink: "लिंक देखें", + viewDocument: "दस्तावेज़ देखें", location: "स्थान", contentWarning: "विषय", + personName: "निवासी का नाम", bankWalletConnected: "ECOin वॉलेट", bankUbiReceived: "UBI प्राप्त", bankTx: "Tx", @@ -1618,6 +1771,7 @@ module.exports = { link: "लिंक", activityProjectFollow: "%OASIS% अब इस परियोजना %PROJECT% को %ACTION% कर रहा है", activityProjectUnfollow: "%OASIS% अब इस परियोजना %PROJECT% को %ACTION% कर रहा है", + activityProjectPledged: "%OASIS% ने परियोजना %PROJECT% को %AMOUNT% %ACTION% किया है", following: "फ़ॉलो कर रहे हैं", unfollowing: "अनफ़ॉलो कर रहे हैं", pledged: "प्रतिज्ञा", @@ -1663,17 +1817,26 @@ module.exports = { courtsVotesSlashTotal: "हाँ/कुल", courtsOpenVote: "मतदान खोलें", courtsAnswerTitle: "उत्तर", + courtsStanceADMIT: "स्वीकार", + courtsStanceDENY: "अस्वीकार", + courtsStancePARTIAL: "आंशिक", + courtsStanceCOUNTERCLAIM: "प्रतिदावा", + courtsStanceNEUTRAL: "तटस्थ", courtsVerdictResult: "परिणाम", courtsVerdictOrders: "आदेश", courtsSettlementText: "समझौता", courtsSettlementAccepted: "स्वीकृत", + courtsSettlementPending: "लंबित", courtsJudge: "न्यायाधीश", courtsThSupports: "समर्थन", courtsFilterOpenCase: 'मामला खोलें', + courtsEvidenceFileLabel: 'साक्ष्य फ़ाइल (छवि, ऑडियो, वीडियो या PDF)', + courtsCaseMediators: 'मध्यस्थ', courtsCaseMediatorsPh: 'मध्यस्थ Oasis IDs, अल्पविराम से अलग', courtsMediatorsLabel: 'मध्यस्थ', courtsThCase: 'मामला', courtsThCreatedAt: 'प्रारंभ तिथि', + courtsThActions: 'कार्रवाई', courtsPublicPrefLabel: 'समाधान के बाद दृश्यता', courtsPublicPrefYes: 'मैं सहमत हूँ कि यह मामला पूर्ण रूप से सार्वजनिक हो सकता है', courtsPublicPrefNo: 'मैं विवरण निजी रखना पसंद करता हूँ', @@ -1681,11 +1844,8 @@ module.exports = { courtsMethodMEDIATION: 'मध्यस्थता', courtsNoCases: 'कोई मामला नहीं।', reportsTitle: "रिपोर्ट", - reportsDescription: "अपने नेटवर्क की रिपोर्टों को प्रबंधित करें और उन पर नज़र रखें।", - reportsSearchPlaceholder: "रिपोर्ट खोजें...", + reportsDescription: "अपने नेटवर्क में मुद्दों, बग, दुरुपयोग और सामग्री चेतावनियों से संबंधित रिपोर्ट प्रबंधित करें और ट्रैक करें।", reportsFilterAll: "सभी", - reportsFilterRecent: "हाल के", - reportsFilterTop: "शीर्ष", reportsFilterMine: "मेरे", reportsFilterFeatures: "सुविधाएँ", reportsFilterBugs: "बग", @@ -1698,13 +1858,18 @@ module.exports = { reportsFilterInvalid: "अमान्य", reportsCreateButton: "रिपोर्ट बनाएँ", reportsTitleLabel: "शीर्षक", + reportsDescriptionLabel: "विवरण", + reportsDescriptionPlaceholder: "कृपया विस्तृत विवरण प्रदान करें।", reportsCategory: "श्रेणी", + reportsCategoryLabel: "श्रेणी", reportsCategoryFeatures: "सुविधाएँ", reportsCategoryBugs: "बग", reportsCategoryAbuse: "दुरुपयोग", - reportsCategoryContent: "सामग्री", + reportsCategoryContent: "सामग्री समस्याएँ", reportsUpdateButton: "अपडेट", reportsDeleteButton: "हटाएँ", + reportsDateLabel: "दिनांक", + reportsUploadFile: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", reportsMineSectionTitle: "आपकी रिपोर्ट", reportsFeaturesSectionTitle: "सुविधा अनुरोध", reportsBugsSectionTitle: "बग", @@ -1712,6 +1877,11 @@ module.exports = { reportsContentSectionTitle: "सामग्री समस्याएँ", reportsAllSectionTitle: "रिपोर्ट", reportsNoItems: "कोई रिपोर्ट उपलब्ध नहीं।", + reportsValidationTitle: "कृपया एक वैध शीर्षक दर्ज करें।", + reportsValidationDescription: "विवरण खाली नहीं हो सकता।", + reportsValidationCategory: "कृपया एक श्रेणी चुनें।", + reportsCreatedAt: "बनाया गया", + reportsCreatedBy: "द्वारा", reportsSeverity: "गंभीरता", reportsSeverityLow: "कम", reportsSeverityMedium: "मध्यम", @@ -1722,6 +1892,9 @@ module.exports = { reportsStatusUnderReview: "समीक्षाधीन", reportsStatusResolved: "हल किया गया", reportsStatusInvalid: "अमान्य", + reportsUpdateStatusButton: "स्थिति अपडेट करें", + reportsAnonymityOption: "गुमनाम रूप से जमा करें", + reportsAnonymousAuthor: "गुमनाम", reportsConfirmButton: "रिपोर्ट पुष्टि करें!", reportsConfirmations: "पुष्टि", reportsConfirmedSectionTitle: "पुष्ट रिपोर्ट", @@ -1769,20 +1942,6 @@ module.exports = { reportsRequestedActionLabel: 'अनुरोधित कार्रवाई', reportsRequestedActionPlaceholder: 'हटाएँ, छुपाएँ, टैग करें, चेतावनी दें, आदि।', tribesTitle: "जनजातियाँ", - tribeFilterAll: "सभी", - tribeFilterRecent: "हाल के", - tribeFilterMine: "मेरे", - tribeFilterMembership: "सदस्यता", - tribeFilterSubtribes: "उप-जनजातियाँ", - tribeFilterTop: "शीर्ष", - tribeFilterGallery: "गैलरी", - tribeFeedFilterTOP: "शीर्ष", - tribeFeedFilterMINE: "मेरे", - tribeFeedFilterALL: "सभी", - tribeFeedFilterRECENT: "हाल के", - courtsStanceDENY: "इनकार", - courtsStanceADMIT: "स्वीकार", - courtsStancePARTIAL: "आंशिक स्वीकार", tribeAllSectionTitle: "जनजातियाँ", tribeMineSectionTitle: "आपकी जनजातियाँ", tribeCreateSectionTitle: "जनजाति बनाएँ", @@ -1794,6 +1953,13 @@ module.exports = { tribeviewSubTribeButton: "उप-जनजाति देखें", tribeRootLabel: "मूल", tribeDescription: "अपने नेटवर्क में जनजातियाँ खोजें या बनाएँ।", + tribeFilterAll: "सभी", + tribeFilterMine: "मेरे", + tribeFilterMembership: "सदस्यता", + tribeFilterRecent: "हाल के", + tribeFilterTop: "शीर्ष", + tribeFilterSubtribes: "उप-जनजातियाँ", + tribeFilterGallery: "गैलरी", tribeMainTribeLabel: "मुख्य जनजाति", tribeCreateButton: "जनजाति बनाएँ", tribeUpdateButton: "अपडेट", @@ -1812,8 +1978,13 @@ module.exports = { tribeModeLabel: "मोड", tribeIsAnonymousLabel: "स्थिति", tribeMembersCount: "सदस्य", + tribeInviteCodePlaceholder: "आमंत्रण कोड दर्ज करें", + tribeJoinByCodeButton: "कोड से जुड़ें", + tribeJoinButton: "शामिल हों", tribeEnterInvite: "कोड दर्ज करें", tribeLeaveButton: "छोड़ें", + tribeYes: "हाँ", + tribeNo: "नहीं", tribePublic: "सार्वजनिक", tribePrivate: "निजी", tribeGenerateInvite: "एकल निमंत्रण", @@ -1822,8 +1993,13 @@ module.exports = { tribeRemoveInvitation: "निमंत्रण हटाएँ", tribeCreatedAt: "बनाया गया", tribeAuthor: "द्वारा", + tribeAuthorLabel: "लेखक", tribeStrict: "सख़्त", tribeOpen: "खुला", + tribeFeedFilterRECENT: "हाल के", + tribeFeedFilterMINE: "मेरे", + tribeFeedFilterALL: "सभी", + tribeFeedFilterTOP: "शीर्ष", tribeFeedRefeeds: "रीफ़ीड", tribeFeedRefeed: "रीफ़ीड", tribeFeedMessagePlaceholder: "एक फ़ीड लिखें…", @@ -1833,12 +2009,17 @@ module.exports = { tribeNotFound: "जनजाति नहीं मिली!", createTribeTitle: "जनजाति बनाएँ", updateTribeTitle: "जनजाति अपडेट करें", + tribeSectionOverview: "अवलोकन", tribeSectionInhabitants: "निवासी", tribeSectionVotations: "मतदान", tribeSectionEvents: "कार्यक्रम", + tribeSectionReports: "रिपोर्ट", tribeSectionTasks: "कार्य", tribeSectionFeed: "फ़ीड", tribeSectionForum: "मंच", + tribeSectionMarket: "बाज़ार", + tribeSectionJobs: "नौकरियाँ", + tribeSectionProjects: "परियोजनाएँ", tribeSectionMedia: "मीडिया", tribeSectionImages: "छवियाँ", tribeSectionAudios: "ऑडियो", @@ -1876,6 +2057,11 @@ module.exports = { tribeTaskStatusClosed: "बंद करें", tribeTaskAssign: "सौंपें", tribeTaskUnassign: "सौंपना रद्द करें", + tribeReportCreate: "रिपोर्ट बनाएँ", + tribeReportsEmpty: "अभी तक कोई रिपोर्ट नहीं।", + tribeReportTitle: "शीर्षक", + tribeReportDescription: "विवरण", + tribeReportCategory: "श्रेणी", tribeVotationCreate: "मतदान बनाएँ", tribeVotationsEmpty: "अभी तक कोई मतदान नहीं।", tribeVotationTitle: "शीर्षक", @@ -1893,6 +2079,27 @@ module.exports = { tribeForumCategory: "श्रेणी", tribeForumReply: "उत्तर दें", tribeForumReplies: "उत्तर", + tribeMarketCreate: "लिस्टिंग बनाएँ", + tribeMarketEmpty: "अभी तक कोई लिस्टिंग नहीं।", + tribeMarketTitle: "शीर्षक", + tribeMarketDescription: "विवरण", + tribeMarketPrice: "मूल्य", + tribeMarketImage: "छवि", + tribeMarketCategory: "श्रेणी", + tribeJobCreate: "नौकरी बनाएँ", + tribeJobsEmpty: "अभी तक कोई नौकरी नहीं।", + tribeJobTitle: "शीर्षक", + tribeJobDescription: "विवरण", + tribeJobLocation: "स्थान", + tribeJobSalary: "वेतन", + tribeJobDeadline: "समय सीमा", + tribeProjectCreate: "परियोजना बनाएँ", + tribeProjectsEmpty: "अभी तक कोई परियोजना नहीं।", + tribeProjectTitle: "शीर्षक", + tribeProjectDescription: "विवरण", + tribeProjectGoal: "लक्ष्य", + tribeProjectFunded: "वित्तपोषित", + tribeProjectDeadline: "समय सीमा", tribeMediaUpload: "मीडिया अपलोड करें", readDocument: "दस्तावेज़ पढ़ें", tribeCreateImage: "छवि बनाएँ", @@ -1903,6 +2110,7 @@ module.exports = { tribeMediaEmpty: "अभी तक कोई मीडिया नहीं।", tribeMediaTitle: "शीर्षक", tribeMediaDescription: "विवरण", + tribeMediaType: "प्रकार", tribeMediaTypeImage: "छवि", tribeMediaTypeVideo: "वीडियो", tribeMediaTypeAudio: "ऑडियो", @@ -1912,6 +2120,11 @@ module.exports = { tribeInviteCodeText: "आमंत्रण कोड: ", oasisVersionLabel: "Oasis संस्करण", tribeInviteCodeHint: "इस कोड को साझा करें जिसे आप आमंत्रित करना चाहते हैं। वे /invites → Tribes से शामिल हो सकते हैं।", + tribeGroupTribe: "जनजाति", + tribeGroupOffice: "कार्यालय", + tribeGroupNetwork: "नेटवर्क", + tribeGroupEconomy: "अर्थव्यवस्था", + tribeGroupMedia: "मीडिया", tribeStatusOpen: "खुला", tribeStatusClosed: "बंद", tribeStatusInProgress: "प्रगति में", @@ -1921,17 +2134,33 @@ module.exports = { tribePriorityCritical: "गंभीर", tribeTaskFilterAll: "सभी", tribeMediaFilterAll: "सभी", + tribeReportCatBug: "बग", + tribeReportCatAbuse: "दुरुपयोग", + tribeReportCatContent: "सामग्री", + tribeReportCatOther: "अन्य", tribeForumCatGeneral: "सामान्य", tribeForumCatProposal: "प्रस्ताव", tribeForumCatQuestion: "प्रश्न", tribeForumCatAnnouncement: "घोषणा", + tribeMarketCatGoods: "सामान", + tribeMarketCatServices: "सेवाएँ", + tribeMarketCatFood: "भोजन", + tribeMarketCatOther: "अन्य", tribeStatusLabel: "स्थिति", tribeSubTribes: "उप-जनजातियाँ", tribeSubTribesCreate: "उप-जनजाति बनाएँ", tribeSubTribesStrictDenied: "जनजाति का सख्त मोड आपको नई उप-जनजातियाँ बनाने की अनुमति नहीं देता। कृपया प्रशासक से संपर्क करें।", + tribeSubTribesEmpty: "अभी तक कोई उप-जनजाति नहीं बनाई गई।", + tribeActivityJoined: "शामिल हुए", + tribeActivityLeft: "छोड़ दिया", + tribeActivityFeed: "फ़ीड", tribeActivityRefeed: "रीफ़ीड", + tribeGroupAnalytics: "विश्लेषिकी", + tribeGroupCreative: "रचनात्मक", tribeSectionActivity: "गतिविधि", tribeSectionTrending: "ट्रेंडिंग", + tribeSectionOpinions: "राय", + tribeSectionPixelia: "PIXELIA", tribeSectionTags: "टैग", tribeSectionSearch: "खोज", tribeActivityEmpty: "अभी तक कोई गतिविधि नहीं।", @@ -1943,15 +2172,25 @@ module.exports = { tribeTrendingPeriodWeek: "इस सप्ताह", tribeTrendingPeriodAll: "सभी समय", tribeTrendingEngagement: "सहभागिता", + tribeOpinionsEmpty: "अभी तक कोई राय नहीं।", + tribeOpinionsCast: "मत दें", + tribeOpinionsRankings: "रैंकिंग", + tribeOpinionsAlreadyVoted: "पहले ही मतदान किया", tribeTopCategory: "अधिक मतदान", + tribePixeliaTitle: "Pixelia", + tribePixeliaDescription: "सहयोगी पिक्सेल कला कैनवास।", + tribePixeliaPaint: "रंग भरें", + tribePixeliaContributors: "योगदानकर्ता", + tribePixeliaTotalPixels: "रंगे गए पिक्सेल", tribeTagsEmpty: "कोई टैग नहीं मिला।", + tribeTagsCloud: "टैग क्लाउड", + tribeTagsContentWith: "टैग वाली सामग्री", tribeSearchPlaceholder: "जनजाति सामग्री खोजें...", tribeSearchEmpty: "कोई परिणाम नहीं मिला।", tribeSearchResults: "परिणाम", tribeSearchMinChars: "खोजने के लिए कम से कम 2 अक्षर दर्ज करें।", agendaTitle: "एजेंडा", agendaDescription: "यहाँ आप अपने सभी सौंपे गए आइटम पा सकते हैं।", - agendaSearchPlaceholder: "एजेंडा खोजें...", agendaFilterAll: "सभी", agendaFilterToday: "आज", agendaFilterUpcoming: "आगामी", @@ -1965,14 +2204,23 @@ module.exports = { agendaFilterReports: "रिपोर्ट", agendaFilterTransfers: "स्थानांतरण", agendaFilterJobs: "नौकरियाँ", - agendaFilterIndustry: "उद्योग", - agendaFilterHousing: "आवास", agendaFilterProjects: "परियोजनाएँ", agendaFilterCalendars: "कैलेंडर", agendaNoItems: "कोई सौंपे गए आइटम नहीं मिले।", + agendaAuthor: "द्वारा", agendaDiscardButton: "निरस्त करें", agendaRestoreButton: "पुनर्स्थापित करें", + agendaCreatedAt: "बनाया गया", + agendaTitleLabel: "शीर्षक", agendaMembersCount: "सदस्य", + agendaDescriptionLabel: "विवरण", + agendaStatus: "स्थिति", + agendaVisibility: "दृश्यता", + agendaEventDate: "कार्यक्रम तिथि", + agendaEventLocation: "स्थान", + agendaEventPrice: "मूल्य", + agendaEventUrl: "URL", + agendaTaskStart: "प्रारंभ समय", agendaLocationLabel: "स्थान", agendaYes: "हाँ", agendaNo: "नहीं", @@ -1982,6 +2230,11 @@ module.exports = { agendareportCategory: "श्रेणी", agendareportSeverity: "गंभीरता", agendareportStatus: "स्थिति", + agendareportDescription: "विवरण", + agendaTaskEnd: "समाप्ति समय", + agendaTaskPriority: "प्राथमिकता", + agendaTransferFrom: "से", + agendaTransferTo: "को", agendaTransferConcept: "अवधारणा", agendaTransferAmount: "राशि", agendaTransferDeadline: "समय सीमा", @@ -1991,7 +2244,47 @@ module.exports = { voteNow: "अभी मतदान करें", alreadyVoted: "आपने पहले ही राय दे दी है।", noOpinionsFound: "कोई राय नहीं मिली।", + RECENTButton: "हाल के", TOPButton: "शीर्ष", + interestingButton: "दिलचस्प", + necessaryButton: "आवश्यक", + funnyButton: "मज़ेदार", + disgustingButton: "घृणित", + sensibleButton: "समझदार", + propagandaButton: "प्रचार", + adultOnlyButton: "केवल वयस्क", + boringButton: "उबाऊ", + confusingButton: "भ्रमित करने वाला", + usefulButton: "उपयोगी", + informativeButton: "सूचनाप्रद", + wellResearchedButton: "अच्छी तरह शोधित", + accurateButton: "सटीक", + needsSourcesButton: "स्रोत चाहिए", + wrongButton: "गलत", + lowQualityButton: "निम्न गुणवत्ता", + creativeButton: "रचनात्मक", + insightfulButton: "अंतर्दृष्टिपूर्ण", + actionableButton: "कार्रवाई योग्य", + inspiringButton: "प्रेरणादायक", + loveButton: "प्यार", + clearButton: "स्पष्ट", + upliftingButton: "उत्थानकारी", + unnecessaryButton: "अनावश्यक", + rejectedButton: "अस्वीकृत", + misleadingButton: "भ्रामक", + offTopicButton: "विषय से हटकर", + duplicateButton: "डुप्लिकेट", + clickbaitButton: "क्लिकबेट", + spamButton: "स्पैम", + trollButton: "ट्रोल", + nsfwButton: "NSFW", + violentButton: "हिंसक", + toxicButton: "विषाक्त", + harassmentButton: "उत्पीड़न", + hateButton: "घृणा", + scamButton: "धोखाधड़ी", + triggeringButton: "उत्तेजक", + opinionsCreatedAt: "बनाया गया", opinionsTotalCount: "कुल राय", voteInteresting: "दिलचस्प", voteNecessary: "आवश्यक", @@ -2028,39 +2321,27 @@ module.exports = { voteCreative: "रचनात्मक", voteSpam: "स्पैम", voteAdultOnly: "केवल वयस्क", + publishBlog: "ब्लॉग प्रकाशित करें", privateMessage: "PM", pmSendTitle: "निजी संदेश", pmSend: "भेजें!", - pmCancel: "रद्द करें", - pmReset: "रीसेट", - pmSharedKeyHint: "इस कुंजी को प्राप्तकर्ता के साथ किसी अन्य माध्यम से साझा करें। डिक्रिप्ट करने के लिए यह आवश्यक है।", - pmDescription: "अन्य निवासियों को एन्क्रिप्टेड संदेश/फ़ाइल भेजने के लिए इस फ़ॉर्म का उपयोग करें।", - pmComposeTitle: "संदेश भेजें", + pmDescription: "अन्य निवासियों को एन्क्रिप्टेड संदेश भेजने के लिए इस फ़ॉर्म का उपयोग करें।", pmRecipients: "प्राप्तकर्ता", pmRecipientsHint: "अल्पविराम से अलग Oasis IDs दर्ज करें", - pmLimitsHint: "प्रति संदेश अधिकतम 7 प्राप्तकर्ता।", - pmTextPlaceholder: "मुख्य पाठ 7000 वर्णों तक सीमित।", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "विषय", pmSubjectHint: "संदेश विषय दर्ज करें", pmText: "संदेश", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "डिक्रिप्ट करें", - fileShareTitle: "फ़ाइल साझा करें", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "फ़ाइल", - fileShareDownload: "डाउनलोड करें", - fileShareMutualError: "आप केवल पारस्परिक समर्थन वाले निवासियों के साथ फ़ाइलें साझा कर सकते हैं।", - fileShareNoFile: "कोई फ़ाइल चयनित नहीं।", - fileShareTooLarge: "फ़ाइल अनुमत आकार से बड़ी है।", - fileShareFailed: "फ़ाइल तैयार नहीं की जा सकी।", - fileShareSendError: "फ़ाइल भेजी नहीं जा सकी।", - fileShareUnavailable: "यह फ़ाइल अभी उपलब्ध नहीं है। बाद में पुनः प्रयास करें।", pmCrypterBadKey: "साझा कुंजी गलत है!", pmCrypterTooLong: "यह संदेश Crypter से एन्क्रिप्ट करने के लिए बहुत लंबा है। कृपया इसे छोटा करके पुनः प्रयास करें।", pmCrypterCipherLabel: "पुनः एन्क्रिप्टेड संदेश", pmCrypterCharsLabel: "अक्षर", pmInvalidRecipients: "अमान्य Oasis ID (यह @…=.ed25519 जैसा होना चाहिए)।", pmEncryptionLabel: "एन्क्रिप्शन", + pmFile: "संलग्नक", private: "निजी", privateDescription: "आपके एन्क्रिप्टेड संदेश।", privateInbox: "इनबॉक्स", @@ -2070,27 +2351,19 @@ module.exports = { noPrivateMessages: "कोई निजी संदेश नहीं।", pmFromLabel: "से:", pmToLabel: "को:", + pmInvalidMessage: "अमान्य संदेश", pmNoSubject: "(कोई विषय नहीं)", pmSubjectLabel: "विषय:", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "L.A.R.P का शासक घराना बदल गया है।", - politicalBotParliamentTitle: "संसद में एक नया शासन चक्र शुरू हो गया है।", - politicalBotTribeTitle: "आपकी किसी जनजाति में एक नया शासन चक्र शुरू हो गया है।", - politicalBotLarpIntro: "अब इस चक्र में {house} शासन करता है: {cycle}", - politicalBotParliamentIntro: "वर्तमान सरकार {gov} है", - politicalBotParliamentIntroLeader: "वर्तमान सरकार {gov} है, {leader} द्वारा संचालित", - politicalBotTribeIntro: "जनजाति {tribe} में वर्तमान सरकार {gov} है", - politicalBotTribeIntroLeader: "जनजाति {tribe} में वर्तमान सरकार {gov} है, {leader} द्वारा संचालित", - politicalBotLeaderLabel: "नेता", + pmBodyLabel: "मुख्य भाग", + pmBotJobs: "42-JobsBOT", + pmBotProjects: "42-ProjectsBOT", + pmBotMarket: "42-MarketBOT", inboxJobSubscribedTitle: "आपके नौकरी प्रस्ताव के लिए नई सदस्यता", pmInhabitantWithId: "OASIS ID वाला निवासी:", pmHasSubscribedToYourJobOffer: "ने आपके नौकरी प्रस्ताव की सदस्यता ली है", + inboxProjectCreatedTitle: "नई परियोजना बनाई गई", + pmHasCreatedAProject: "ने एक परियोजना बनाई है", inboxMarketItemSoldTitle: "आइटम बिक गया", - inboxShopSoldTitle: "उत्पाद बिका", pmYourItem: "आपका आइटम", pmHasBeenSoldTo: "बेचा गया", pmFor: "के लिए", @@ -2098,6 +2371,7 @@ module.exports = { pmHasPledged: "ने प्रतिज्ञा की है", pmToYourProject: "आपकी परियोजना को", blockchain: 'BlockExplorer', + blockchainTitle: 'BlockExplorer', blockchainDescription: 'ब्लॉकचेन में ब्लॉक खोजें और विज़ुअलाइज़ करें।', blockchainNoBlocks: 'ब्लॉकचेन में कोई ब्लॉक नहीं मिला।', blockchainStatsTitle: 'आँकड़े', @@ -2109,36 +2383,42 @@ module.exports = { blockchainBlockCarbon: 'कार्बन फुटप्रिंट', blockchainBlockType: 'प्रकार', blockchainBlockTimestamp: 'टाइमस्टैम्प', + blockchainBlockContent: 'ब्लॉक', + blockchainBlockURL: 'URL:', + blockchainContent: 'ब्लॉक', + blockchainContentPreview: 'ब्लॉक सामग्री का पूर्वावलोकन', blockchainLatestDatagram: 'नवीनतम डेटाग्राम', - blockchainDatagram: "डेटाग्राम", - blockchainDetails: "ब्लॉक विवरण देखें", - blockchainViewBlockexplorer: "Blockexplorer में देखें", + blockchainDatagram: 'डेटाग्राम', + blockchainDetails: 'ब्लॉक विवरण देखें', + blockchainViewBlockexplorer: "ब्लॉकएक्सप्लोरर में देखें", + blockchainBlockInfo: 'ब्लॉक जानकारी', + blockchainBlockDetails: 'चयनित ब्लॉक का विवरण', blockchainBack: 'BlockExplorer पर वापस जाएँ', blockchainContentDeleted: "यह सामग्री टॉम्बस्टोन कर दी गई है", visitContent: "सामग्री देखें", - favoriteAdd: "पसंदीदा में पिन करें", - pmContentTooltip: "निजी संदेश भेजें", - favoriteRemove: "पसंदीदा से हटाएँ", - reportContent: "सामग्री की रिपोर्ट करें", banking: 'बैंकिंग', bankingTitle: 'बैंकिंग', - bankingDescription: "ECOin अर्थव्यवस्था: शेष, यूबीआई, कर और उद्योग मूल्य।", + bankingDescription: 'ECOin का वर्तमान मूल्य और संबंधित UBI आवंटन खोजें, जो भागीदारी और विश्वास के आधार पर प्रति युग वितरित होता है।', bankOverview: 'अवलोकन', bankEpochs: 'युग', bankRules: 'नियम', pending: 'लंबित', closed: 'बंद', + bankBack: 'बैंकिंग पर वापस जाएँ', bankViewTx: 'Tx देखें', bankClaimNow: 'अभी दावा करें', bankClaimUBI: 'UBI का दावा करें!', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: 'इस अवधि के लिए कोई लंबित UBI आवंटन नहीं।', bankEpoch: 'युग', bankPool: 'पूल (यह युग)', bankWeightsSum: 'भारों का योग', + bankAllocations: 'आवंटन', bankNoAllocations: 'कोई आवंटन नहीं मिला।', bankNoEpochs: 'कोई युग नहीं मिला।', bankEpochAllocations: 'युग आवंटन', @@ -2152,11 +2432,10 @@ module.exports = { bankEpochId: 'युग ID', bankRuleHash: 'नियम स्नैपशॉट हैश', bankViewEpoch: 'युग देखें', - bankYourIndustryBalance: "आपका औद्योगिक हिस्सा", - bankYourUbiMonth: "आपकी UBI (इस माह)", - bankYourFundsMonth: "आपकी निधि (इस माह)", - bankIndustryBalance: "औद्योगिक उत्पादन (अनुमानित)", bankUserBalance: 'आपका शेष', + ecoWalletNotConfigured: 'ECOin वॉलेट कॉन्फ़िगर नहीं है', + editWallet: 'वॉलेट संपादित करें', + addWallet: 'वॉलेट जोड़ें', bankAddresses: 'पते', bankNoAddresses: 'कोई पता नहीं मिला।', bankUser: 'Oasis ID', @@ -2181,10 +2460,18 @@ module.exports = { search: 'खोजें!', bankLocal: 'स्थानीय', bankFromOasis: 'Oasis', + bankMyAddress: 'आपका पता', + bankRemoveMyAddress: 'मेरा पता हटाएँ', + bankNotRemovableOasis: 'पते स्थानीय रूप से नहीं हटाए जा सकते', + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "कोई धन नहीं!", bankUbiAvailableOk: "उपलब्ध!", - bankUbiAvailability: "UBI (नेटवर्क)", + bankUbiAvailability: "UBI उपलब्धता", bankAlreadyClaimedThisMonth: "इस महीने पहले ही दावा किया गया", bankUbiThisMonth: "UBI (इस महीने)", bankUbiLastClaimed: "UBI (अंतिम दावा)", @@ -2198,6 +2485,13 @@ module.exports = { shopsTitle: "दुकानें", shopDescription: "नेटवर्क में दुकानें खोजें और प्रबंधित करें।", shopTitle: "दुकान", + shopFilterAll: "सभी", + shopFilterMine: "मेरी", + shopFilterRecent: "हालिया", + shopFilterTop: "शीर्ष", + shopFilterProducts: "उत्पाद", + shopFilterPrices: "मूल्य", + shopFilterFavorites: "पसंदीदा", shopUpload: "दुकान बनाएं", shopAllSectionTitle: "सभी दुकानें", shopMineSectionTitle: "मेरी दुकानें", @@ -2214,10 +2508,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "Clearnet पर प्रकाशित करें", + shopClearnetUnpublish: "Clearnet से हटाएं", + shopClearnetUrlLabel: "Clearnet URL", + shopClearnetWarning: "Clearnet पर प्रकाशित करने से यह दुकान बाहरी सर्च इंजन और क्रॉलर द्वारा अनुक्रमणीय हो जाती है।", shopAddFavorite: "पसंदीदा जोड़ें", shopRemoveFavorite: "पसंदीदा हटाएं", shopOpen: "खुली", shopClosed: "बंद", + shopOpenShop: "दुकान खोलें", + shopCloseShop: "दुकान बंद करें", shopMakePrivate: "निजी बनाएं (E2E)", shopMakePublic: "सार्वजनिक बनाएं", shopProducts: "उत्पाद", @@ -2245,6 +2545,8 @@ module.exports = { shopOrderPrice: "मूल्य", shopOrderBuyer: "खरीदार", shopBackToShop: "दुकान पर वापस", + shopShareUrl: "शेयर URL", + shopVisitShop: "दुकान देखें", marketVisitItem: "आइटम देखें", shopStatus: "स्थिति", shopCreatedAt: "बनाया गया", @@ -2253,10 +2555,12 @@ module.exports = { shopLocation: "स्थान", shopTags: "टैग", shopVisibility: "दृश्यता", + shopImage: "छवि", shopShortDescription: "संक्षिप्त विवरण", shopShortDescriptionPlaceholder: "दुकान कार्ड के लिए संक्षिप्त विवरण (अधिकतम 160 अक्षर)", shopTitlePlaceholder: "आपकी दुकान का नाम", shopDescriptionPlaceholder: "आपकी दुकान का विस्तृत विवरण", + shopUrlPlaceholder: "https://aapki-dukan-url.com", shopLocationPlaceholder: "शहर, देश", shopTagsPlaceholder: "टैग1, टैग2, टैग3", mapTitlePlaceholder: "मानचित्र शीर्षक", @@ -2274,6 +2578,7 @@ module.exports = { bankUnitMinutes: "मिनट", bankUnitDays: "दिन", bankExchangeNoData: 'कोई डेटा उपलब्ध नहीं', + bankExchangeIndex: 'ECOin मूल्य (1h)', bankInflation: 'ECOin मुद्रास्फीति (anual)', bankInflationMonthly: 'ECOin मुद्रास्फीति (mensual)', bankExchangeChartTitle: 'समय के साथ ECOin मूल्य की प्रगति', @@ -2287,17 +2592,38 @@ module.exports = { bankingSyncStatusOutdated: 'पुराना', statsTitle: 'सांख्यिकी', statistics: "सांख्यिकी", + statsInhabitant: "निवासी सांख्यिकी", statsDescription: "अपने नेटवर्क के बारे में सांख्यिकी खोजें।", ALLButton: "सभी", MINEButton: "मेरे", TOMBSTONEButton: "टॉम्बस्टोन", + statsYou: "आप", + statsUserId: "Oasis ID", statsCreatedAt: "बनाया गया", statsYourContent: "सामग्री", statsYourOpinions: "राय", + statsYourTombstone: "टॉम्बस्टोन", + statsNetwork: "नेटवर्क", + statsTotalInhabitants: "निवासी", + statsDiscoveredTribes: "जनजातियाँ (सार्वजनिक)", + statsPrivateDiscoveredTribes: "जनजातियाँ (निजी)", statsNetworkContent: "सामग्री", + statsYourMarket: "बाज़ार", + statsYourJob: "नौकरियाँ", + statsYourProject: "परियोजनाएँ", + statsYourTransfer: "स्थानांतरण", + statsYourForum: "मंच", statsNetworkOpinions: "राय", + statsDiscoveredMarket: "बाज़ार", + statsDiscoveredJob: "नौकरियाँ", + statsDiscoveredProject: "परियोजनाएँ", + statsBankingTitle: "बैंकिंग", statsEcoWalletLabel: "ECOIN वॉलेट", statsEcoWalletNotConfigured: "कॉन्फ़िगर नहीं!", + statsTotalEcoAddresses: "कुल पते", + statsDiscoveredTransfer: "स्थानांतरण", + statsDiscoveredForum: "मंच", + statsNetworkTombstone: "टॉम्बस्टोन", statsBookmark: "बुकमार्क", statsEvent: "कार्यक्रम", statsTask: "कार्य", @@ -2319,12 +2645,16 @@ module.exports = { statsAiExchange: "AI", statsPUBs: 'PUBs', statsPost: "पोस्ट", + statsOasisID: "Oasis ID", statsSize: "कुल (आकार)", statsBlockchainSize: "ब्लॉकचेन (आकार)", statsBlobsSize: "ब्लॉब (आकार)", statsActivity7d: "गतिविधि (पिछले 7 दिन)", statsActivity7dTotal: "7-दिन कुल", statsActivity30dTotal: "30-दिन कुल", + statsKarmaScore: "कर्म स्कोर", + statsPublic: "सार्वजनिक", + statsPrivate: "निजी", day: "दिन", messages: "संदेश", statsProject: "परियोजनाएँ", @@ -2336,14 +2666,30 @@ module.exports = { statsProjectsCancelled: "रद्द", statsProjectsGoalTotal: "कुल लक्ष्य", statsProjectsPledgedTotal: "कुल प्रतिज्ञा", + statsProjectsSuccessRate: "सफलता दर", + statsProjectsAvgProgress: "औसत प्रगति", + statsProjectsMedianProgress: "मध्यांश प्रगति", + statsProjectsActiveFundingAvg: "औसत सक्रिय वित्तपोषण", + statsJobsTitle: "नौकरियाँ", + statsJobsTotal: "कुल नौकरियाँ", + statsJobsOpen: "खुली", + statsJobsClosed: "बंद", + statsJobsOpenVacants: "खुली रिक्तियाँ", + statsJobsSubscribersTotal: "कुल सदस्य", + statsJobsAvgSalary: "औसत वेतन", + statsJobsMedianSalary: "मध्यांश वेतन", statsMarketTitle: "बाज़ार", statsMarketTotal: "कुल आइटम", statsMarketForSale: "बिक्री के लिए", statsMarketReserved: "आरक्षित", statsMarketClosed: "बंद", statsMarketSold: "बेचा गया", + statsMarketRevenue: "राजस्व", + statsMarketAvgSoldPrice: "औसत बिक्री मूल्य", statsUsersTitle: "निवासी", user: "निवासी", + statsTombstoneTitle: "टॉम्बस्टोन", + statsNetworkTombstones: "नेटवर्क टॉम्बस्टोन", statsTombstoneRatio: "टॉम्बस्टोन अनुपात (%)", statsParliamentCandidature: "संसद उम्मीदवारी", statsParliamentTerm: "संसद कार्यकाल", @@ -2370,21 +2716,30 @@ module.exports = { aiConfiguration: "प्रॉम्प्ट सेट करें", aiPromptUsed: "प्रॉम्प्ट", aiClearHistory: "चैट इतिहास साफ़ करें", + aiSharePrompt: "यह उत्तर सामूहिक प्रशिक्षण में जोड़ें?", + aiShareYes: "हाँ", + aiShareNo: "नहीं", + aiSharedLabel: "प्रशिक्षण में जोड़ा गया", + aiRejectedLabel: "प्रशिक्षण में नहीं जोड़ा गया", aiServerError: "AI उत्तर नहीं दे सका। कृपया पुनः प्रयास करें।", aiInputPlaceholder: "Oasis क्या है?", typeAiExchange: "AI", aiApproveTrain: "सामूहिक प्रशिक्षण में जोड़ें", aiRejectTrain: "प्रशिक्षण न करें", + aiTrainPending: "अनुमोदन लंबित", aiTrainApproved: "प्रशिक्षण के लिए अनुमोदित", aiTrainRejected: "प्रशिक्षण के लिए अस्वीकृत", aiSnippetsUsed: "उपयोग किए गए अंश", aiSnippetsLearned: "सीखे गए अंश", statsAITraining: "AI प्रशिक्षण", + aiApproveCustomTrain: "इस कस्टम उत्तर का उपयोग करके प्रशिक्षित करें", aiCustomAnswerPlaceholder: "अपना कस्टम उत्तर लिखें…", + statsAIExchanges: "मॉडल आदान-प्रदान", marketMineSectionTitle: "आपके आइटम", marketCreateSectionTitle: "आइटम बनाएँ", marketUpdateSectionTitle: "अपडेट", marketAllSectionTitle: "बाज़ार", + marketRecentSectionTitle: "हाल का बाज़ार", marketTitle: "बाज़ार", marketDescription: "अपने नेटवर्क में सामान या सेवाओं के आदान-प्रदान के लिए एक बाज़ार।", marketFilterAll: "सभी", @@ -2414,11 +2769,14 @@ module.exports = { marketItemDeadline: "समय सीमा", marketItemIncludesShipping: "शिपिंग शामिल है?", marketItemHighestBid: "सबसे ऊँची बोली", + marketItemHighestBidder: "सबसे ऊँची बोली लगाने वाला", marketItemStock: "स्टॉक", marketOutOfStock: "स्टॉक में नहीं", + marketItemBidTime: "बोली समय", marketActionsUpdate: "अपडेट", marketUpdateButton: "अपडेट", marketActionsDelete: "हटाएँ", + marketActionsSold: "बिका हुआ चिह्नित करें", marketActionsChangeStatus: "स्थिति बदलें", marketActionsBuy: "खरीदें!", marketAuctionBids: "वर्तमान बोलियाँ", @@ -2427,82 +2785,19 @@ module.exports = { marketNoItems: "अभी तक कोई आइटम उपलब्ध नहीं।", marketYourBid: "आपकी बोली", marketCreateFormImageLabel: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", + marketSearchLabel: "खोज", marketSearchPlaceholder: "शीर्षक या टैग खोजें", marketMinPriceLabel: "न्यूनतम मूल्य", marketMaxPriceLabel: "अधिकतम मूल्य", + marketSortLabel: "क्रमबद्ध करें", marketSortRecent: "सबसे हाल का", marketSortPrice: "मूल्य", marketSortDeadline: "समय सीमा", marketSearchButton: "खोजें", marketAuctionEndsIn: "समाप्त होता है", marketAuctionEnded: "समाप्त हो गया", + marketMyBidBadge: "आपकी बोली", marketNoItemsMatch: "आपकी खोज से कोई आइटम मेल नहीं खाता।", - housingTitle: "आवास", - housingDescriptionText: "अपने नेटवर्क में जगहें खोजें और प्रबंधित करें।", - modulesHousingLabel: "आवास", - modulesHousingDescription: "जगहें खोजने और प्रबंधित करने का मॉड्यूल।", - typeHousing: "आवास", - housingFilterAll: "सभी", - housingFilterMine: "मेरे", - housingFilterRequested: "अनुरोधित", - housingFilterSale: "बिक्री", - housingFilterRent: "किराया", - housingFilterCouchsurfing: "काउचसर्फिंग", - housingFilterOpen: "उपलब्ध", - housingFilterClosed: "अनुपलब्ध", - housingFilterRecent: "हाल के", - housingFilterTop: "शीर्ष", - housingType: "प्रस्ताव", - housingTypeSALE: "बिक्री के लिए", - housingTypeRENT: "किराये के लिए", - housingTypeCOUCHSURFING: "काउचसर्फिंग", - housingProperty: "संपत्ति", - housingPropertyAPARTMENT: "अपार्टमेंट", - housingPropertyHOUSE: "घर", - housingPropertyROOM: "कमरा", - housingPropertyLAND: "ज़मीन", - housingPropertyOTHER: "अन्य", - housingTitleLabel: "शीर्षक", - housingTitlePlaceholder: "जगह का नाम", - housingDescription: "विवरण", - housingDescriptionPlaceholder: "जगह, आस-पास और क्या शामिल है बताएं: बाथरूम, बगीचा, बिस्तर…", - housingRules: "नियम", - housingRulesPlaceholder: "पालतू जानवर, धूम्रपान, शांति के घंटे, न्यूनतम ठहराव…", - housingPlace: "जगह", - housingPlacePlaceholder: "देश, शहर, पता", - housingPrice: "कीमत", - housingPriceLabel: "कीमत (ECO)", - housingSizeLabel: "क्षेत्रफल (m²)", - housingFree: "मुफ़्त", - housingPerMonth: "/माह", - housingRooms: "कमरे", - housingSize: "क्षेत्रफल", - housingCapacity: "मेहमान", - housingAvailableFrom: "से उपलब्ध", - housingAvailableTo: "तक उपलब्ध", - housingTags: "टैग", - spreadEditWarning: "संपादित करने पर यह सामग्री अपने स्प्रेड खो देगी", - housingRequests: "अनुरोध", - housingRequestButton: "अनुरोध करें", - housingCancelButton: "अनुरोध रद्द करें", - housingRequestedBadge: "अनुरोधित", - housingStatus: "स्थिति", - housingStatusOPEN: "उपलब्ध", - housingStatusCLOSED: "अनुपलब्ध", - housingSetOpen: "उपलब्ध करें", - housingSetClosed: "अनुपलब्ध करें", - housingCreateButton: "जगह बनाएं", - housingUpdateButton: "अपडेट", - housingDeleteButton: "हटाएं", - housingSearchPlaceholder: "जगहें खोजें...", - housingSearchButton: "खोजें", - housingMinPrice: "न्यूनतम कीमत", - housingMaxPrice: "अधिकतम कीमत", - housingSortRecent: "सबसे नए", - housingSortPrice: "सबसे कम कीमत", - housingSortRequests: "सबसे ज़्यादा अनुरोध", - housingSortRating: "सर्वश्रेष्ठ रेटेड", - housingNoItems: "कोई जगह नहीं मिली।", jobsTitle: "नौकरियाँ", jobsCandidatesTitle: "सुझाए गए उम्मीदवार", jobsCandidatesDescription: "ऐसे निवासी जिनके कौशल इस नौकरी से मेल खाते हैं। उन्हें आमंत्रित करने के लिए निजी संदेश भेजें।", @@ -2550,6 +2845,7 @@ module.exports = { jobLocationPresencial: "स्थानीय", jobLocationRemote: "दूरस्थ", jobVacantsPlaceholder: "पदों की संख्या", + jobSalaryPlaceholder: "1 समर्पित घंटे के लिए ECO में वेतन", jobImage: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", jobTasks: "कार्य", jobType: "नौकरी प्रकार", @@ -2559,6 +2855,7 @@ module.exports = { jobsFilterTop: "शीर्ष", jobsTopTitle: "शीर्ष वेतन नौकरियाँ", createJobButton: "नौकरी प्रकाशित करें", + viewDetailsButton: "विवरण देखें", noJobsFound: "कोई नौकरी प्रस्ताव नहीं मिला।", jobAuthor: "द्वारा", jobTypeFreelance: "फ्रीलांसर", @@ -2570,6 +2867,10 @@ module.exports = { jobsHoursOffered: "दिए गए घंटे", jobsHoursRequested: "बदले में माँगे गए घंटे", jobsExchangeSkill: "बदले में चाहिए कौशल", + jobsHoursOfferedPlaceholder: "जैसे 4", + jobsHoursRequestedPlaceholder: "जैसे 4", + jobsExchangeSkillPlaceholder: "जैसे बढ़ईगिरी, डिज़ाइन", + jobExchangeHint: "घंटों के आदान-प्रदान वाली नौकरियों के लिए, वेतन के बजाय नीचे के फ़ील्ड भरें।", jobTimePartial: "अंशकालिक", jobTimeComplete: "पूर्णकालिक", jobsDeleteButton: "हटाएँ", @@ -2577,201 +2878,31 @@ module.exports = { jobsFilterApplied: "आवेदित", jobsAppliedTitle: "मेरे आवेदन", jobsAppliedBadge: "आवेदित", + jobsFilterFavs: "पसंदीदा", + jobsFavsTitle: "पसंदीदा", + jobsFilterNeeds: "मदद चाहिए", + jobsNeedsTitle: "मदद चाहिए", + jobsSearchLabel: "खोज", jobsSearchPlaceholder: "शीर्षक, टैग, विवरण खोजें...", jobsMinSalaryLabel: "न्यूनतम वेतन", jobsMaxSalaryLabel: "अधिकतम वेतन", + jobsSortLabel: "क्रमबद्ध करें", jobsSortRecent: "सबसे हाल का", jobsSortSalary: "सबसे अधिक वेतन", jobsSortSubscribers: "सबसे अधिक आवेदक", jobsSearchButton: "खोजें", + jobsFavoriteButton: "पसंदीदा", + jobsUnfavoriteButton: "पसंदीदा हटाएँ", + jobsMessageAuthorButton: "PM", jobsApplicants: "आवेदक", jobsUpdatedAt: "अपडेट किया गया", jobSetOpen: "खुली सेट करें", + jobNewBadge: "नया", jobsTagsLabel: "टैग", + jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "आपकी खोज से कोई नौकरी मेल नहीं खाती।", - industrySearchPlaceholder: "फैक्टरियाँ खोजें…", - industryAllSectors: "सभी क्षेत्र", - industryVisitButton: "उद्योग देखें", - industryAdvanceTo: "पर सेट करें", - industryAmount: "राशि", - industryApproveBuild: "मंज़ूर करें", - industryBlueprint: "ब्लूप्रिंट", - industryBlueprints: "ब्लूप्रिंट", - industryBuild: "बिल्ड", - industryBuildNotes: "नोट्स", - industryBuildStart: "आरंभ तिथि", - industryVoteUpdateButton: "अपडेट के लिए वोट", - industryVoteDeleteButton: "हटाने के लिए वोट", - industryRevokeVote: "वापस लें", - industryTimeLeft: "शेष समय", - industryBuildEnd: "समाप्ति तिथि", - industryBuilds: "बिल्ड्स", - industryBuildTitle: "शीर्षक", - industryContribute: "योगदान करें", - industryContributeButton: "योगदान करें", - industryContributions: "योगदान", - industryCreateBlueprintButton: "ब्लूप्रिंट बनाएं", - industryDistribute: "वितरण करें", - industryDistributeButton: "हिस्सों के अनुसार वितरण", - industryDistribution: "वितरण", - industryEcoAmount: "राशि (ECO)", - industryEditBlueprint: "ब्लूप्रिंट संपादित करें", - industryFacility: "फैक्टरी", - industryKindLabel: "प्रकार", - industryKind_labor: "श्रम", - industryKind_material: "सामग्री", - industryKind_eco: "निधि", - industryLaborHours: "श्रम (घंटे)", - industryHours: "घंटे", - industryMaterialItem: "सामग्री", - industryMaterials: "सामग्री", - industryEstMaterials: "कुल सामग्री", - industryEstLabor: "कुल श्रम", - industryEstTotal: "अनुमानित मूल्य", - industryBuildingPrice: "निर्माण मूल्य", - industryFinalPrice: "अंतिम मूल्य", - industryBlueprintDescPlaceholder: "विनिर्देश, आयाम, वज़न, दस्तावेज़…", - industryMaterialValue: "मूल्य (ECO)", - industryMember: "सदस्य", - industryNewBlueprint: "नया ब्लूप्रिंट", - industryNewBuild: "बिल्ड प्रस्तावित करें", - industryEditBuild: "उत्पादन संपादित करें", - industryNeedBlueprint: "पहले ब्लूप्रिंट बनाएँ: हर उत्पादन उसी को बनाता है।", - industryNoBlueprints: "अभी तक कोई ब्लूप्रिंट नहीं।", - industryNoBuilds: "अभी तक कोई बिल्ड नहीं।", - industryNote: "नोट", - industryOutput: "आउटपुट", - industryOutputKind: "उत्पाद का प्रकार", - industryOutputValue: "आउटपुट मूल्य (ECO, जैसे बिक्री से)", - industryPostJob: "नौकरी बनाएँ", - industryPot: "कोष", - industryProposeBuildButton: "बिल्ड प्रस्तावित करें", - industryProposer: "प्रस्तावक", - industryAuthor: "लेखक", - industrySellOnMarket: "बाज़ार में भेजें", - industrySendToProjects: "प्रोजेक्ट बनाएँ", - industrySkills: "कौशल", - industryTreasury: "कोष", - industryKind_physical: "भौतिक", - industryKind_digital: "डिजिटल", - industryBuildStatus_PROPOSED: "प्रस्तावित", - industryBuildStatus_REJECTED: "अस्वीकृत", - industryBuildStatus_APPROVED: "स्वीकृत", - industryBuildStatus_STOCKING: "भंडारण", - industryBuildStatus_IN_PRODUCTION: "उत्पादन में", - industryBuildStatus_COMPLETED: "पूर्ण", - industryBuildStatus_FAILED: "विफल", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "एक नौकरी आपके CV से मेल खाती है", - jobsBotMatchIntro: "एक नौकरी आपके CV से मेल खाती है।", - jobsBotMatchJob: "नौकरी", - jobsBotMatchHint: "यह इसलिए मिला क्योंकि आपका CV AI द्वारा प्रबंधित है। आप इसे CV में बंद कर सकते हैं।", - cvAiManaged: "AI प्रबंधित", - switchOn: "चालू", - switchOff: "बंद", - cvAiManagedHint: "चालू होने पर, JobsBot निजी संदेश से बताता है जब कोई नौकरी आपके CV से मेल खाती है।", - cvMatchThreshold: "इस मेल स्तर से सूचित करें", - housingBotRequestedTitle: "किसी ने आपकी एक जगह का अनुरोध किया है।", - housingBotCancelledTitle: "आपकी एक जगह का अनुरोध रद्द कर दिया गया है।", - housingBotYouRequestedTitle: "आपने एक जगह का अनुरोध किया।", - housingBotYouCancelledTitle: "आपने एक अनुरोध रद्द किया।", - housingBotUnavailableTitle: "आपके द्वारा अनुरोधित जगह अब उपलब्ध नहीं है।", - housingBotUnavailableSentTitle: "आपने अनुरोध करने वालों को सूचित कर दिया।", - industryBotAdmittedTitle: "आपको एक फैक्टरी में स्वीकार किया गया है।", - industryBotApplicationTitle: "आपकी फैक्टरी में नया सदस्यता अनुरोध।", - industryBotInvitedTitle: "आपको एक फैक्टरी में आमंत्रित किया गया है।", - industryBotDissolvedTitle: "आपकी एक फैक्टरी विघटित कर दी गई है।", - industryBotBuildApprovedTitle: "एक बिल्ड स्वीकृत हो गया है।", - industryBotDistributedTitle: "एक बिल्ड का आउटपुट वितरित किया गया है।", - industryFilterRules: "नियम", - industryRulesTitle: "नियम", - industryRulesIntro: "Industry निवासियों को उत्पादन के साधनों का सामूहिक स्वामित्व देता है: एक फैक्टरी नेटवर्क-स्वामित्व वाली कार्यशाला है जिसे कोई निजी रूप से नहीं रखता।", - industryRulesSteward: "संस्थापक प्रबंधक होता है — संरक्षक, मालिक नहीं: वह फैक्टरी को अकेले बेच, निजीकृत या विघटित नहीं कर सकता।", - industryRulesMembership: "सदस्यता नीति तय करती है कि कैसे जुड़ें: खुला (तुरंत), मतदान द्वारा (सदस्यों के वोट से स्वीकृत) या केवल आमंत्रण (पहले किसी सदस्य को आमंत्रित करना होगा)।", - industryRulesQuorum: "कोरम वह न्यूनतम सदस्य संख्या है जिन्हें किसी निर्णय के गिनने के लिए मतदान करना चाहिए, ताकि छोटी फैक्टरी एक व्यक्ति द्वारा तय न हो।", - industryRulesMajority: "बहुमत पारित होने के लिए आवश्यक मतों का अंश है (0.5 = आधा, 1 = सर्वसम्मति); निर्णय तब पारित होता है जब हाँ वोट कम से कम max(कोरम, सदस्य × बहुमत) तक पहुँचें।", - industryRulesDecisions: "सदस्य नए लोगों को स्वीकारने, बिल्ड स्वीकृत करने और फैक्टरी विघटित करने के लिए मतदान करते हैं; प्रबंधक इन सामूहिक निर्णयों को नहीं टाल सकता।", - industryRulesBlueprints: "ब्लूप्रिंट स्वतंत्र (COPYLEFT) व्यंजन हैं — इनपुट (सामग्री, श्रम घंटे, कौशल) और आउटपुट (एक भौतिक या डिजिटल वस्तु) — और कोई भी इन्हें फोर्क कर सकता है।", - industryRulesBuilds: "बिल्ड एक उत्पादन आदेश है: प्रस्तावित → स्वीकृत (मतदान से) → भंडारण → उत्पादन में → पूर्ण, और स्वीकृत होने के बाद ही योगदान स्वीकार करता है।", - industryRulesContributions: "सदस्य किसी बिल्ड में श्रम (घंटे), सामग्री (घोषित ECO मूल्य) या ECO का योगदान करते हैं; हर योगदान Karma अर्जित करता है।", - industryRulesLaborRate: "श्रम दर इस फैक्टरी में एक कार्य-घंटे का ECO मूल्य है; यह घंटों को Karma में बदलती है ताकि श्रम और पूँजी की निष्पक्ष तुलना हो: Karma = घंटे × दर + सामग्री मूल्य + ECO।", - industryRulesShares: "किसी बिल्ड में आपका हिस्सा आपका Karma ÷ कुल Karma है, और यह तय करता है कि आपको आउटपुट मूल्य का कितना भाग मिलेगा।", - industryRulesDistribution: "उत्पादन पूरा होने पर स्टीवर्ड पॉट (उत्पादन की निधि और बिक्री मूल्य) को योगदानकर्ताओं में उनके हिस्से के अनुपात में बाँटता है।", - industryRulesTreasury: "ECO योगदान वितरण तक प्रबंधक द्वारा बिल्ड कोष के संरक्षक के रूप में रखे जाते हैं; सभी लेन-देन नेटवर्क पर दर्ज होते हैं और विवाद Courts में जा सकते हैं।", - industryJobs: "नौकरियाँ", - industryReportButton: "रिपोर्ट", - industryCreateTask: "कार्य बनाएँ", - industryOpenDispute: "विवाद खोलें", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "आमंत्रण उत्पन्न करें", - industryPauseVotes: "स्थिति मत", - industryTitle: "उद्योग", - industryDescription: "उत्पादन के साधनों को सामूहिक रूप से प्रबंधित करने का मॉड्यूल।", - typeIndustryBuild: "बिल्ड", - typeIndustryBlueprint: "ब्लूप्रिंट", - typeIndustryAllocation: "वितरण", - typeIndustry: "उद्योग", - modulesIndustryLabel: "उद्योग", - modulesIndustryDescription: "उत्पादन के साधनों को सामूहिक रूप से प्रबंधित करने का मॉड्यूल।", - industryFilterAll: "सभी", - industryFilterMember: "सदस्य", - industryFilterBlueprints: "ब्लूप्रिंट", - industryFilterBuilds: "उत्पादन", - industryFilterMine: "मेरी", - industryFilterActive: "चालू", - industryFilterPaused: "रोकी गई", - industryFilterDissolved: "विघटित", - industryAllTitle: "उद्योग", - industryMemberTitle: "जिन फैक्टरियों में मैं हूँ", - industryMineTitle: "मेरी फैक्टरियाँ", - industryActiveTitle: "चालू", - industryPausedTitle: "रोकी गई फैक्टरियाँ", - industryDissolvedTitle: "विघटित फैक्टरियाँ", - industryNoFacilitiesFound: "कोई फैक्टरी नहीं मिली।", - industryCreateFacility: "फैक्टरी बनाएं", - industryMembers: "सदस्य", - industryMemberBadge: "सदस्य", - industryName: "नाम", - industryNamePlaceholder: "फैक्टरी का नाम", - industryDescriptionLabel: "विवरण", - industryDescriptionPlaceholder: "यह फैक्टरी क्या उत्पादित करती है?", - industrySector: "क्षेत्र", - industryMembershipPolicy: "सदस्यता नीति", - industryQuorum: "कोरम", - industryMajority: "बहुमत", - industryLaborRate: "श्रम दर", - industryCreateButton: "फैक्टरी बनाएं", - industryUpdateButton: "अपडेट", - industrySteward: "प्रबंधक", - industryStatusActive: "चालू", - industryStatusPaused: "रोका गया", - industryStatusDissolved: "विघटित", - industryStatusLabel: "स्थिति", - industryJoinButton: "शामिल हों", - industryApplyButton: "शामिल होने का अनुरोध", - industryLeaveButton: "छोड़ें", - industryInviteButton: "आमंत्रित करें", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "शामिल होने के लिए आपको आमंत्रण चाहिए।", - industryPauseButton: "रोकने के लिए मतदान", - industryResumeButton: "फिर से शुरू करने के लिए मतदान", - industryDeleteButton: "हटाएं", - industryPendingApplicants: "लंबित आवेदक", - industryVotesYes: "हाँ", - industryVotesNo: "नहीं", - industryAlreadyVoted: "मतदान किया", - industryAdmit: "स्वीकार करें", - industryReject: "अस्वीकार करें", - industryDissolveTitle: "फैक्टरी विघटित करें", - industryDissolveVote: "विघटन के लिए मतदान", - industryPolicy_open: "खुला", - industryPolicy_vote: "मतदान से", - industryPolicy_invite: "केवल आमंत्रण", projectsTitle: "परियोजनाएँ", projectsDescription: "अपने नेटवर्क में समुदाय-संचालित परियोजनाएँ बनाएँ, वित्तपोषित करें और अनुसरण करें।", - projectSearchPlaceholder: "परियोजनाएँ खोजें...", projectCreateProject: "परियोजना बनाएँ", projectCreateButton: "परियोजना बनाएँ", projectUpdateButton: "अपडेट", @@ -2815,8 +2946,14 @@ module.exports = { projectPledgeTitle: "इस परियोजना का समर्थन करें", projectPledgePlaceholder: "ECO में राशि", projectBounties: "इनाम", + projectBountiesInputLabel: "इनाम (प्रति पंक्ति एक: शीर्षक|राशि [ECO]|विवरण)", + projectBountiesPlaceholder: "UI बग ठीक करें|100|समस्या का लिंक\nडॉक्स लिखें|250|उपयोग उदाहरण रूपरेखा", projectNoBounties: "कोई इनाम नहीं मिला।", projectTitle: "शीर्षक", + projectAddBountyTitle: "नया इनाम", + projectBountyTitle: "इनाम शीर्षक", + projectBountyAmount: "राशि (ECO)", + projectBountyDescription: "विवरण", projectMilestoneSelect: "मील का पत्थर चुनें", projectBountyCreateButton: "इनाम बनाएँ", projectBountyStatus: "इनाम स्थिति", @@ -2831,15 +2968,19 @@ module.exports = { projectMilestoneTitle: "मील का पत्थर शीर्षक", projectMilestoneTargetPercent: "प्रतिशत (%)", projectMilestoneDueDate: "दिनांक", + projectMilestoneCreateButton: "मील का पत्थर बनाएँ", projectMilestoneStatus: "मील का पत्थर स्थिति", projectMilestoneOpen: "खुला", projectMilestoneDone: "पूर्ण", projectMilestoneDue: "देय", + projectNoMilestones: "कोई मील का पत्थर नहीं मिला।", projectMilestoneMarkDone: "पूर्ण चिह्नित करें", projectMilestoneTitlePlaceholder: "मील का पत्थर शीर्षक दर्ज करें", projectMilestoneDescriptionPlaceholder: "इस मील के पत्थर का विवरण दर्ज करें", projectMilestoneDescription: "मील का पत्थर विवरण", + projectBudgetGoal: "बजट (लक्ष्य)", projectBudgetAssigned: "इनामों को सौंपा गया", + projectBudgetRemaining: "शेष", projectBudgetOver: "बजट से अधिक: सौंपा गया लक्ष्य से अधिक है", projectFollowers: "अनुसरणकर्ता", projectFollowersTitle: "अनुसरणकर्ता", @@ -2852,6 +2993,7 @@ module.exports = { projectBackersTotalPledged: "कुल प्रतिज्ञा", projectBackersYourPledge: "आपकी प्रतिज्ञा", projectBackersNone: "अभी तक कोई प्रतिज्ञा नहीं।", + projectNoRemainingBudget: "कोई शेष बजट नहीं।", projectFilterBackers: "समर्थक", projectFilterApplied: "आवेदित", projectAppliedTitle: "आवेदित", @@ -2860,15 +3002,30 @@ module.exports = { projectBackerAmount: "कुल योगदान", projectBackerPledges: "प्रतिज्ञाएँ", projectBackerProjects: "परियोजनाएँ", + projectPledgeAmount: "राशि", projectSelectMilestoneOrBounty: "मील का पत्थर या इनाम चुनें", projectPledgeButton: "प्रतिज्ञा करें", footerLicense: "GPLv3", + footerPackage: "पैकेज", + footerVersion: "संस्करण", modulesModuleName: "नाम", modulesModuleDescription: "विवरण", modulesModuleStatus: "स्थिति", modulesTotalModulesLabel: "लोड किए गए मॉड्यूल", modulesEnabledModulesLabel: "सक्षम", modulesDisabledModulesLabel: "अक्षम", + modulesPopularLabel: "लोकप्रिय", + modulesPopularDescription: "ट्रेंडिंग, सबसे अधिक देखी गई या सबसे अधिक टिप्पणी की गई पोस्ट प्राप्त करने का मॉड्यूल।", + modulesTopicsLabel: "विषय", + modulesTopicsDescription: "साझा रुचियों पर आधारित चर्चा श्रेणियाँ प्राप्त करने का मॉड्यूल।", + modulesSummariesLabel: "सारांश", + modulesSummariesDescription: "लंबी चर्चाओं या पोस्ट के सारांश प्राप्त करने का मॉड्यूल।", + modulesLatestLabel: "नवीनतम", + modulesLatestDescription: "सबसे हाल की पोस्ट और चर्चाएँ प्राप्त करने का मॉड्यूल।", + modulesThreadsLabel: "थ्रेड्स", + modulesThreadsDescription: "विषय या प्रश्न के अनुसार समूहीकृत बातचीत प्राप्त करने का मॉड्यूल।", + modulesMultiverseLabel: "मल्टीवर्स", + modulesMultiverseDescription: "अन्य संघीय पीयर से सामग्री प्राप्त करने का मॉड्यूल।", modulesInvitesLabel: "आमंत्रण", modulesInvitesDescription: "आमंत्रण कोड प्रबंधित और लागू करने का मॉड्यूल।", modulesWalletLabel: "वॉलेट", @@ -2909,12 +3066,8 @@ module.exports = { modulesOpinionsDescription: "राय खोजने और मतदान करने का मॉड्यूल।", modulesTransfersLabel: "स्थानांतरण", modulesTransfersDescription: "स्मार्ट-कॉन्ट्रैक्ट (स्थानांतरण) खोजने और प्रबंधित करने का मॉड्यूल।", - modulesFediverseLabel: "मल्टीवर्स", - modulesBlogsLabel: "ब्लॉग", - modulesBlogsDescription: "ब्लॉग खोजने और प्रबंधित करने का मॉड्यूल।", - modulesPollsLabel: "सर्वेक्षण", - modulesPollsDescription: "नेटवर्क से पूछने और उत्तर गिनने का मॉड्यूल।", - modulesFediverseDescription: "अपने अन्य मल्टीवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।", + modulesFediverseLabel: "फ़ेडिवर्स", + modulesFediverseDescription: "अपने अन्य फ़ेडिवर्स खातों को प्रबंधित करें, जिसमें सामग्री भेजना और प्राप्त करना शामिल है।", modulesFeedLabel: "फ़ीड", modulesFeedDescription: "लघु-पाठ (फ़ीड) खोजने और साझा करने का मॉड्यूल।", modulesParliamentLabel: "संसद", @@ -2950,33 +3103,37 @@ module.exports = { visibilityMakeHidden: "छिपाएँ", invitesInhabitantsTitle: "निवासी", invitesInhabitantsFollow: "फॉलो करें", - aiNavPlaceholder: "बताएँ आपको क्या चाहिए...", + aiNavPlaceholder: "आप कहाँ जाना चाहते हैं?", aiNavDisabled: "AI नेविगेशन अक्षम है।", - aiNavResultsTitle: "AINav सुझाव", + aiNavResultsTitle: "AI नेविगेशन सुझाव", aiNavQueryLabel: "प्रश्न", + aiNavResultsDescription: "अपने अनुरूप मार्ग चुनें।", + aiNavResultPath: "मार्ग", + aiNavResultDescription: "क्या किया जा सकता है", aiNavResultMatch: "मेल", aiNavResultsEmpty: "कोई मेल खाता मार्ग नहीं। /search आज़माएँ।", + aiNavSearchInstead: "इसके बजाय खोजें", modulesAINavLabel: "AINav", modulesAINavDescription: "नेटवर्क की सामग्री पर प्राकृतिक भाषा में प्रश्न करने का मॉड्यूल।", + directConnect: "सीधा कनेक्शन", directConnectDescription: "किसी पीयर से सीधे कनेक्ट करने के लिए उनका IP पता, पोर्ट और सार्वजनिक कुंजी दर्ज करें। पीयर को एक अनुसरित कनेक्शन के रूप में जोड़ा जाएगा।", peerHost: "IP", peerPort: "पोर्ट (डिफ़ॉल्ट: 8008)", peerPublicKey: "सार्वजनिक कुंजी (@...ed25519)", connectAndFollow: "कनेक्ट करें", + deviceSourceLabel: "डिवाइस स्रोत", modulesPresetTitle: "सामान्य कॉन्फ़िगरेशन", - workflowsTitle: "वर्कफ़्लो", - workflowsDescription: "वर्कफ़्लो थीम और सक्रिय मॉड्यूल तय करता है।", - workflowsSet: "वर्कफ़्लो लागू करें", - workflow_default: "डिफ़ॉल्ट", - workflow_jobs: "रोज़गार", - workflow_ruling: "शासन", - workflow_politics: "राजनीति", - workflow_social: "सामाजिक", - workflow_business: "व्यापार", - workflow_night: "रात", - workflow_mobile: "मोबाइल", + modulesPreset_minimal: "न्यूनतम", + modulesPreset_basic: "बुनियादी", + modulesPreset_social: "सामाजिक", + modulesPreset_economy: "अर्थव्यवस्था", + modulesPreset_full: "पूर्ण", statsCarbonFootprintTitle: "कार्बन फुटप्रिंट", + statsCarbonFootprintNetwork: "नेटवर्क कार्बन फुटप्रिंट", + statsCarbonFootprintYours: "आपका कार्बन फुटप्रिंट", statsCarbonTombstone: "टॉम्बस्टोनिंग फुटप्रिंट", + feedSuccessMsg: "फ़ीड सफलतापूर्वक प्रकाशित!", + dominantOpinionLabel: "प्रमुख राय", uploadMedia: "मीडिया अपलोड करें (अधिकतम-आकार: 50MB)", feedOpenDiscussion: "चर्चा खोलें", feedDetailTitle: "फ़ीड", @@ -3009,13 +3166,14 @@ module.exports = { mapDescriptionLabel: "विवरण", mapDescriptionPlaceholder: "मानचित्र या स्थान का वर्णन करें...", mapTypeLabel: "मानचित्र प्रकार", - mapInviteMode: "आमंत्रण", - mapTypeSingle: "एकल", - mapTypeOpen: "खुला", - mapTypeClosed: "बंद", + mapTypeSingle: "एकल (केवल प्रारंभिक स्थान)", + mapTypeOpen: "खुला (कोई भी मार्कर जोड़ सकता है)", + mapTypeClosed: "बंद (केवल निर्माता मार्कर जोड़ता है)", mapTagsLabel: "टैग", mapTagsPlaceholder: "अल्पविराम से अलग टैग दर्ज करें", mapUrlLabel: "मानचित्र URL", + mapPickCoordLabel: "या ग्रिड पर स्थान चुनें:", + mapMarkersLabel: "मार्कर", mapMarkersTitle: "मार्कर", mapMarkerDefault: "मार्कर", mapMarkerLatLabel: "मार्कर अक्षांश", @@ -3042,9 +3200,14 @@ module.exports = { padTitle: "पैड", modulesPadsLabel: "पैड्स", modulesPadsDescription: "सहयोगी पाठ संपादकों को प्रबंधित करने का मॉड्यूल।", + padFilterAll: "सभी", + padFilterMine: "मेरे", + padFilterRecent: "हालिया", + padFilterOpen: "खुला", + padFilterClosed: "बंद", padCreate: "पैड बनाएं", - padUpdate: "अपडेट करें", - padDelete: "हटाएं", + padUpdate: "पैड अपडेट करें", + padDelete: "पैड हटाएं", padTitleLabel: "शीर्षक", padTitlePlaceholder: "पैड का शीर्षक दर्ज करें...", padStatusLabel: "स्थिति", @@ -3055,7 +3218,14 @@ module.exports = { padTagsLabel: "टैग", padTagsPlaceholder: "टैग1, टैग2, ...", padMembersLabel: "सदस्य", + padVisitPad: "पैड देखें", + padShareUrl: "URL साझा करें", padCreated: "बनाया गया", + padAuthor: "लेखक", + padGenerateCode: "कोड बनाएं", + padInviteCodeLabel: "आमंत्रण कोड", + padInviteCodePlaceholder: "आमंत्रण कोड दर्ज करें...", + padValidateInvite: "सत्यापित करें", padStartEditing: "संपादन शुरू करें!", padEditorPlaceholder: "लिखना शुरू करें...", padSubmitEntry: "सबमिट करें", @@ -3068,11 +3238,12 @@ module.exports = { padClosedSectionTitle: "बंद पैड्स", padCreateSectionTitle: "नया पैड बनाएं", padUpdateSectionTitle: "पैड अपडेट करें", + padInviteGenerated: "आमंत्रण कोड बनाया गया", typePad: "पैड्स", padNew: "नया", padAddFavorite: "पसंदीदा में जोड़ें", padRemoveFavorite: "पसंदीदा से हटाएं", - padClose: "बंद करें", + padClose: "पैड बंद करें", padBackToEditor: "संपादक पर वापस", padSearchPlaceholder: "पैड खोजें...", @@ -3080,6 +3251,8 @@ module.exports = { modulesChatsDescription: "एन्क्रिप्टेड चैट खोजने और प्रबंधित करने का मॉड्यूल।", typeChat: "चैट्स", typeChatMessage: "चैट संदेश", + chatLabel: "चैट्स", + chatMessageLabel: "चैट संदेश", chatsTitle: "चैट्स", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3089,50 +3262,52 @@ module.exports = { chatDescription: "विवरण", chatCategory: "श्रेणी", chatStatus: "स्थिति", + chatFilterAll: "सभी", + chatFilterMine: "मेरे", + chatFilterRecent: "हाल के", + chatFilterFavorites: "पसंदीदा", + chatFilterOpen: "खुले", + chatFilterClosed: "बंद", chatCreate: "चैट बनाएं", - chatUpdate: "अपडेट", - chatDelete: "हटाएं", + chatUpdate: "चैट अपडेट करें", + chatDelete: "चैट हटाएं", chatClose: "चैट बंद करें", chatVisitChat: "चैट देखें", chatUntitled: "बिना शीर्षक चैट", chatNoItems: "कोई चैट नहीं मिली।", chatParticipants: "प्रतिभागी", - chatMessagesLabel: "संदेश", - chatThreadsLabel: "थ्रेड", + chatStartChatting: "चैट शुरू करें!", + chatGenerateCode: "कोड बनाएं", + chatShareUrl: "URL शेयर करें", chatCreatedAt: "बनाया गया", chatSearchPlaceholder: "चैट खोजें...", chatStatusOpen: "खुला", chatStatusInviteOnly: "केवल आमंत्रण", chatStatusClosed: "बंद", chatSendMessage: "भेजें", - chatJumpLatest: "नवीनतम संदेश", - chatPickChat: "खोलने के लिए एक चैट चुनें", - chatNoneYet: "अभी तक कोई चैट उपलब्ध नहीं है।", - activitySearchPlaceholder: "गतिविधि में खोजें...", - trendingSearchPlaceholder: "ट्रेंडिंग में खोजें...", - opinionsSearchPlaceholder: "राय में खोजें...", - votesSearchPlaceholder: "मतदान में खोजें...", - welcomeStepUxTitle: "अपना इंटरफ़ेस चुनें", - welcomeStepUxText: "तय करें Oasis कैसा दिखे। बाद में सेटिंग्स में बदल सकते हैं।", - welcomeStepUxAction: "यह दृश्य उपयोग करें", - chatRateLimitTitle: "बहुत अधिक संदेश", - chatRateLimitMessage: "आप इस कक्ष की प्रति घंटा संदेश सीमा तक पहुँच गए हैं। बाद में पुनः प्रयास करें।", chatMessagePlaceholder: "संदेश लिखें...", chatNoMessages: "अभी तक कोई संदेश नहीं।", + chatLeave: "चैट छोड़ें", + chatInviteCodeLabel: "आमंत्रण कोड दर्ज करें", + chatJoinByInvite: "शामिल हों", chatTitlePlaceholder: "चैट का शीर्षक", chatDescriptionPlaceholder: "चैट का विवरण", chatTagsPlaceholder: "टैग1, टैग2", chatImageLabel: "छवि फ़ाइल चुनें (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "आमंत्रण कोड", chatAuthor: "लेखक", + chatCreated: "बनाया", chatAddFavorite: "पसंदीदा में जोड़ें", chatRemoveFavorite: "पसंदीदा से हटाएं", chatPM: "PM", chatStatusLabel: "स्थिति", chatCategoryLabel: "श्रेणी", + chatParticipantsLabel: "प्रतिभागी", gamesTitle: "खेल", gamesDescription: "कुछ खेल खोजें और खेलें।", gamesFilterAll: "सभी", gamesPlayButton: "खेलें!", + gamesBackToGames: "खेलों पर वापस जाएं", modulesGamesLabel: "खेल", modulesGamesDescription: "कुछ खेल खोजने और खेलने का मॉड्यूल।", modulesGraphosLabel: "ग्राफोस", @@ -3149,6 +3324,8 @@ module.exports = { gamesArkanoidDesc: "अपने पैडल और गेंद से सभी ईंटें तोड़ें। एक क्लासिक आर्केड चुनौती।", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "AI के खिलाफ क्लासिक पिंग-पोंग। 5 पॉइंट पहले पाने वाला जीतता है।", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "समय के खिलाफ दौड़! यातायात से बचें और समय से पहले मंजिल पर पहुंचें।", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "क्षुद्रग्रह क्षेत्र में अपना यान उड़ाएं। उन्हें आने से पहले नष्ट करें।", gamesRockPaperScissorsTitle: "पत्थर कागज कैंची", @@ -3169,6 +3346,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3179,6 +3358,12 @@ module.exports = { modulesCalendarsLabel: "कैलेंडर", modulesCalendarsDescription: "कैलेंडर खोजने और प्रबंधित करने का मॉड्यूल।", typeCalendar: "कैलेंडर", + calendarFilterAll: "सभी", + calendarFilterMine: "मेरे", + calendarFilterOpen: "खुला", + calendarFilterClosed: "बंद", + calendarFilterRecent: "हाल के", + calendarFilterFavorites: "पसंदीदा", calendarCreate: "कैलेंडर बनाएं", calendarUpdate: "अपडेट करें", calendarDelete: "हटाएं", @@ -3191,17 +3376,24 @@ module.exports = { calendarTagsLabel: "टैग", calendarTagsPlaceholder: "टैग1, टैग2...", calendarParticipantsLabel: "प्रतिभागी", + calendarParticipantsCount: "प्रतिभागी", + calendarVisitCalendar: "कैलेंडर देखें", calendarCreated: "बनाया गया", + calendarAuthor: "लेखक", calendarJoin: "जुड़ें", calendarGenerateInvite: "आमंत्रण बनाएं", + calendarInviteCodePlaceholder: "आमंत्रण कोड दर्ज करें...", + calendarValidateInvite: "कोड सत्यापित करें", + calendarJoined: "जुड़े हुए", + calendarAddDate: "तारीख जोड़ें", + calendarAddNote: "नोट जोड़ें", calendarDateLabel: "तारीख", calendarDatePlaceholder: "इस तारीख का वर्णन करें...", - calendarNoteLabel: "नोट्स", + calendarNoteLabel: "नोट", calendarNotePlaceholder: "नोट जोड़ें...", calendarFirstDateLabel: "तारीख", calendarFirstNoteLabel: "नोट्स", calendarIntervalLabel: "Interval", - calendarIntervalNone: "कोई पुनरावृत्ति नहीं", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3212,6 +3404,8 @@ module.exports = { calendarsDescription: "अपने नेटवर्क में कैलेंडर खोजें और प्रबंधित करें।", calendarMonthPrev: "← पिछला", calendarMonthNext: "अगला →", + calendarMonthLabel: "तारीखें", + calendarsShareUrl: "शेयर URL", calendarAllSectionTitle: "कैलेंडर", calendarRecentSectionTitle: "हाल के कैलेंडर", calendarFavoritesSectionTitle: "पसंदीदा", @@ -3225,6 +3419,7 @@ module.exports = { calendarRemoveFavorite: "पसंदीदा से हटाएं", calendarSearchPlaceholder: "कैलेंडर खोजें...", calendarAddEntry: "प्रविष्टि जोड़ें", + calendarLeave: "कैलेंडर छोड़ें", statsCalendar: "कैलेंडर", statsCalendarDate: "कैलेंडर तारीखें", statsCalendarNote: "कैलेंडर नोट", @@ -3271,6 +3466,7 @@ module.exports = { torrentSortOldest: "सबसे पुराने", torrentSortTop: "सबसे अधिक वोट", torrentNoMatch: "कोई मिलान टॉरेंट नहीं।", + torrentMessageAuthorButton: "लेखक को संदेश भेजें", torrentUpdatedAt: "अपडेट किया गया", statsTorrent: "टॉरेंट", typeTorrent: "टॉरेंट", @@ -3278,18 +3474,10 @@ module.exports = { modulesTorrentsDescription: "टॉरेंट खोजने और प्रबंधित करने का मॉड्यूल।", favoritesFilterTorrents: "टॉरेंट", favoritesFilterMarket: "मार्केट", - favoritesFilterEvents: "इवेंट्स", - favoritesFilterForum: "फ़ोरम", - favoritesFilterHousing: "आवास", - favoritesFilterJobs: "नौकरियाँ", - favoritesFilterProjects: "परियोजनाएँ", - favoritesFilterReports: "रिपोर्ट्स", - favoritesFilterTasks: "कार्य", - favoritesFilterTransfers: "स्थानांतरण", - favoritesFilterVotes: "मतदान", favoritesFilterShopProducts: "दुकान वस्तुएं", tribeSectionTorrents: "टॉरेंट", tribeCreateTorrent: "टॉरेंट अपलोड करें", + tribeMediaTypeTorrent: "टॉरेंट", settingsWishTitle: "इच्छा", settingsWishDesc: "अपने अवतार की इच्छा कॉन्फ़िगर करें।", settingsWishWhole: "Multiverse", @@ -3300,10 +3488,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "आउटबाउंड UX गार्डरेल है। इनबाउंड ध्यान फ़िल्टर है।", + pmBlockedNonMutual: "केवल पारस्परिक समर्थन वाले निवासियों को PM भेज सकते हैं।", inhabitantsPendingFollowsTitle: "लंबित समर्थन अनुरोध", inhabitantsPendingAccept: "स्वीकार", inhabitantsPendingReject: "अस्वीकार", + bxEncrypted: "एन्क्रिप्टेड", + bxEncryptedHexLabel: "सिफरटेक्स्ट (पूर्वावलोकन)", tribeSectionGovernance: "शासन", + tribeSubStatusPublic: "सार्वजनिक", + tribeSubStatusPrivate: "निजी", + tribeSubInheritedPrivate: "निजी (मुख्य जनजाति से विरासत)", tribePadInviteRequired: "पैड तक पहुंच नहीं। निमंत्रण मांगें।", tribeChatInviteRequired: "चैट तक पहुंच नहीं। निमंत्रण मांगें।", tribeGovernanceDesc: "इस जनजाति का आंतरिक शासन।", @@ -3343,31 +3537,44 @@ module.exports = { tribeGovNoHistorical: "अभी तक कोई रिकॉर्ड नहीं", logsTitle: "लॉग", logsDescription: "नेटवर्क पर अपना अनुभव रिकॉर्ड करें।", + logsReadMore: "और पढ़ें", logsViewTitle: "लॉग", logsColumnType: "प्रकार", logsView: "देखें", + logsManualPrompt: "अपनी लॉग लिखें", logsUpdateButton: "अपडेट करें", - logsEditTitle: "लॉग अपडेट करें", + logsEditTitle: "प्रविष्टि संपादित करें", + logsCreateDescription: "अपना अनुभव रिकॉर्ड करें...", logsCreateTitle: "प्रविष्टि बनाएँ", logsWriteButton: "लिखें", logsTextPlaceholder: "अपने अनुभव वर्णित करें...", + logsTextField: "पाठ", + logsLabelPlaceholder: "लेबल...", + logsLabelField: "अपनी लॉग लिखें (लेबल)", + logsAiDisabledWarn: "एआई लिखित लॉग के लिए /modules में एआई मॉड्यूल सक्षम करें।", + logsAiContextValue: "ब्लॉकचेन दिन", + logsAiContext: "संदर्भ", + logsAiModStatus: "AImod", + logsModeApply: "लागू करें!", logsModeAIWritten: "AI-Assistant", logsModeManual: "मैन्युअल", logsModeAI: "एआई", + logsColumnDelete: "हटाएँ", + logsColumnEdit: "संपादित करें", logsColumnLog: "लॉग", logsColumnDate: "दिनांक", logsDelete: "हटाएँ", - logsEdit: "अपडेट करें", + logsEdit: "संपादित करें", logsFilterAlways: "हमेशा", logsFilterYear: "पिछला वर्ष", logsFilterMonth: "पिछला महीना", logsFilterWeek: "पिछला सप्ताह", logsFilterToday: "आज", - logsComments: "टिप्पणियाँ", - logsAuthorEntries: "प्रविष्टियाँ", + logsFilterRecent: "हाल के", + logsFilterAll: "सभी", logsCreate: "प्रविष्टि बनाएँ", logsExport: "लॉग निर्यात करें", - logsExportOne: "लॉग निर्यात करें", + logsExportOne: "निर्यात", logsViewDetails: "विवरण देखें", logsGenerateButton: "पाठ जनरेट करें", logsSearchText: "लॉग में खोजें...", @@ -3377,25 +3584,31 @@ module.exports = { modulesLogsLabel: "लॉग", modulesLogsDescription: "(एआई सहायक के माध्यम से) अपने अनुभव रिकॉर्ड करने के लिए मॉड्यूल।", statsLogsTitle: "लॉग", + statsLogsEntries: "प्रविष्टियाँ", typeLog: "लॉग", + blockchainCycle: "चक्र", larpTitle: "L.A.R.P.", larpDescription: "सहयोगात्मक प्रयोग के लिए एक लाइव-एक्शन रोल-प्लेइंग परत।", - larpNoHousesMatch: "आपकी खोज से कोई हाउस मेल नहीं खाता।", - larpSearchPlaceholder: "हाउस खोजें...", larpAllHouses: "गृह:", larpFilterRuling: "शासन", larpFilterHouses: "गृह", larpFilterRules: "FAQ", larpRulesTitle: "L.A.R.P. FAQ", + larpRulesEntryTitle: "प्रारंभिक गृह", larpRulesEntryText: "हर निवासी डिफ़ॉल्ट रूप से ACADEMIA में शुरू होता है। ACADEMIA से, \"गृह में शामिल हों\" पैनल में कोई गृह चुनें: इससे उसका प्रवेश परीक्षण खुलता है। 10 प्रश्नों के उत्तर दें और सबमिट करें। यदि आप सीमा (7/10) पार करते हैं, तो आपको स्वचालित रूप से उस गृह में स्वीकार कर लिया जाता है; नहीं तो अस्वीकार कर ACADEMIA में रखा जाता है। दोनों ही मामलों में सिस्टम आपके OasisID और प्रयास के चक्र को दर्ज करता है और 30 चक्रों के कूलडाउन समाप्त होने तक पुनः प्रयास से रोकता है।", + larpRulesLeaveText: "किसी भी गृह के सदस्य अपने गृह पृष्ठ से कभी भी ACADEMIA में लौट सकते हैं; इससे उनकी सदस्यता रीसेट हो जाती है, परंतु परीक्षण कूलडाउन समाप्त नहीं होता।", + larpRulesGovernanceTitle: "शासन दीवार", larpRulesGovernanceText: "अपने चक्र के दौरान, शासक गृह \"शासन\" बैज पहनता है और एकमात्र है जिसकी दीवार शासन टैब के तहत सार्वजनिक रूप से दिखाई जाती है।", - larpRulesWallText: "हर घर की एक दीवार होती है जहाँ उसके सदस्य लिखते हैं। किसी घर की दीवार निजी होती है, सिवाय शासक घर और ACADEMIA की, जिन्हें कोई भी पढ़ सकता है।", + larpRulesSignTitle: "L.A.R.P. प्रतीक", larpRulesSignText: "निवासी इसे चालू कर सकते हैं (/profile/edit > सेंसर) ताकि उनके वर्तमान गृह की छवि उनके प्रोफ़ाइल, लेखक और निवासी कार्ड पर मुहर के रूप में दिखे। मुहर गृह पृष्ठ से जुड़ी होती है।", larpPostsTitle: "दीवार", larpPostsEmpty: "अभी कोई पोस्ट नहीं।", + larpPostLabel: "नई पोस्ट", larpPostPlaceholder: "यह गृह क्या कहना चाहता है?", + larpPostPublic: "सार्वजनिक (किसी को भी दिखे, अन्यथा केवल सदस्यों को)", larpPostSubmit: "प्रकाशित करें", + larpPostMembersOnly: "केवल सदस्य", larpCycleLabel: "चक्र:", audioBackToBcs: "Back to BCS", audioFilterBcs: "BCS", @@ -3512,9 +3725,12 @@ module.exports = { bankTaxesLookupNote: "अपने स्थानीय फ़ीड में देखने के लिए ब्लॉक ID दर्ज करें।", bankTaxesUserNoteChipsClick: "ब्लॉकexplorer में इसके ब्लॉक का निरीक्षण करने के लिए किसी भी चिप पर क्लिक करें।", mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntry: "आमंत्रण कोड", larpRulesInviteEntryText: "गृह सदस्य आमंत्रण कोड बना सकते हैं जो गृह तक सीधी पहुंच देते हैं।", larpRulesOneHouseText: "आप किसी भी समय अधिकतम एक गृह से संबंध रख सकते हैं। यदि आप किसी से संबंधित नहीं हैं, तो आप ACADEMIA में हैं। हर गैर-ACADEMIA गृह पृष्ठ अपने सदस्यों को \"गृह छोड़ें\" बटन दिखाता है जो सदस्यता को ACADEMIA पर रीसेट कर देता है। छोड़ने से परीक्षण कूलडाउन समाप्त नहीं होता।", - larpRulesTestEntryText: "WILL परीक्षण में हर विकल्प एक या अधिक घरों को भार देता है; भेजने पर सिस्टम अंक जोड़ता है और सबसे अधिक अंक वाले घर में आपको स्वतः प्रवेश देता है।", + larpRulesOneHouseTitle: "एक समय में एक गृह", + larpRulesTestEntry: "WILL परीक्षण", + larpRulesTestEntryText: "प्रत्येक विकल्प एक या अधिक गृहों को वज़न देता है; जमा करने पर, सिस्टम अंकों को जोड़ता है और आपको सबसे अधिक अंक वाले गृह में स्वत: स्वीकार करता है।", larpWillTestHint: "पूरा होने पर, आपको वह गृह सौंपा जाएगा जो आपके उत्तरों के साथ सबसे अच्छा मेल खाता है। आपके व्यक्तिगत उत्तरों को स्थानीय रूप से संसाधित किया जाता है और कभी संग्रहीत नहीं किया जाता — केवल परिणामी गृह असाइनमेंट आपकी फ़ीड पर प्रकाशित होता है।", larpWillTestTitle: "WILL परीक्षण", settingsLanDesc: "इस पीयर को समय-समय पर उसी स्थानीय नेटवर्क में अन्य Oasis इंस्टेंस को घोषित करें। UDP प्रसारण रोकने के लिए अक्षम करें।", @@ -3533,27 +3749,31 @@ module.exports = { aiExchangeMarkHelpful: "+1 सहायक", larpCyclesUntilRuling: "अगला शासन चक्र", larpVisitTribe: "जनजाति देखें", - larpStartJourney: "यात्रा शुरू करें", larpGoverning: "शासन:", - larpStartHere: "यहाँ से शुरू करें:", larpMyHouse: "मेरा गृह:", larpMembersCount: "सदस्य", + larpMembersTitle: "सदस्य", + larpNoMembers: "अभी कोई सदस्य नहीं।", larpRolesLabel: "भूमिकाएँ", larpFunctionLabel: "कार्य", larpMonthLabel: "शासन चक्र", larpLeaveToAcademia: "ACADEMIA लौटें", - larpAcademiaJoinTitle: "L.A.R.P. गृह", + larpAcademiaJoinTitle: "गृह में शामिल हों", + larpAcademiaJoinHint: "मनोवैज्ञानिक प्रोफ़ाइल परीक्षण दें ताकि आपको आपके लिए सबसे उपयुक्त गृह सौंपा जाए, या किसी गृह सदस्य द्वारा साझा किया गया आमंत्रण कोड भुनाएँ।", + larpTakeTestButton: "प्रोफ़ाइल परीक्षण दें", larpInviteRedeem: "गृह में शामिल हों", larpInviteCreate: "आमंत्रण कोड बनाएँ", larpInvitePlaceholder: "आमंत्रण कोड", larpInviteBannerTitle: "नया आमंत्रण कोड", larpInviteBannerHint: "इस कोड को ACADEMIA में किसी के साथ साझा करें। यह 30 चक्रों या एक उपयोग के बाद समाप्त हो जाता है।", + larpTestAssignedHouse: "नियत गृह", larpTestRankingTitle: "गृह के अनुसार स्कोर", invitesHousesTitle: "L.A.R.P.", invitesHouseInviteCodePlaceholder: "गृह आमंत्रण कोड", invitesHouseJoinButton: "गृह में शामिल हों", larpLastAttemptTitle: "आपका पिछला प्रयास", larpLastAttemptHouse: "गृह", + larpLastAttemptResult: "परिणाम", larpLastAttemptWhen: "कब", larpLastAttemptCooldown: "अगला प्रयास", larpTestTitle: "प्रवेश परीक्षण", @@ -3562,14 +3782,37 @@ module.exports = { larpTestCooldownActive: "अगला परीक्षण उपलब्ध", larpTestCooldownDays: "चक्र", larpTestResultTitle: "परीक्षण परिणाम", + larpTestPassed: "परीक्षण पास — स्वागत है!", + larpTestFailed: "परीक्षण पास नहीं हुआ", larpTestScore: "स्कोर", larpTestNextAttempt: "आप 30 चक्रों के बाद एक और परीक्षण दे सकते हैं।", larpGoToHouse: "अपने गृह पर जाएँ", larpBackToAcademia: "ACADEMIA लौटें", + larpBackToHouses: "◀ गृह", larpBadgeYou: "आप", larpBadgeRuling: "शासन", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Oasis की लाइव-एक्शन रोल-प्लेइंग परत (9 गृह) के लिए मॉड्यूल।", + melodyUploadTitlePlaceholder: "Composition title", + melodyUploadDescPlaceholder: "Optional description", + ecoinTaxLabel: "ECOin Tax", + bankTaxesNetworkTitle: "Network Taxes", + bankTaxesUserTitle: "Your taxes", + bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", + bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", + bankTaxesExample: "Example", + bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", + statsEcoTaxTitle: "ECO Tax", + audioTranscodeTitle: "BCS Transcode", + audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", + audioTranscodeStegoTitle: "Embedded in the waveform", + audioBackToAudio: "Back to audio", + audioAuthorLabel: "Author", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + mapClickToCreate: "Click on the map to select a location", + mapClickToAddMarker: "Click on the map to add a marker", + gameScoreLabel: "GAME SCORES", larpProfileQ1: "जब आप किसी जटिल समस्या का सामना करते हैं, तो पहले क्या करते हैं?", larpProfileQ1O1: "सहमति खोजने के लिए दूसरों से बात करना", larpProfileQ1O2: "परीक्षण के लिए प्रोटोटाइप बनाना", diff --git a/src/client/assets/translations/oasis_it.js b/src/client/assets/translations/oasis_it.js index e30d008..a71a93f 100644 --- a/src/client/assets/translations/oasis_it.js +++ b/src/client/assets/translations/oasis_it.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { it: { + bbAdd: "Aggiungi", + bbQuickActions: "Azioni rapide", + bbPickTitle: "Fissa un modulo", + bbPickDesc: "Scegli un modulo per lo spazio + nella barra inferiore.", + bbPickNone: "Nessuno", + bbManage: "Modifica", + bbYourBar: "La tua barra", + bbRemove: "Rimuovi", + bbFull: "Massimo raggiunto", + bbDone: "Fatto", + activityChangeFilter: "Cambia filtro", + emptyConnectHint: "Connettiti a un pub per sincronizzarti con la rete.", + emptyConnectCta: "Connettiti a un pub", + menuCommunity: "Comunità", + activityGroupCommunity: "Comunità e governance", + activityGroupOrg: "Organizzazione", + activityGroupComm: "Comunicazione", + activityGroupEconomy: "Economia", + activityGroupMedia: "Contenuti e media", + activityGroupOther: "Altri", + languageName: "Italiano", shopOrderStatusPaid: "Pagamento ricevuto", shopOrderStatusReceived: "Ricevuto", shopOrderMarkPaid: "Segna pagamento ricevuto", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "Non hai ancora acquisti.", shopOrderPmSeller: "PM Seller", shopOrderSeller: "Venditore", + shopOrderPaymentConcept: "Pagamento", shopOrderInvoice: "Fattura", shopOrderInvoiceConcept: "Fattura", shopOrderStatusPending: "In attesa", shopOrderStatusAccepted: "Accettato", shopOrderStatusRejected: "Rifiutato", shopOrderStatusShipped: "Spedito", + shopOrderStatusDelivered: "Consegnato", shopOrderAccept: "Accetta", shopOrderReject: "Rifiuta", shopOrderMarkShipped: "Segna spedito", + shopOrderMarkDelivered: "Segna consegnato", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "Contratto", shopOrderContractConcept: "Ordine negozio", shopOrdersPending: "Ordini in sospeso", all: "Tutti", @@ -50,13 +75,14 @@ module.exports = { viewJson: "Vedi JSON", matchScore: "Punteggio di corrispondenza", preferencesLabel: "Preferenze", - inhabitantProfileTitle: "Abitante", + inhabitantProfileTitle: "Profilo abitante", contentWarningLabel: "Avviso sul contenuto", invalidPost: "Contenuto non valido", invalidPostHint: "Questo messaggio ha testo vuoto/non valido.", spreadBy: "Da", spreadChron: "Diffusione", spreaded: "Diffuso", + spreadMore: "altro", spreadContentUnavailable: "Contenuto non ancora disponibile (replica in sospeso)", filteredByTag: "Filtrato per tag", filteredByTagTitle: "Filtrato per tag", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "Nessuna gravità", calendarDeleteDate: "Elimina data", calendarIntervalUntil: "Fino a", + bookmarkCategory: "Categoria", bookmarkLastVisit: "Ultima visita", profileClearnetBookmarksLabel: "Segnalibri", + forumFilterHot: "Caldi", invitesPort: "Porta", currentlyUnrecheable: "ERRORE!", peerHostValidation: "IPv4 valido (es. 192.168.1.100) o hostname (es. pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "Stima max annuale", statsCarbonNetwork: "Totale rete", statsCarbonOfEstMax: "della capacità max stimata", - statsCarbonYearProgress: "dell'anno trascorso", - statsNetworkTitle: "Rete", - statsStorageTitle: "Archiviazione", - statsEcoTaxTitle: "Tassa ECO", - statsAccountTitle: "Account", - statsAveragesTitle: "Medie", statsCarbonOfNetwork: "del totale della rete", statsCarbonUser: "La tua impronta", statsContentCountColumn: "Conteggio", @@ -154,6 +176,10 @@ module.exports = { statsTopTagsTitle: "Tag principali", statsTopTypesTitle: "Tipi di contenuto principali", statsTotalMsgs: "Messaggi totali", + feedViewDetails: "Vedi dettagli", + eventFileLabel: "Allega immagine", + taskFileLabel: "Allega immagine", + courtsRespondentRequired: "L'ID del convenuto è obbligatorio", extended: "Multiverso", extendedDescription: [ "Quando supporti qualcuno, puoi scaricare i post degli abitanti che supportano, e questi appaiono qui, ordinati per data.", @@ -197,27 +223,36 @@ module.exports = { graphosShowing: "Mostrando", graphosYou: "Tu", graphosTotalNodes: "Nodi totali", + manualMode: "Modalità manuale", mentions: "Menzioni", mentionsDescription: [ - strong("Ogni messaggio che ti @menziona"), - ".", + strong("Post che ti @menzionano"), + ", ordinati per data.", ], + nextPage: "Avanti", + previousPage: "Indietro", noMentions: "Non hai ancora ricevuto @menzioni.", - mentionsSearchPlaceholder: "Cerca menzioni...", privateReminders: "PROMEMORIA", inboxFiltersLabel: "Filtri:", inboxToggleToMutuals: "Passa a reciproci", inboxToggleToWhole: "Passa a tutti", + privateFrom: "Da", + privateTo: "A", privateDate: "Data", pmReply: "Rispondi", pmReplies: "risposte", + pmNew: "nuove", + pmMarkRead: "Segna come letto", inReplyTo: "IN RISPOSTA A", pmPreview: "Anteprima", pmPreviewTitle: "Anteprima messaggio", peers: "Peer", searchDescription: "Descrizione", + imageSearch: "Ricerca immagini", searchFromLabel: "Da", searchToLabel: "A", + searchMinOpinionsLabel: "Min opinioni", + searchInhabitantLabel: "Abitante (Oasis ID)", searchQueryLabel: "Ricerca", searchPerPageLabel: "Risultati per pagina", settings: "Impostazioni", @@ -229,38 +264,67 @@ module.exports = { melodyTitle: 'Melody', melodyDescription: 'Riproduce la melodia della tua blockchain — ogni blocco diventa una nota.', melodyEmpty: 'Nessuna nota ancora — la tua blockchain non ha ancora prodotto blocchi.', + melodyCompositionHelp: 'Ogni blocco della tua blockchain diventa una nota musicale in base al tipo e alla dimensione.', + melodyStatsTitle: 'Suddivisione per tipo', + melodyInhabitantLabel: 'Abitante', melodyTotalBlocks: 'Note', + melodyType: 'Tipo', + melodyCount: 'Blocchi', melodyFilterAll: 'TUTTE', + melodyFilterShare: 'Carica melodia', + melodyShareTitle: 'Condividi la tua melodia', + melodyShareHelp: 'Pubblica un’istantanea della tua melodia attuale per farla ascoltare e remixare ad altri.', + melodyShareTitleLabel: 'Titolo (opzionale)', + melodyShareTitlePlaceholder: 'Un fantasma di blockchain', + melodyShareSubmit: 'Pubblica melodia', + melodyNoShared: 'Nessuna melodia di blockchain ancora.', melodyRegenerate: 'Rigenera', melodyDownload: "Scarica BCS", + profileActivityTitle: 'Attività', + profileActivityMore: 'Vedi tutta l’attività', profileContentSectionTitle: 'Contenuto dell’Avatar', profileContentHelp: 'Scegli quali moduli mostrare nel tuo avatar.', profileSensorsHelp: 'Metriche opzionali mostrate nel tuo avatar.', profileClearnetHelp: 'Moduli accessibili dall’esterno di Oasis.', profileHubAll: 'Tutto', + profileHubTitle: 'Contenuto Pubblico', + footerPulseTitle: 'Pulse', + footerPulsePeers: 'Peer', + footerPulseInbox: 'Inbox', + footerPulseSync: 'Ultima sinc.', + footerPulseEcoValue: 'ECO/h', + footerPulseLastActivity: 'Ultima attività', footerLicenseLabel: 'Licenza', profileSwitchToOasis: 'Torna a Oasis', - profileVisibilityFediverse: "Multiverso", + profileSwitchToClearnet: 'Immergiti in Clearnet', + profileVisibilityFediverse: "Fediverse", + profileSwitchToFediverse: "Tuffati nel fediverso", + coordLabel: 'Coordinate (es. A3)', + coordPlaceholder: 'Inserisci coordinata', contributorsTitle: "Contributori", + pixeliaBy: "di", colorLabel: 'Scegli un colore', paintButton: 'Dipingi!', + invalidCoordinate: 'Coordinata errata', + goToMuralButton: "Vedi murale", totalPixels: 'Pixel totali', modules: "Moduli", modulesViewTitle: "Moduli", modulesViewDescription: "Configura il tuo ambiente attivando o disattivando i moduli.", inbox: "Posta in arrivo", multiverse: "Multiverso", - fediverse: "Multiverso", - fediverseDescription: "Gestisci i tuoi altri account del multiverso, inclusi l'invio e la ricezione di contenuti.", + fediverse: "Fediverse", + fediverseDescription: "Gestisci i tuoi altri account del fediverso, inclusi l'invio e la ricezione di contenuti.", fediverseStatus: "Stato", - fediverseDisconnected: "Multiverso disconnesso. Controlla %LINK% o lo stato della connessione.", - fediverseSettingsTitle: "Multiverso", + fediverseDisconnected: "Fediverso disconnesso. Controlla %LINK% o lo stato della connessione.", + fediverseSettingsTitle: "Fediverse", fediverseInstanceLabel: "Indirizzo", fediverseTokenLabel: "Token di accesso", fediverseTokenHelp: "Imposta il tuo token di accesso. Sarà usato per gestire il tuo account Mastodon da Oasis.", fediverseConnect: "Connetti", fediverseConnectedAs: "Connesso come", fediverseDisconnect: "Disconnetti", + fediverseEmpty: "Quando colleghi altri account del fediverso dalle %LINK%, puoi gestirli da qui.", fediverseEmptyLink: "impostazioni", fediverseComposePlaceholder: "A cosa stai pensando?", fediverseReplyPlaceholder: "Scrivi la tua risposta…", @@ -269,15 +333,21 @@ module.exports = { fediverseReply: "Rispondi", fediverseBoosted: "ha rilanciato", fediverseNoPosts: "La tua timeline è vuota.", + fediverseThread: "Discussione", fediverseTimeline: "Cronologie", + fediverseManageTimeline: "Gestisci la cronologia", fediverseFollowers: "Follower", fediverseFollowing: "Seguiti", fediversePosts: "Post", fediverseJoined: "Iscritto", + fediverseOverview: "Panoramica", fediverseRefresh: "Aggiorna", fediverseWrite: "Scrivi", fediversePreview: "Anteprima", + fediverseEdit: "Modifica", + fediverseOpenFeed: "Visita il feed", fediverseManage: "Gestisci", + fediverseStatusNotConnected: "Non connesso", fediverseError: "Impossibile contattare Mastodon.", fediverseErrInstance: "URL dell'istanza non valido.", fediverseErrAuth: "Token non valido o scaduto.", @@ -288,6 +358,17 @@ module.exports = { fediverseErrPost: "Impossibile pubblicare.", fediverseErrMedia: "Impossibile caricare il file.", fediverseErrEmpty: "Scrivi qualcosa o allega un file.", + popularLabel: "In evidenza", + topicsLabel: "Argomenti", + latestLabel: "Più recenti", + summariesLabel: "Riassunti", + threadsLabel: "Discussioni", + multiverseLabel: "Multiverso", + inboxLabel: "Posta in arrivo", + invitesLabel: "Inviti", + walletLabel: "Portafoglio", + legacyLabel: "Chiavi", + cipherLabel: "Crittografia", bookmarksLabel: "Segnalibri", videosLabel: "Video", torrentsLabel: "Torrents", @@ -298,14 +379,18 @@ module.exports = { inhabitantsLabel: "Abitanti", trendingLabel: "Tendenze", eventsLabel: "Eventi", + tasksLabel: "Compiti", apply: "Applica", menuPersonal: "Personale", + menuContent: "Contenuto", + menuBlogs: "Blog", menuGovernance: "Governance", menuOffice: "Ufficio", - menuNetwork: "Comunità", + menuMultiverse: "Multiverso", + menuNetwork: "Rete", menuCreative: "Creativo", menuEconomy: "Economia", - menuMedia: "Biblioteca", + menuMedia: "Media", menuTools: "Strumenti", comment: "Commento", subtopic: "Sotto-argomento", @@ -315,6 +400,13 @@ module.exports = { follow: "Supporta", block: "Blocca", unblock: "Sblocca", + newerPosts: "Post più recenti", + olderPosts: "Post più vecchi", + feedRangeEmpty: "L'intervallo indicato è vuoto per questo feed. Prova a visualizzare il ", + seeFullFeed: "feed completo", + feedEmpty: "La rete Oasis non ha mai visto post da questo account.", + beginningOfFeed: "Questo è l'inizio del feed", + noNewerPosts: "Non sono ancora stati ricevuti post più recenti.", relationshipNotFollowing: "Non ti supporta", relationshipTheyFollow: "Ti supporta", relationshipMutuals: "Supporto reciproco", @@ -324,20 +416,25 @@ module.exports = { relationshipBlockedBy: "Ti ha bloccato", relationshipMutualBlock: "Blocco reciproco", relationshipNone: "Non stai supportando", + relationshipConflict: "Conflitto", relationshipBlockingPost: "Post bloccato", viewLikes: "Vedi diffusioni", spreadedDescription: "Lista dei post diffusi dall'abitante.", + totalspreads: "Diffusioni totali", + attachFiles: "Allega file", preview: "Anteprima", publish: "Pubblica", contentWarningPlaceholder: "Aggiungi un oggetto al post (opzionale)", - publishWarningPlaceholder: "Corpo limitato a 7000 caratteri.", - publishTooLong: "Il tuo post è troppo lungo. Accorcialo, per favore.", + privateWarningPlaceholder: "Aggiungi abitanti per inviare un post privato (opzionale)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "RICORDA: A causa della tecnologia blockchain, una volta pubblicato un post non può essere modificato né eliminato.", ], commentWarning: [ "RICORDA: A causa della tecnologia blockchain, una volta pubblicato un post non può essere modificato né eliminato.", ], + commentPublic: "pubblico", + commentPrivate: "privato", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -357,6 +454,7 @@ module.exports = { ".", ], publishCustom: "Scrivi un post avanzato", + subtopicLabel: "Crea un sotto-argomento di questo post", messagePreview: "Anteprima post", mentionsMatching: "Menzioni corrispondenti", mentionsName: "Nome", @@ -379,93 +477,21 @@ module.exports = { ssbLogStream: "Blokchain", ssbLogStreamDescription: "Configura il limite di messaggi per i flussi della Blockchain.", saveSettings: "Salva impostazioni", + exportTitle: "Esporta dati", exportDescription: "Imposta una password (minimo 32 caratteri) per crittografare la tua chiave", exportDataTitle: "Backup", exportDataDescription: "Scarica i tuoi dati (chiave segreta esclusa!)", exportDataButton: "Scarica database", + pubWallet: "Portafoglio PUB", + pubWalletDescription: "Imposta l'URL del portafoglio PUB. Verrà utilizzato per le transazioni PUB (inclusa la UBI).", + pubWalletConfiguration: "Salva configurazione", + importTitle: "Importa dati", importDescription: "Importa il tuo segreto crittografato (chiave privata) per attivare il tuo avatar", + importAttach: "Allega file crittografato (.enc)", + passwordLengthInfo: "La password deve avere almeno 32 caratteri.", passwordImport: "Scrivi la tua password per decifrare i dati che verranno salvati nella home del sistema (nome: secret)", exportPasswordPlaceholder: "Usa minuscole, maiuscole, numeri e simboli", - fileInfo: "La tua chiave segreta crittografata verrà scaricata sul tuo dispositivo (nome: oasis.enc)", - developer: "Sviluppo", - devTitle: "Sviluppo", - welcomeBannerText: "Benvenuta in OASIS. Segui questi passi rapidi per configurare il tuo avatar.", - aiSuggestionBanner: "Suggerimento:", - aiSuggestionsEnable: "Suggerimenti dell'IA", - welcomeBannerAction: "Inizia!", - welcomeTitle: "Benvenuta", - welcomeStepGreetingAction: "Invia", - welcomeJoinPubButton: "Unisciti al pub", - welcomeStepLarpAction: "Unisciti a \"L Accademia\"", - welcomeStepLarpText: "Unisciti al nostro gioco di ruolo dal vivo.", - welcomeStepLarpTitle: "Unisciti al L.A.R.P.", - welcomeStepGreetingTitle: "Invia un \"Ciao mondo!\"", - welcomeStepGreetingText: "Invia un messaggio perché gli altri abitanti possano scoprirti.", - welcomeJoinPub: "Unisciti a", - welcomeDescription: "Alcune cose da fare prima di iniziare.", - welcomeProgress: "Completato", - welcomeStepDone: "fatto", - welcomeStepPending: "in sospeso", - welcomeSkip: "Non ora", - welcomeStepLanguageTitle: "Scegli la tua lingua", - welcomeStepLanguageText: "Oasis parla diverse lingue. Puoi cambiarla quando vuoi.", - welcomeStepProfileTitle: "Modifica il tuo profilo", - welcomeStepProfileText: "Scegli un nome, aggiungi una descrizione e metti un immagine perché gli altri abitanti ti riconoscano.", - welcomeStepProfileAction: "Salva profilo", - welcomeStepFederationTitle: "Unisciti alla rete principale", - welcomeStepFederationText: "Collegati al nostro pub per incontrare altri abitanti e iniziare a replicare.", - welcomeStepFederationAction: "Vai agli inviti", - welcomeStepBackupTitle: "Fai il backup del tuo ID", - welcomeStepBackupText: "La tua identità è un file di chiavi su questo dispositivo.", - welcomeStepBackupWarning: "SE LO PERDI, NESSUNO POTRÀ RECUPERARLO PER TE.", - welcomeStepBackupAction: "Backup!", - welcomeCompleteTitle: "Tutto pronto.", - welcomeCompleteText: "Il tuo nodo è pronto. Da qui la rete cresce con le persone che segui.", - welcomeFindPeople: "Trova abitanti", - devFilterFiles: "FILE", - devFilterSearch: "CERCA", - devFilterModules: "MODULI", - devFilterTranslations: "TRADUZIONI", - devFilterStyles: "STILI", - devFilterTests: "TEST", - devVersion: "Versione", - devNodeVersion: "Node", - devStatsTests: "Suite di test", - devParentFolder: "Cartella superiore", - devOpenFolder: "apri", - devDescription: "Esplora il codice sorgente in esecuzione su questo nodo.", - devRoot: "radice", - devSearchPlaceholder: "Testo da cercare nel codice", - devAnyExtension: "Qualsiasi file", - devStatsFiles: "File", - devStatsLines: "Righe", - devStatsModules: "Moduli", - devStatsLanguages: "Lingue", - devNotViewable: "non visualizzabile", - devEmptyFolder: "Questa cartella è vuota.", - devSize: "Dimensione", - devShowing: "Mostrando", - devLine: "riga", - devReportBug: "Segnala Bug", - devCreateTask: "Crea Attività", - devRaw: "RAW", - devReportContext: "Trovato leggendo il codice sorgente di Oasis", - devTaskPrefix: "Rivedere", - devPrevPage: "Righe precedenti", - devNextPage: "Righe successive", - devMatches: "corrispondenze", - devFilesScanned: "file analizzati", - devNoResults: "Ancora nessuna corrispondenza.", - devColModule: "Modulo", - devColState: "Stato", - devColModel: "Modello", - devColView: "Vista", - devColRoutes: "Rotte", - devColTests: "Test", - devOn: "attivo", - devOff: "inattivo", - modulesDevLabel: "Sviluppo", - modulesDevDescription: "Modulo per gestire il codice sorgente di Oasis.", + fileInfo: "La tua chiave segreta crittografata verrà salvata nella home del sistema (nome: oasis.enc)", themeIntro: "Scegli un tema.", setTheme: "Imposta tema", @@ -473,13 +499,16 @@ module.exports = { languageDescription: "Se desideri usare un'altra lingua, selezionala qui.", setLanguage: "Imposta lingua", + peerConnections: "Peer", peerConnectionsIntro: "Gestisci tutte le tue connessioni con altri peer.", peerConnectionsTitle: "Connessioni", online: "Online", offline: "Offline", discovered: 'Scoperti', unknown: 'Sconosciuto', + lanBroadcastLabel: "Broadcast LAN", lanBroadcastActive: "Attivo (multicast UDP sulla LAN)", + lanBroadcastDisabled: "Disattivato (modalità pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -492,6 +521,8 @@ module.exports = { noConnections: "Nessun peer connesso.", noDiscovered: "Nessun peer scoperto.", noUnknownPeers: "Nessun peer sconosciuto nel grafo delle amicizie.", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -501,6 +532,9 @@ module.exports = { peerImport: "Importa", peerImportTitle: "Importa lista peer", peerImportPlaceholder: "Incolla un indirizzo multiserver per riga, o carica un file .txt…", + noSupportedConnections: "Nessun peer supportato.", + noBlockedConnections: "Nessun peer bloccato.", + noRecommendedConnections: "Nessun peer consigliato.", connectionActionIntro: "", startNetworking: "Avvia rete", @@ -514,19 +548,9 @@ module.exports = { homePageDescription: "Seleziona quale modulo vuoi come pagina iniziale.", saveHomePage: "Imposta home", invites: "Inviti", + invitesTitle: "Inviti", + invitesInvites: "Inviti", invitesDescription: "Gestisci e applica codici invito nella tua rete.", - invitesPubsHint: "Incolla un codice d'invito di un pub per federarti con lui e replicarne gli abitanti.", - invitesFederationsHint: "Pub con cui sei federato: aggiornali, togli quelli irraggiungibili o esporta l'elenco.", - invitesHousesHint: "Inserisci un codice casa per unirti a una casa del LARP e partecipare alla sua economia.", - invitesTribesHint: "Inserisci un codice tribù per diventarne membro e accedere ai contenuti privati.", - invitesChatsHint: "Inserisci un codice chat per unirti alla conversazione e ai suoi thread.", - invitesPadsHint: "Inserisci un codice pad per scrivere insieme su un documento condiviso.", - invitesCalendarsHint: "Inserisci un codice calendario per condividere date e promemoria.", - invitesEventsHint: "Inserisci un codice evento per partecipare a un evento privato.", - invitesForumsHint: "Inserisci un codice forum per leggere e rispondere in un forum privato.", - invitesMapsHint: "Inserisci un codice mappa per vedere e aggiungere luoghi su una mappa condivisa.", - invitesShopsHint: "Inserisci un codice negozio per unirti a un negozio e al suo catalogo.", - invitesInhabitantsHint: "Inserisci un codice abitante per seguirvi a vicenda e scambiare contenuti.", invitesTribesTitle: "Tribù", invitesTribeInviteCodePlaceholder: "Inserisci codice invito tribù", invitesTribeJoinButton: "Unisciti alla tribù", @@ -557,6 +581,7 @@ module.exports = { invitesAlreadyFederated: "Sei già federato con questo pub.", invitesFederationsTitle: "Federazioni", invitesAcceptedInvites: "Federate", + invitesNoInvites: "Nessun invito accettato ancora.", invitesUnfollow: "Smetti di seguire", invitesFollow: "Segui", invitesUnfollowedInvites: "Non federate", @@ -576,24 +601,39 @@ module.exports = { panicMode: "Modalità panico!", encryptData: "Imposta una password (minimo 32 caratteri) per crittografare la tua blockchain", decryptData: "Inserisci la password per decifrare la tua blockchain", + panicModeDescription: "Crittografa/Decifra o ELIMINA la tua blockchain", removeDataDescription: "ATTENZIONE: Questo processo non può essere annullato.", + encryptPanicButton: "Crittografa blockchain", + decryptPanicButton: "Decifra blockchain", removePanicButton: "ELIMINA TUTTI I TUOI DATI!", searchTitle: "Cerca", searchDescriptionLabel: "Cerca contenuti nella tua rete.", + searchLanguagesLabel: "Lingue", + searchSkillsLabel: "Competenze", searchPlaceholder:"Cerca contenuti...", + searchDateLabel:"Data", searchLocationLabel:"Posizione", searchPriceLabel:"Prezzo", + searchUrlLabel:"URL", searchCategoryLabel:"Categoria", + searchStartLabel:"Inizio", + searchEndLabel:"Fine", searchPriorityLabel:"Priorità", searchStatusLabel:"Stato", statusLabel:"Stato", + totalVotesLabel:"Voti totali", noResultsFound:"Nessun risultato trovato.", votesOption:"Opzioni di voto", voteYesLabel:"Sì", voteNoLabel:"No", allTypesLabel: "TUTTI", + ABSTENTIONLabel:"ASTENSIONE", YESLabel:"SÌ", NOLabel:"NO", + FOLLOW_MAJORITYLabel: "SEGUI MAGGIORANZA", + CONFUSEDLabel: "CONFUSO", + NOT_INTERESTEDLabel: "NON INTERESSATO", + StatusLabel:"Stato", votesOptionYesLabel:"Sì", votesOptionNoLabel:"No", votesOptionAbstentionLabel:"Astensione", @@ -601,6 +641,7 @@ module.exports = { votesCreatedByLabel:"Creato da", voteStatusOpen:"APERTO", voteStatusClosed:"CHIUSO", + imageSearchLabel: "Inserisci parole per cercare immagini etichettate con esse.", commentDescription: ({ parentUrl }) => [ " ha commentato in ", a({ href: parentUrl }, " discussione"), @@ -611,20 +652,53 @@ module.exports = { a({ href: parentUrl }, " un post"), ], subtopicTitle: ({ authorName }) => [`Sotto-argomento del post di @${authorName}`], + mysteryDescription: "ha pubblicato un post misterioso", oasisDescription: "Rete del Progetto OASIS", searchSubmit: "Cerca...", + postLabel: "POST", + aboutLabel: "ABITANTI", + feedLabel: "FEED", votesLabel: "VOTAZIONI", + reportLabel: "SEGNALAZIONI", + imageLabel: "IMMAGINI", + videoLabel: "VIDEO", + audioLabel: "AUDIO", + documentLabel: "DOCUMENTI", + torrentLabel: "TORRENT", pdfFallbackLabel: "Documento PDF", + eventLabel: "EVENTI", + taskLabel: "COMPITI", + transferLabel: "TRASFERIMENTI", + curriculumLabel: "CURRICULUM", + bookmarkLabel: "SEGNALIBRI", + tribeLabel: "TRIBÙ", + marketLabel: "MERCATO", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "PORTAFOGLI", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "Invia", + subjectLabel: "Oggetto", editProfile: "Modifica avatar", + profileQrAlt: "Codice QR del profilo", + profileQrHint: "Scansiona per copiare questo Oasis ID.", oasisIdLabel: "OasisID", - spreadContent: "Replica", + spreadLabel: "Diffondi", + spreadHint: "Diffondi questo a chi ti supporta (replica via il tuo feed).", welcomePmSubject: "Hello World!", - welcomePmBody: "Benvenuto su Oasis — un social network libero, peer-to-peer, federato e cifrato, con un'architettura distribuita solo su invito.\n\nAlcuni posti interessanti da cui partire:\n\n- /profile — Modifica il tuo avatar, nome, descrizione e quali moduli appaiono sul tuo lato pubblico.\n- /invites — Aggiungi un pub per federarti con il resto della rete.\n- /peers — Vedi chi è connesso, riscansiona la LAN, esporta o importa liste di peer.\n- /inhabitants — Scopri e visita gli avatar di altre persone.\n- /tribes — Crea o unisciti a gruppi privati cifrati end-to-end.\n- /inbox — Leggi e invia messaggi privati.\n- /activity — Vedi l'attività recente, tua e della tua rete.\n- /publish — Scrivi un post o condividi un'immagine, un audio, un video o un documento.\n- /search — Cerca i contenuti della rete per tipo.\n\nAlcuni luoghi interessanti per connettersi:\n\n- /multiverse — Gestisci i tuoi altri account del multiverso, inclusi l'invio e la ricezione di contenuti.\n\nQuesto messaggio è stato inviato privatamente da te a te stesso, perciò non serviva alcuna connessione per riceverlo.", + welcomePmBody: "Benvenuto su Oasis — un social network libero, peer-to-peer, federato e cifrato, con un'architettura distribuita solo su invito.\n\nAlcuni posti interessanti da cui partire:\n\n- /profile — Modifica il tuo avatar, nome, descrizione e quali moduli appaiono sul tuo lato pubblico.\n- /invites — Aggiungi un pub per federarti con il resto della rete.\n- /peers — Vedi chi è connesso, riscansiona la LAN, esporta o importa liste di peer.\n- /inhabitants — Scopri e visita gli avatar di altre persone.\n- /tribes — Crea o unisciti a gruppi privati cifrati end-to-end.\n- /inbox — Leggi e invia messaggi privati.\n- /activity — Vedi l'attività recente, tua e della tua rete.\n- /publish — Scrivi un post o condividi un'immagine, un audio, un video o un documento.\n\nAlcuni luoghi interessanti per connettersi:\n\n- /fediverse — Gestisci i tuoi altri account del fediverso, inclusi l'invio e la ricezione di contenuti.\n\nQuesto messaggio è stato inviato privatamente da te a te stesso, perciò non serviva alcuna connessione per riceverlo.", + profileVisibilityTitle: "Visibilità del profilo pubblico", + profileVisibilityHint: "Scegli quali campi mostrano gli altri abitanti nel tuo profilo. Per impostazione predefinita appare solo Karma.", profileSensorsSectionTitle: "Sensori", profileVisibilityActivity: "Livello di attività", + profileVisibilityDevice: "Dispositivo", profileDeviceLockedHint: "Questo sensore è sempre attivo: mostra agli altri da quale dispositivo ti connetti e non può essere disattivato.", profileVisibilityKarma: "Punteggio KARMA", profileVisibilityUbi: "UBI", @@ -632,6 +706,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "Chiave GPG", + profileVisibilityClearnet: "Pubblica il mio profilo", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Negozi", profileClearnetJobsLabel: "Lavori", @@ -685,6 +760,7 @@ module.exports = { " o lo stato della connessione.", ], walletSentToLine: ({ destination, amount }) => `Inviati ECO ${amount} a ${destination}`, + walletSettingsTitle: "Portafoglio", walletSettingsDescription: "Integra Oasis con il tuo portafoglio ECOin.", walletSettingsDocLink: "Guida all'installazione di ECOin", walletStatusMessages: { @@ -706,19 +782,37 @@ module.exports = { cipherTitle: "Crittografia", cipherDescription: "Crittografa e decifra il tuo testo simmetricamente (usando una password condivisa).", randomPassword: "Password casuale", + cipherEncryptTitle: "Crittografa testo", + cipherEncryptDescription: "Inserisci il testo da crittografare", + cipherTextLabel: "Testo da crittografare", cipherTextPlaceholder: "Inserisci il testo da crittografare...", cipherPasswordLabel: "Imposta una password (minimo 32 caratteri) per crittografare il testo", cipherPasswordDecryptLabel: "Imposta una password (minimo 32 caratteri) per decifrare il testo", cipherPasswordPlaceholder: "Inserisci una password...", cipherEncryptButton: "Crittografa", + cipherDecryptTitle: "Decifra testo", + cipherDecryptDescription: "Inserisci il testo da decifrare", cipherEncryptedMessageLabel: "Testo crittografato", cipherDecryptedMessageLabel: "Testo decifrato", + cipherPasswordUsedLabel: "Password usata per crittografare (conservala!)", cipherEncryptedTextPlaceholder: "Inserisci il testo crittografato...", + cipherIvLabel: "IV", + cipherIvPlaceholder: "Inserisci il vettore di inizializzazione...", cipherDecryptButton: "Decifra", password: "Password", text: "Testo", encryptedText: "Testo crittografato", iv: "Vettore di inizializzazione (IV)", + encryptTitle: "Crittografa il tuo testo", + encryptDescription: "Inserisci il testo che vuoi crittografare e fornisci una password.", + encryptButton: "Crittografa", + decryptTitle: "Decifra il tuo testo", + decryptDescription: "Inserisci il testo crittografato e fornisci la stessa password usata per la crittografia.", + decryptButton: "Decifra", + passwordLengthError: "La password deve avere almeno 32 caratteri.", + missingFieldsError: "Testo, password o IV non forniti.", + encryptionError: "Errore durante la crittografia del testo.", + decryptionError: "Errore durante la decifratura del testo.", bookmarkTitle: "Segnalibri", bookmarkDescription: "Scopri e gestisci i segnalibri nella tua rete.", bookmarkAllSectionTitle: "Segnalibri", @@ -744,6 +838,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "Opzionale", bookmarkTagsLabel: "Tag", bookmarkTagsPlaceholder: "Inserisci tag separati da virgole", + bookmarkCategoryLabel: "Categoria", + bookmarkCategoryPlaceholder: "Opzionale", bookmarkLastVisitLabel: "Ultima visita", bookmarkSearchPlaceholder: "Cerca URL, tag, categoria, autore...", bookmarkSortRecent: "Più recenti", @@ -758,6 +854,8 @@ module.exports = { noLastVisit: "Nessuna ultima visita", videoTitle: "Video", videoDescription: "Esplora e gestisci contenuti video nella tua rete.", + videoPluginTitle: "Titolo", + videoPluginDescription: "Descrizione", videoMineSectionTitle: "I tuoi video", videoCreateSectionTitle: "Carica video", videoUpdateSectionTitle: "Aggiorna video", @@ -789,6 +887,7 @@ module.exports = { videoSortOldest: "Più vecchi", videoSortTop: "Più votati", videoSearchButton: "Cerca", + videoMessageAuthorButton: "PM", videoUpdatedAt: "Aggiornato", videoNoMatch: "Nessun video corrisponde alla ricerca.", documentTitle: "Documenti", @@ -829,6 +928,8 @@ module.exports = { documentUpdatedAt: "Aggiornato", audioTitle: "Audio", audioDescription: "Esplora e gestisci contenuti audio nella tua rete.", + audioPluginTitle: "Titolo", + audioPluginDescription: "Descrizione", audioMineSectionTitle: "I tuoi audio", audioCreateSectionTitle: "Carica audio", audioUpdateSectionTitle: "Aggiorna audio", @@ -839,6 +940,7 @@ module.exports = { audioFilterMine: "MIEI", audioFilterRecent: "RECENTI", audioFilterTop: "TOP", + audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Carica audio", audioUpdateButton: "Aggiorna", audioDeleteButton: "Elimina", @@ -865,7 +967,6 @@ module.exports = { audioFilterFavorites: "PREFERITI", favoritesTitle: "Preferiti", favoritesDescription: "Tutti i tuoi contenuti preferiti in un unico posto.", - favoritesSearchPlaceholder: "Cerca nei preferiti...", favoritesFilterAll: "TUTTI", favoritesFilterRecent: "RECENTI", favoritesFilterAudios: "AUDIO", @@ -881,13 +982,18 @@ module.exports = { favoritesNoItems: "Nessun preferito ancora.", yourContacts: "I tuoi contatti", allInhabitants: "Abitanti", - allCVs: "CV", + allCVs: "Tutti i CV", discoverPeople: "Scopri abitanti nella tua rete.", + allInhabitantsButton: "TUTTI", + contactsButton: "SUPPORTATI", + CVsButton: "CV", + matchSkills: "Abbina competenze", + matchSkillsButton: "ABBINA COMPETENZE", + suggestedButton: "SUGGERITI", searchInhabitantsPlaceholder: "FILTRA abitanti PER NOME …", filterLocation: "FILTRA abitanti PER POSIZIONE …", filterLanguage: "FILTRA abitanti PER LINGUA …", filterSkills: "FILTRA abitanti PER COMPETENZE …", - cvSkillsCount: "Competenze", applyFilters: "Applica filtri", locationLabel: "Posizione", languagesLabel: "Lingue", @@ -896,18 +1002,17 @@ module.exports = { mutualFollowers: "Follower reciproci", suggestedFollowsYou: "Ti segue", latestInteractions: "Ultime interazioni", + viewAvatar: "Vedi avatar", + viewCV: "Vedi CV", suggestedSectionTitle: "Suggeriti", topkarmaSectionTitle: "Top Karma", topecoSectionTitle: "Top Eco", topactivitySectionTitle: "Top Attività", - "TOP INACTIVITYButton": "PIÙ INATTIVI", - topinactivitySectionTitle: "Abitanti meno attivi", blockedSectionTitle: "Bloccati", gallerySectionTitle: "GALLERIA", - topSectionTitle: "TOP", + blockedButton: "BLOCCATI", blockedLabel: "Abitante bloccato", inhabitantviewDetails: "Vedi dettagli", - cvVisitButton: "Visita il CV", keepReading: "Continua a leggere...", oasisId: "ID", noInhabitantsFound: "Nessun abitante trovato ancora.", @@ -920,7 +1025,6 @@ module.exports = { parliamentFilterCandidatures: "CANDIDATURE", parliamentFilterProposals: "PROPOSTE", parliamentFilterLaws: "LEGGI", - parliamentFilterRevocations: "REVOCHE", parliamentFilterHistorical: "STORICO", parliamentFilterLeaders: "LEADER", parliamentFilterRules: "REGOLE", @@ -946,8 +1050,10 @@ module.exports = { parliamentPoliciesDeclined: "LEGGI RESPINTE", parliamentPoliciesDiscarded: "LEGGI SCARTATE", parliamentEfficiency: "% EFFICIENZA", + parliamentFilterRevocations: 'REVOCHE', parliamentRevocationFormTitle: 'Revoca legge', parliamentRevocationLaw: 'Legge', + parliamentRevocationTitle: 'Titolo', parliamentRevocationReasons: 'Motivazioni', parliamentRevocationPublish: 'Pubblica revoca', parliamentCurrentRevocationsTitle: 'Revoche attuali', @@ -960,12 +1066,23 @@ module.exports = { parliamentNoGovernments: "Non ci sono ancora governi.", parliamentCandidatureFormTitle: "Proponi candidatura", parliamentCandidatureIdPh: "Oasis ID (@...) o nome tribù", + parliamentCandidatureSlogan: "Slogan (max 140 caratteri)", + parliamentCandidatureSloganPh: "Un breve motto", parliamentCandidatureMethod: "Metodo", parliamentCandidatureProposeBtn: "Pubblica candidatura", + parliamentThDate: "Data proposta", + parliamentThSlogan: "Slogan", + parliamentThSince: "Profilo dal", + parliamentThVotes: "Voti ricevuti", + parliamentThVoteAction: "Vota", + parliamentTypeUser: "Abitante", + parliamentTypeTribe: "Tribù", parliamentVoteBtn: "Vota", parliamentProposalFormTitle: "Proponi legge", parliamentProposalDescription: "Descrizione (≤1000)", parliamentProposalPublish: "Pubblica proposta", + parliamentFinalize: "Finalizza", + parliamentDeadline: "Scadenza", parliamentNoProposals: "Non ci sono ancora proposte di legge.", parliamentNoLaws: "Non ci sono ancora leggi approvate.", parliamentMethodDEMOCRACY: "Democrazia", @@ -983,21 +1100,29 @@ module.exports = { parliamentVotesSlashTotal: "Voti/Totale", parliamentVotesNeeded: "Voti necessari", parliamentFutureLawsTitle: "Leggi future", + parliamentNoFutureLaws: "Nessuna legge futura, per ora.", parliamentVoteAction: "Vota", + parliamentLeadersTitle: "Leader", parliamentThLeader: "AVATAR", parliamentPopulation: "Popolazione", + parliamentThType: "Tipo", + parliamentThInPower: "Al potere", parliamentThPresented: "CANDIDATURE", parliamentNoLeaders: "Nessun leader, per ora.", parliamentCandidaturesListTitle: "Lista delle candidature", parliamentTimeRemaining: "Tempo rimanente", + parliamentNoLeader: "Nessuna candidatura vincente, per ora.", parliamentElectionsStatusTitle: "Prossimo governo", parliamentProposalDeadlineLabel: "Scadenza", parliamentProposalTimeLeft: "Tempo rimanente", + parliamentProposalOnTrack: "In linea per l'approvazione", + parliamentProposalOffTrack: "Supporto insufficiente", parliamentRulesTitle: "Come funziona il Parlamento", parliamentRulesIntro: "Le elezioni si risolvono ogni 2 mesi; le candidature sono continue e si azzerano quando un governo viene scelto.", parliamentRulesCandidates: "Qualsiasi abitante può proporre se stesso, un altro abitante o qualsiasi tribù. Ogni abitante può proporre fino a 3 candidature per ciclo; i duplicati nello stesso ciclo vengono rifiutati.", parliamentRulesElection: "Il vincitore è la candidatura con il maggior numero di voti al momento della risoluzione.", parliamentRulesTies: "Ordine di spareggio: karma più alto dell'abitante; in caso di parità, profilo più vecchio; se ancora in parità, proposta più antica; poi ordine lessicografico per ID.", + parliamentRulesFallback: "Se nessuno vota, vince l'ultima candidatura proposta. Se non ci sono candidature, viene selezionata una tribù casuale.", parliamentRulesTerm: "La scheda Governo mostra il governo attuale e le statistiche.", parliamentRulesMethods: "Forme: Anarchia (maggioranza semplice), Democrazia (50%+1), Maggioranza (80%), Minoranza (20%), Karmatocrazia (proposte con karma più alto), Dittatura (approvazione istantanea).", parliamentRulesAnarchy: "L'Anarchia è la modalità predefinita: se nessuna candidatura viene eletta alla risoluzione, viene proclamata l'Anarchia. In Anarchia, qualsiasi abitante può proporre leggi.", @@ -1019,7 +1144,11 @@ module.exports = { courtsCaseFormTitle: "Apri caso", courtsCaseRespondent: "Accusato / Convenuto", courtsCaseRespondentPh: "Oasis ID (@...) o nome tribù", + courtsCaseMediatorsAccuser: "Mediatori (accusatore)", courtsCaseMethod: "Metodo di risoluzione", + courtsCaseDescription: "Descrizione (max 1000 caratteri)", + courtsCaseEvidenceTitle: "Prove del caso", + courtsCaseEvidenceHelp: "Allega immagini, audio, documenti (PDF) o video a supporto del tuo caso.", courtsCaseSubmit: "Presenta caso", courtsNominateJudge: "Nomina giudice", courtsJudgeId: "Giudice", @@ -1064,6 +1193,22 @@ module.exports = { courtsRulesMisconduct: "Molestie, manipolazione o prove fabbricate possono portare a una risoluzione negativa immediata.", courtsRulesGlossary: "Caso: registro di un conflitto. Prove: materiali a supporto delle affermazioni. Verdetto: decisione con rimedio. Appello: richiesta di revisione del verdetto.", courtsFilterActions: "AZIONI", + courtsNoActions: "Nessuna azione in sospeso per il tuo ruolo.", + courtsCaseTitlePlaceholder: "Breve descrizione del conflitto", + courtsCaseSeverity: "Gravità", + courtsCaseSeverityNone: "Nessun livello di gravità", + courtsCaseSeverityLOW: "Bassa", + courtsCaseSeverityMEDIUM: "Media", + courtsCaseSeverityHIGH: "Alta", + courtsCaseSeverityCRITICAL: "Critico", + courtsCaseSubject: "Argomento", + courtsCaseSubjectNone: "Nessun argomento", + courtsCaseSubjectBEHAVIOUR: "Comportamento", + courtsCaseSubjectCONTENT: "Contenuto", + courtsCaseSubjectGOVERNANCE: "Governance / regole", + courtsCaseSubjectFINANCIAL: "Finanze / risorse", + courtsCaseSubjectOTHER: "Altro", + courtsHiddenRespondent: "Nascosto (visibile solo ai ruoli coinvolti).", courtsThRole: "Ruolo", courtsRoleAccuser: "Accusatore", courtsRoleDefence: "Difesa", @@ -1074,19 +1219,51 @@ module.exports = { courtsAssignJudgeBtn: "Scegli giudice", trendingTitle: "Tendenze", exploreTrending: "Esplora i contenuti più popolari nella tua rete.", + bookmarkButton: "SEGNALIBRI", + transferButton: "TRASFERIMENTI", + eventButton: "EVENTI", + taskButton: "COMPITI", votesButton: "VOTAZIONI", - shopProductButton: "NEGOZIO", + reportButton: "SEGNALAZIONI", + feedButton: "FEED", + marketButton: "MERCATO", + imageButton: "IMMAGINI", + audioButton: "AUDIO", + videoButton: "VIDEO", + documentButton: "DOCUMENTI", + torrentButton: "TORRENT", + noTrendingFound: "Nessun contenuto di tendenza trovato.", + noContentMessage: "Nessun contenuto di tendenza disponibile.", + trendingDescription: "Descrizione", + trendingDate: "Data", + trendingLocation: "Posizione", + trendingPrice: "Prezzo", + trendingUrl: "URL", + trendingCategory: "Categoria", + trendingStart: "Inizio", + trendingEnd: "Fine", + trendingPriority: "Priorità", + trendingStatus: "Stato", + trendingFrom: "Da", + trendingTo: "A", + trendingConcept: "Concetto", + trendingAmount: "Importo", + trendingDeadline: "Scadenza", + trendingItemStatus: "Stato articolo", + trendingTotalVotes: "Voti totali", trendingTotalOpinions: "Opinioni totali", - trendingNoContentMessage: "Ancora nessuna tendenza.", + trendingNoContentMessage: "Nessun contenuto di tendenza disponibile.", + trendingAuthor: "Di", + trendingCreatedAtLabel: "Creato il", trendingTotalCount: "Conteggio totale", tasksTitle: "Compiti", tasksDescription: "Scopri e gestisci compiti nella tua rete.", - taskSearchPlaceholder: "Cerca attività...", taskTitleLabel: "Titolo", taskDescriptionLabel: "Descrizione", taskStartTimeLabel: "Ora di inizio", taskEndTimeLabel: "Ora di fine", taskPriorityLabel: "Priorità", + taskPrioritySelect: "Seleziona priorità", taskPriorityUrgent: "Urgente", taskPriorityHigh: "Alta", taskPriorityMedium: "Media", @@ -1096,13 +1273,13 @@ module.exports = { taskVisibilityLabel: "Visibilità", taskPublic: "pubblico", taskPrivate: "privato", + taskCreatedAt: "Creato il", + taskBy: "Di", taskStatus: "Stato", taskStatusOpen: "Apri", taskStatusInProgress: "In corso", taskStatusClosed: "Chiuso", taskAssignedTo: "Assegnato a", - taskAssignedChip: "Assegnata", - taskUnassignedChip: "Non assegnata", taskAssignees: "Assegnatari", taskAssignButton: "Assegna a me", taskUnassignButton: "Rimuovi assegnazione", @@ -1115,6 +1292,7 @@ module.exports = { taskFilterInProgress: "IN CORSO", taskFilterClosed: "CHIUSI", taskFilterAssigned: "ASSEGNATI", + taskFilterArchived: "ARCHIVIATI", taskFilterUrgent: "URGENTE", taskFilterHigh: "ALTA", taskFilterMedium: "MEDIA", @@ -1127,21 +1305,23 @@ module.exports = { taskInProgressTitle: "Compiti in corso", taskClosedTitle: "Compiti chiusi", taskAssignedTitle: "Compiti assegnati", + taskArchivedTitle: "Compiti archiviati", + taskPublicTitle: "Compiti pubblici", + taskPrivateTitle: "Compiti privati", notasks: "Nessun compito disponibile.", noLocation: "Nessuna posizione specificata", taskSetStatus: "Imposta stato", + eventTitle: "Eventi", eventDateLabel: "Data", - eventRecurrenceLabel: "Ricorrenza", - eventRecurrenceUntil: "Ripeti fino al", - eventRecurrenceHint: "Lascia vuota la data finale per un evento singolo.", - eventUpcomingDates: "Prossime date", eventsTitle: "Eventi", eventsDescription: "Scopri e gestisci eventi nella tua rete.", - eventSearchPlaceholder: "Cerca eventi...", + eventDescription: "Descrizione", eventPrice: "Prezzo", eventStatus: "Stato", + eventOrganizer: "Organizzatore", eventAllSectionTitle: "Eventi", eventMineSectionTitle: "I tuoi eventi", + eventArchivedTitle: "Eventi archiviati", eventCreateSectionTitle: "Crea evento", eventUpdateSectionTitle: "Aggiorna evento", eventDeleteButton: "Elimina", @@ -1149,26 +1329,11 @@ module.exports = { eventAddToCalendar: "Aggiungi al calendario", eventVisitCalendar: "Vai al calendario", viewTask: "Visualizza attività", - viewEvent: "Vedi evento", - viewPad: "Vedi pad", - viewMap: "Vedi mappa", - viewCalendar: "Vedi calendario", viewReport: "Visualizza segnalazione", viewTransfer: "Visualizza trasferimento", viewItem: "Visualizza articolo", viewShop: "Visualizza negozio", viewProject: "Visualizza progetto", - viewJob: "Vedi Lavoro", - viewPlace: "Vedi Luogo", - generatePdf: "Genera PDF", - sharePm: "Condividi via MP", - galleryImages: "Immagini (dimensione max: 50MB ciascuna)", - galleryPhotos: "Immagini", - galleryAddPhoto: "Aggiungi Immagine", - galleryRemovePhoto: "Rimuovi", - galleryVideo: "Video (dimensione max: 50MB)", - galleryAddVideo: "Aggiungi Video", - galleryRemoveVideo: "Rimuovi video", viewVotation: "Visualizza votazione", voteCastTitle: "Esprimi il voto", voteResults: "Risultati", @@ -1176,15 +1341,29 @@ module.exports = { eventDescriptionLabel: "Descrizione", eventDescriptionPlaceholder: "Inserisci descrizione evento...", eventUpdateButton: "Aggiorna", + eventAttendeesLabel: "Partecipanti", + eventAttendeesPlaceholder: "Inserisci partecipanti, separati da virgole...", eventTagsLabel: "Tag", + eventTagsPlaceholder: "Inserisci tag evento, separati da virgole...", + eventTags: "Tag", eventPriceLabel: "Prezzo", eventUrlLabel: "URL", eventAttendees: "Partecipanti", + noAttendees: "Nessun partecipante", + eventCreatedAt: "Creato il", eventLocation: "Posizione", eventLocationLabel: "Posizione", + eventNoLocation: "Nessuna posizione specificata", + eventNoURL: "Nessun URL specificato", + eventBy: "Di", noevents: "Nessun evento disponibile.", eventDate: "Data", + eventDateFormat: "DD:MM:YYYY HH:mm", eventAttendButton: "Partecipa all'evento", + eventUnattendButton: "Lascia l'evento", + eventCreatedBy: "Creato da", + eventAttendeesCount: "Numero partecipanti", + eventCreatedByYou: "Hai creato questo evento", eventFilterAll: "TUTTI", eventFilterMine: "MIEI", eventFilterToday: "OGGI", @@ -1192,9 +1371,18 @@ module.exports = { eventFilterMonth: "QUESTO MESE", eventFilterYear: "QUEST'ANNO", eventFilterArchived: "ARCHIVIATI", + eventTodayTitle: "Eventi di oggi", + eventThisWeekTitle: "Eventi di questa settimana", + eventThisMonthTitle: "Eventi di questo mese", + eventThisYearTitle: "Eventi di quest'anno", eventPrivacyLabel: "Visibilità", eventPublic: "Pubblico", eventPrivate: "Privato", + eventPublicTitle: "Eventi pubblici", + eventNoPrice: "Evento gratuito", + eventNoImage: "Nessuna immagine caricata", + eventAttendConfirmation: "Stai partecipando a questo evento", + eventUnattendConfirmation: "Non partecipi più a questo evento", eventAttended: "Partecipato", eventUnattended: "Non partecipato", eventStatusOpen: "Aperto", @@ -1205,14 +1393,9 @@ module.exports = { tagsTopSectionTitle: "Tag più usati", tagsCloudSectionTitle: "Nuvola di tag", tagsFilterAll: "TUTTI", - tagsFilterMine: "MIE", - tagsFilterRecent: "RECENTI", - tagsMineSectionTitle: "Le mie etichette", - tagsRecentSectionTitle: "Etichette recenti", tagsFilterTop: "TOP", tagsFilterCloud: "NUVOLA", tagsNoItems: "Nessun tag disponibile.", - tagsSearchPlaceholder: "Cerca etichette...", tagsTableHeaderTag: "TAG/LINK:", tagsTableHeaderCount: "CONTATORE:", transfersTitle: "Trasferimenti", @@ -1252,12 +1435,18 @@ module.exports = { transfersDeadline: "Scadenza", transfersTags: "Tag", transfersStatus: "Stato", + transfersStatusUnconfirmed: "NON CONFERMATO", + transfersStatusClosed: "CHIUSO", + transfersStatusDiscarded: "SCARTATO", + transfersCreatedAt: "Creato il", transfersConfirmations: "CONFERME", - transfersExportContract: "Genera Fattura", + transfersContainingBlock: "Blocco contenitore", + transfersExportContract: "Crea contratto", transfersConfirmButton: "Conferma trasferimento", transfersNoItems: "Nessun trasferimento trovato.", transfersMineSectionTitle: "I tuoi trasferimenti", transfersUBISectionTitle: "Trasferimenti RBU", + transfersMarketSectionTitle: "Trasferimenti di mercato", transfersTopSectionTitle: "Trasferimenti principali", transfersPendingSectionTitle: "Trasferimenti in attesa", transfersUnconfirmedSectionTitle: "Trasferimenti non confermati", @@ -1265,13 +1454,21 @@ module.exports = { transfersDiscardedSectionTitle: "Trasferimenti scartati", transfersCreateSectionTitle: "Crea trasferimento", transfersAllSectionTitle: "Trasferimenti", + transfersFilterFavs: "Preferiti", + transfersFavsSectionTitle: "Trasferimenti preferiti", + transfersSearchLabel: "Cerca", transfersSearchPlaceholder: "Cerca concetto, tag, abitanti...", transfersMinAmountLabel: "Importo minimo", transfersMaxAmountLabel: "Importo massimo", + transfersSortLabel: "Ordina per", transfersSortRecent: "Più recenti", transfersSortAmount: "Importo più alto", transfersSortDeadline: "Scadenza più vicina", transfersSearchButton: "Cerca", + transfersFavoriteButton: "Preferito", + transfersUnfavoriteButton: "Rimuovi preferito", + transfersMessageUserButton: "Messaggio", + transfersExpiringSoonBadge: "IN SCADENZA", transfersExpiredBadge: "SCADUTO", transfersUpdatedAt: "Aggiornato", transfersNoMatch: "Nessun trasferimento corrisponde alla ricerca.", @@ -1284,9 +1481,6 @@ module.exports = { voteClosedTitle: "Votazioni chiuse", voteAllSectionTitle: "Votazioni", voteCreateButton: "Crea votazione", - voteErrorTitle: "Controlla il modulo", - voteErrorDeadlineMin: "La scadenza deve essere di almeno {days} giorni da ora.", - voteErrorGeneric: "Non è stato possibile salvare la votazione.", voteUpdateButton: "Aggiorna", voteDeleteButton: "Elimina", voteOptionYes: "SÌ", @@ -1316,6 +1510,16 @@ module.exports = { voteNoQuestion: "Nessuna domanda fornita", voteUnknownCreator: "Creatore sconosciuto", voteUnknownDate: "Data sconosciuta", + errorVoteNotFound: "Voto non trovato", + errorAlreadyVoted: "Hai già espresso un'opinione.", + errorVoteClosedCannotEdit: "Non puoi modificare una votazione chiusa", + errorVoteDeadlinePassed: "La scadenza per questa votazione è passata", + errorRetrievingVote: "Errore nel recupero del voto", + errorCreatingVote: "Errore nella creazione del voto", + errorVoteAlreadyVoted: "Non puoi modificare dopo aver espresso un'opinione", + errorDeletingOldVote: "Errore nell'eliminazione della vecchia opinione", + errorCreatingUpdatedVote: "Errore nella creazione dell'opinione aggiornata", + errorCreatingTombstone: "Errore nella creazione del tombstone", voteDetailSectionTitle: 'Dettagli voto', voteCommentsLabel: 'Commenti', voteCommentsForumButton: 'Apri discussione', @@ -1325,6 +1529,7 @@ module.exports = { voteNewCommentButton: 'Pubblica commento', voteNewCommentLabel: 'Aggiungi un commento', cvTitle: "Curriculum", + cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Modifica CV", cvCreateSectionTitle: "Crea CV", cvDescription: "Gestisci e condividi le tue competenze e informazioni professionali.", @@ -1343,121 +1548,29 @@ module.exports = { cvLocationLabel: "Posizione", cvStatusLabel: "Stato", cvPreferencesLabel: "Preferenze", + cvOasisContributorLabel: "Contributore Oasis", cvPersonal: "Personale", cvOasis: "Contributore Oasis (opzionale)", - cvOasisContributorView: "Contributo", + cvOasisContributorView: "Contributo Oasis", cvEducational: "Formazione (opzionale)", cvEducationalView: "Formazione", cvProfessional: "Professionale (opzionale)", cvProfessionalView: "Professionale", cvAvailability: "Disponibilità (opzionale)", + cvAvailabilityView: "Disponibilità", cvUpdateButton: "Aggiorna", cvCreateButton: "Crea CV", + cvContactLabel: "Contatto", cvCreatedAt: "Creato il", cvUpdatedAt: "Aggiornato il", cvEditButton: "Aggiorna", cvDeleteButton: "Elimina", cvNoCV: "Nessun CV trovato.", blogSubject: "Oggetto", - blogMessage: "Messaggio", + blogMessage: "Corpo limitato a 8096 caratteri.", blogImage: "Carica media (max: 50MB)", - blogMedia: "Allegati (fino a 8 immagini e un video)", blogPublish: "Anteprima", - pollsTitle: "Sondaggi", - dataTitle: "Corrispondenze", - dataDescription: "Esplora e visualizza le corrispondenze della tua rete.", - dataFilterAll: "TUTTI", - dataFilterMine: "I MIEI", - dataFilterRecent: "RECENTI", - dataFilterTop: "TOP", - dataKindInhabitants: "ABITANTI", - dataKindJobs: "LAVORI", - dataKindProjects: "PROGETTI", - dataKindEvents: "EVENTI", - dataKindTribes: "TRIBÙ", - dataKindMarket: "MERCATO", - dataKindHousing: "ALLOGGI", - dataKindIndustry: "INDUSTRIA", - dataKindTasks: "COMPITI", - dataKindReports: "RAPPORTI", - dataKindVotes: "VOTAZIONI", - dataSearchPlaceholder: "Cerca nei dati incrociati...", - dataCohesionTitle: "Coefficiente di coesione (CC)", - dataCcShort: "CC", - dataCohesionHint: "Affinità media fra tutto ciò che la rete ha pubblicato.", - dataAffinity: "Affinità", - dataCommonTerms: "Parola chiave", - dataStatPeople: "CV", - dataStatConnected: "Collegati", - dataStatSkills: "Competenze", - dataBestMatch: "Miglior match", - dataStatIsolated: "Isolati", - dataStatEntities: "Entità confrontate", - dataStatTerms: "Termini distinti", - dataStatPairs: "Coppie collegate", - dataMatchesTitle: "Corrispondenze", - dataMatchesHint: "Suggerimenti personalizzati dell'algoritmo.", - dataTermsTitle: "Parole chiave", - dataTermsHint: "I termini che compaiono nel maggior numero di contenuti.", - dataConnections: "Corrispondenze collegate", - dataTopicTitle: "Tutto ciò che riguarda", - dataTopicHint: "Ciò che l'algoritmo collega a questo tema", - dataNoMatches: "Nessun dato incrociato da mostrare.", - dataNoProfile: "Pubblica contenuti o scrivi il tuo CV così l'algoritmo impara cosa ti interessa.", - pollsDescription: "Modulo per interrogare la rete e contare le risposte.", - pollFilterAll: "TUTTI", - pollFilterMine: "I MIEI", - pollFilterRecent: "RECENTI", - pollFilterTop: "TOP", - pollFilterVoted: "VOTATI", - pollFilterOpen: "APERTI", - pollFilterClosed: "CHIUSI", - pollCreateButton: "Crea Sondaggio", - pollCreateTitle: "Crea Sondaggio", - pollEditTitle: "Modifica Sondaggio", - pollQuestion: "Domanda", - pollQuestionPlaceholder: "Cosa vuoi chiedere?", - pollOptions: "Opzioni (una per riga)", - pollOutcome: "Risultato", - pollNoVotesYet: "Ancora nessun voto", - pollTie: "Pareggio", - pollOptionsPlaceholder: "Piazza\nParco\nBar", - pollAnonymous: "Anonimo", - pollAnonymousLabel: "Sondaggio anonimo", - pollMultiple: "Scelta multipla", - pollMultipleLabel: "Consenti più risposte", - pollDeadline: "Scadenza", - pollTags: "Etichette", - pollPublishButton: "Pubblica Sondaggio", - pollUpdateButton: "Aggiorna", - pollCloseButton: "Chiudi", - pollDeleteButton: "Elimina", - pollVoteButton: "Vota", - pollChangeVote: "Cambia Voto", - pollVoters: "Votanti", - pollComments: "Commenti", - pollStatusLabel: "Stato", - pollStatusOpen: "APERTO", - pollStatusClosed: "CHIUSO", - pollSearchPlaceholder: "Cerca sondaggi...", - pollsNoItems: "Nessun sondaggio trovato.", - viewPoll: "Vedi Sondaggio", - pollChatCreate: "Crea Sondaggio", - pollInChat: "Sondaggio", - blogTitle: "Blog", - blogDescription: "Modulo per scoprire e gestire i blog.", - blogFilterAll: "TUTTI", - blogFilterMine: "I MIEI", - blogFilterRecent: "RECENTI", - blogFilterFavorites: "PREFERITI", - blogFilterTop: "TOP", - blogCreateButton: "Crea Blog", - blogSearchPlaceholder: "Cerca blog...", - blogComments: "Commenti", - blogAllowComments: "Consenti commenti", - blogCommentsClosed: "L'autore ha chiuso i commenti di questo blog.", - blogNoItems: "Nessun blog trovato.", - viewBlog: "Vedi Blog", + noPopularMessages: "Nessun messaggio popolare pubblicato ancora", forumTitle: "Forum", forumCategoryLabel: "Categoria", forumTitleLabel: "Titolo", @@ -1465,22 +1578,43 @@ module.exports = { forumCreateButton: "Crea forum", forumCreateSectionTitle: "Crea forum", forumDescription: "Parla apertamente con altri abitanti nella tua rete.", - forumSearchPlaceholder: "Cerca forum...", forumFilterAll: "TUTTI", forumFilterMine: "MIEI", forumFilterRecent: "RECENTI", forumFilterTop: "TOP", + forumMineSectionTitle: "I tuoi forum", + forumRecentSectionTitle: "Forum recenti", + forumAllSectionTitle: "Forum", forumDeleteButton: "Elimina", forumParticipants: "partecipanti", forumMessages: "messaggi", + forumLastMessage: "Ultimo messaggio", forumMessageLabel: "Messaggio", forumMessagePlaceholder: "Scrivi il tuo messaggio...", forumSendButton: "Invia", + forumVisitForum: "Visita forum", noForums: "Nessun forum trovato.", - forumVisitButton: "Vedi Forum", - forumTopicLabel: "Argomento", + forumVisitButton: "Visita forum", + forumCatGENERAL: "Generale", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", + forumCatPOLITICS: "Politica", + forumCatTECH: "Tecnologia", + forumCatSCIENCE: "Scienza", + forumCatMUSIC: "Musica", + forumCatART: "Arte", + forumCatGAMING: "Gaming", + forumCatBOOKS: "Libri", + forumCatFILMS: "Film", + forumCatPHILOSOPHY: "Filosofia", + forumCatSOCIETY: "Società", + forumCatPRIVACY: "Privacy", + forumCatCYBERWARFARE: "Cyberwarfare", + forumCatSURVIVALISM: "Survivalismo", imageTitle: "Immagini", imageDescription: "Esplora e gestisci immagini nella tua rete.", + imagePluginTitle: "Titolo", + imagePluginDescription: "Descrizione", imageMineSectionTitle: "Le tue immagini", imageCreateSectionTitle: "Carica immagine", imageUpdateSectionTitle: "Aggiorna immagine", @@ -1489,12 +1623,14 @@ module.exports = { imageTopSectionTitle: "Immagini più votate", imageFavoritesSectionTitle: "Preferiti", imageGallerySectionTitle: "Galleria", + imageMemeSectionTitle: "Meme", imageFilterAll: "TUTTE", imageFilterMine: "MIE", imageFilterRecent: "RECENTI", imageFilterTop: "TOP", imageFilterFavorites: "PREFERITE", imageFilterGallery: "GALLERIA", + imageFilterMeme: "MEME", imageCreateButton: "Carica immagine", imageUpdateButton: "Aggiorna", imageDeleteButton: "Elimina", @@ -1507,6 +1643,7 @@ module.exports = { imageTitlePlaceholder: "Opzionale", imageDescriptionLabel: "Descrizione", imageDescriptionPlaceholder: "Opzionale", + imageMemeLabel: "MEME?", imageNoFile: "Nessun file immagine fornito", noImages: "Nessuna immagine disponibile.", imageSearchPlaceholder: "Cerca titolo, tag, descrizione, autore...", @@ -1514,6 +1651,7 @@ module.exports = { imageSortOldest: "Più vecchie", imageSortTop: "Più votate", imageSearchButton: "Cerca", + imageMessageAuthorButton: "Messaggio", imageUpdatedAt: "Aggiornato", imageNoMatch: "Nessuna immagine corrisponde alla ricerca.", feedTitle: "Feed", @@ -1521,6 +1659,7 @@ module.exports = { createFeedButton: "Invia Feed!", feedPlaceholder: "Cosa succede? (max 280 caratteri)", TODAYButton: "OGGI", + CREATEButton: "Crea Feed", totalOpinions: "Opinioni totali", moreVoted: "Più votato", noFeedsFound: "Nessun feed trovato.", @@ -1543,12 +1682,20 @@ module.exports = { concept: "Concetto", description: "Descrizione", meme: "Meme", + activityContact: "Contatto", + activityBy: "Nome", + activityPixelia: "Nuovo pixel aggiunto", + viewImage: "Vedi immagine", + playAudio: "Riproduci audio", + playVideo: "Riproduci video", typeRecent: "RECENTI", - typeTop: "TOP", + errorActivity: "Errore nel recupero dell'attività", typePost: "BLOGS", + recentButton: "RECENTI", typeTribe: "TRIBÙ", typeLarp: "L.A.R.P.", typeInhabitants: "ABITANTI", + activityProfileUpdated: "ha aggiornato il profilo", typeAbout: "ABITANTI", typeCurriculum: "CVS", typeImage: "IMMAGINI", @@ -1592,6 +1739,8 @@ module.exports = { typeCourtsSettlementAccepted: "Tribunali · Accordo accettato", typeCourtsNomination: "Tribunali · Nomina", typeCourtsNominationVote: "Tribunali · Voto di nomina", + activitySupport: "Nuova alleanza forgiata", + activityJoin: "Nuovo PUB unito", question: "Domanda", deadline: "Scadenza", status: "Stato", @@ -1605,8 +1754,12 @@ module.exports = { date: "Data", category: "Categoria", attendees: "Partecipanti", + activitySpread: "->", + visitLink: "Visita link", + viewDocument: "Vedi documento", location: "Posizione", contentWarning: "Oggetto", + personName: "Nome abitante", bankWalletConnected: "Portafoglio ECOin", bankUbiReceived: "UBI ricevuto", bankTx: "Tx", @@ -1616,6 +1769,7 @@ module.exports = { link: "Link", activityProjectFollow: "%OASIS% ora sta %ACTION% questo progetto %PROJECT%", activityProjectUnfollow: "%OASIS% ora sta %ACTION% questo progetto %PROJECT%", + activityProjectPledged: "%OASIS% ha %ACTION% %AMOUNT% al progetto %PROJECT%", following: "SEGUENDO", unfollowing: "NON SEGUENDO", pledged: "CONTRIBUITO", @@ -1661,17 +1815,26 @@ module.exports = { courtsVotesSlashTotal: "SÌ / TOTALE", courtsOpenVote: "Apri votazione", courtsAnswerTitle: "Rispondi alla denuncia", + courtsStanceADMIT: "Ammetti", + courtsStanceDENY: "Nega", + courtsStancePARTIAL: "Parziale", + courtsStanceCOUNTERCLAIM: "Controdenuncia", + courtsStanceNEUTRAL: "Neutrale", courtsVerdictResult: "Risultato", courtsVerdictOrders: "Ordini", courtsSettlementText: "Condizioni", courtsSettlementAccepted: "Accettato", + courtsSettlementPending: "In attesa", courtsJudge: "Giudice", courtsThSupports: "Supporti", courtsFilterOpenCase: 'Apri caso', + courtsEvidenceFileLabel: 'File di prova (immagine, audio, video o PDF)', + courtsCaseMediators: 'Mediatori', courtsCaseMediatorsPh: 'Oasis ID dei mediatori, separati da virgola', courtsMediatorsLabel: 'Mediatori', courtsThCase: 'Caso', courtsThCreatedAt: 'Data di inizio', + courtsThActions: 'Azioni', courtsPublicPrefLabel: 'Visibilità dopo la risoluzione', courtsPublicPrefYes: 'Acconsento che questo caso sia completamente pubblico', courtsPublicPrefNo: 'Preferisco mantenere i dettagli privati', @@ -1679,11 +1842,8 @@ module.exports = { courtsMethodMEDIATION: 'Mediazione', courtsNoCases: 'Nessun caso.', reportsTitle: "Rapporti", - reportsDescription: "Gestisci e segui le segnalazioni della tua rete.", - reportsSearchPlaceholder: "Cerca segnalazioni...", + reportsDescription: "Gestisci e monitora le segnalazioni relative a problemi, bug, abusi e avvisi sui contenuti nella tua rete.", reportsFilterAll: "TUTTI", - reportsFilterRecent: "RECENTI", - reportsFilterTop: "TOP", reportsFilterMine: "MIEI", reportsFilterFeatures: "FUNZIONALITÀ", reportsFilterBugs: "BUG", @@ -1696,13 +1856,18 @@ module.exports = { reportsFilterInvalid: "NON VALIDI", reportsCreateButton: "Crea segnalazione", reportsTitleLabel: "Titolo", + reportsDescriptionLabel: "Descrizione", + reportsDescriptionPlaceholder: "Fornisci una descrizione dettagliata.", reportsCategory: "Categoria", + reportsCategoryLabel: "Categoria", reportsCategoryFeatures: "Funzionalità", reportsCategoryBugs: "Bug", reportsCategoryAbuse: "Abuso", - reportsCategoryContent: "Contenuto", + reportsCategoryContent: "Problemi di contenuto", reportsUpdateButton: "Aggiorna", reportsDeleteButton: "Elimina", + reportsDateLabel: "Data", + reportsUploadFile: "Carica media (max: 50MB)", reportsMineSectionTitle: "Le tue segnalazioni", reportsFeaturesSectionTitle: "Richieste di funzionalità", reportsBugsSectionTitle: "Bug", @@ -1710,6 +1875,11 @@ module.exports = { reportsContentSectionTitle: "Problemi di contenuto", reportsAllSectionTitle: "Rapporti", reportsNoItems: "Nessuna segnalazione disponibile.", + reportsValidationTitle: "Inserisci un titolo valido.", + reportsValidationDescription: "La descrizione non può essere vuota.", + reportsValidationCategory: "Seleziona una categoria.", + reportsCreatedAt: "Creato il", + reportsCreatedBy: "Di", reportsSeverity: "Gravità", reportsSeverityLow: "Bassa", reportsSeverityMedium: "Media", @@ -1720,10 +1890,13 @@ module.exports = { reportsStatusUnderReview: "In revisione", reportsStatusResolved: "Risolto", reportsStatusInvalid: "Non valido", + reportsUpdateStatusButton: "Aggiorna stato", + reportsAnonymityOption: "Invia in modo anonimo", + reportsAnonymousAuthor: "Anonimo", reportsConfirmButton: "CONFERMA SEGNALAZIONE!", reportsConfirmations: "Conferme", reportsConfirmedSectionTitle: "Segnalazioni confermate", - reportsCreateTaskButton: "Crea Compito", + reportsCreateTaskButton: "CREA COMPITO", reportsOpenSectionTitle: "Segnalazioni aperte", reportsUnderReviewSectionTitle: "Segnalazioni in revisione", reportsResolvedSectionTitle: "Segnalazioni risolte", @@ -1767,20 +1940,6 @@ module.exports = { reportsRequestedActionLabel: 'Azione richiesta', reportsRequestedActionPlaceholder: 'Rimuovi, nascondi, tagga, avvisa, ecc.', tribesTitle: "Tribù", - tribeFilterAll: "TUTTE", - tribeFilterRecent: "RECENTI", - tribeFilterMine: "MIE", - tribeFilterMembership: "ADESIONE", - tribeFilterSubtribes: "SOTTOTRIBÙ", - tribeFilterTop: "TOP", - tribeFilterGallery: "GALLERIA", - tribeFeedFilterTOP: "TOP", - tribeFeedFilterMINE: "MIEI", - tribeFeedFilterALL: "TUTTI", - tribeFeedFilterRECENT: "RECENTI", - courtsStanceDENY: "Nega", - courtsStanceADMIT: "Ammetti", - courtsStancePARTIAL: "Ammetti in parte", tribeAllSectionTitle: "Tribù", tribeMineSectionTitle: "Le tue tribù", tribeCreateSectionTitle: "Crea tribù", @@ -1792,6 +1951,13 @@ module.exports = { tribeviewSubTribeButton: "Visita Sotto-Tribù", tribeRootLabel: "RADICE", tribeDescription: "Esplora o crea tribù nella tua rete.", + tribeFilterAll: "TUTTE", + tribeFilterMine: "MIE", + tribeFilterMembership: "ISCRIZIONE", + tribeFilterRecent: "RECENTI", + tribeFilterTop: "TOP", + tribeFilterSubtribes: "SOTTO-TRIBÙ", + tribeFilterGallery: "GALLERIA", tribeMainTribeLabel: "TRIBÙ PRINCIPALE", tribeCreateButton: "Crea tribù", tribeUpdateButton: "Aggiorna", @@ -1810,8 +1976,13 @@ module.exports = { tribeModeLabel: "MODE", tribeIsAnonymousLabel: "STATO", tribeMembersCount: "Membri", + tribeInviteCodePlaceholder: "Inserisci codice invito", + tribeJoinByCodeButton: "Unisciti con codice", + tribeJoinButton: "UNISCITI", tribeEnterInvite: "INSERISCI CODICE", tribeLeaveButton: "ESCI", + tribeYes: "SÌ", + tribeNo: "NO", tribePublic: "PUBBLICO", tribePrivate: "PRIVATO", tribeGenerateInvite: "INVITO SINGOLO", @@ -1820,8 +1991,13 @@ module.exports = { tribeRemoveInvitation: "RIMUOVI INVITO", tribeCreatedAt: "Creato il", tribeAuthor: "Di", + tribeAuthorLabel: "AUTORE", tribeStrict: "Rigoroso", tribeOpen: "Aperto", + tribeFeedFilterRECENT: "RECENTI", + tribeFeedFilterMINE: "MIEI", + tribeFeedFilterALL: "TUTTI", + tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Ricondivisioni", tribeFeedRefeed: "Ricondividi", tribeFeedMessagePlaceholder: "Scrivi un feed…", @@ -1831,12 +2007,17 @@ module.exports = { tribeNotFound: "Tribù non trovata!", createTribeTitle: "Crea tribù", updateTribeTitle: "Aggiorna tribù", + tribeSectionOverview: "Panoramica", tribeSectionInhabitants: "Abitanti", tribeSectionVotations: "VOTAZIONI", tribeSectionEvents: "EVENTI", + tribeSectionReports: "Segnalazioni", tribeSectionTasks: "COMPITI", tribeSectionFeed: "FEED", tribeSectionForum: "FORUM", + tribeSectionMarket: "Mercato", + tribeSectionJobs: "Lavori", + tribeSectionProjects: "Progetti", tribeSectionMedia: "Media", tribeSectionImages: "IMMAGINI", tribeSectionAudios: "AUDIO", @@ -1874,6 +2055,11 @@ module.exports = { tribeTaskStatusClosed: "CHIUDI", tribeTaskAssign: "ASSEGNA", tribeTaskUnassign: "RIMUOVI", + tribeReportCreate: "Crea segnalazione", + tribeReportsEmpty: "Nessuna segnalazione.", + tribeReportTitle: "Titolo", + tribeReportDescription: "Descrizione", + tribeReportCategory: "Categoria", tribeVotationCreate: "Crea votazione", tribeVotationsEmpty: "Nessuna votazione.", tribeVotationTitle: "Titolo", @@ -1891,6 +2077,27 @@ module.exports = { tribeForumCategory: "Categoria", tribeForumReply: "Rispondi", tribeForumReplies: "Risposte", + tribeMarketCreate: "Crea annuncio", + tribeMarketEmpty: "Nessun annuncio.", + tribeMarketTitle: "Titolo", + tribeMarketDescription: "Descrizione", + tribeMarketPrice: "Prezzo", + tribeMarketImage: "Immagine", + tribeMarketCategory: "Categoria", + tribeJobCreate: "Crea lavoro", + tribeJobsEmpty: "Nessun lavoro.", + tribeJobTitle: "Titolo", + tribeJobDescription: "Descrizione", + tribeJobLocation: "Posizione", + tribeJobSalary: "Stipendio", + tribeJobDeadline: "Scadenza", + tribeProjectCreate: "Crea progetto", + tribeProjectsEmpty: "Nessun progetto.", + tribeProjectTitle: "Titolo", + tribeProjectDescription: "Descrizione", + tribeProjectGoal: "Obiettivo", + tribeProjectFunded: "Finanziato", + tribeProjectDeadline: "Scadenza", tribeMediaUpload: "Carica media", readDocument: "Leggi Documento", tribeCreateImage: "Crea Immagine", @@ -1901,6 +2108,7 @@ module.exports = { tribeMediaEmpty: "Nessun media.", tribeMediaTitle: "Titolo", tribeMediaDescription: "Descrizione", + tribeMediaType: "Tipo", tribeMediaTypeImage: "Immagine", tribeMediaTypeVideo: "Video", tribeMediaTypeAudio: "Audio", @@ -1910,6 +2118,11 @@ module.exports = { tribeInviteCodeText: "Codice invito: ", oasisVersionLabel: "Versione di Oasis", tribeInviteCodeHint: "Condividi questo codice con chi vuoi invitare. Può unirsi tramite /invites → Tribes.", + tribeGroupTribe: "Tribù", + tribeGroupOffice: "Ufficio", + tribeGroupNetwork: "Rete", + tribeGroupEconomy: "Economia", + tribeGroupMedia: "Media", tribeStatusOpen: "APERTO", tribeStatusClosed: "CHIUSO", tribeStatusInProgress: "IN CORSO", @@ -1919,17 +2132,33 @@ module.exports = { tribePriorityCritical: "CRITICA", tribeTaskFilterAll: "TUTTI", tribeMediaFilterAll: "TUTTI", + tribeReportCatBug: "BUG", + tribeReportCatAbuse: "ABUSO", + tribeReportCatContent: "CONTENUTO", + tribeReportCatOther: "ALTRO", tribeForumCatGeneral: "GENERALE", tribeForumCatProposal: "PROPOSTA", tribeForumCatQuestion: "DOMANDA", tribeForumCatAnnouncement: "ANNUNCIO", + tribeMarketCatGoods: "BENI", + tribeMarketCatServices: "SERVIZI", + tribeMarketCatFood: "CIBO", + tribeMarketCatOther: "ALTRO", tribeStatusLabel: "Stato", tribeSubTribes: "SOTTO-TRIBÙ", tribeSubTribesCreate: "Crea Sotto-Tribù", tribeSubTribesStrictDenied: "La modalità rigida della tribù non ti consente di creare nuove sotto-tribù. Si prega di contattare l'amministratore.", + tribeSubTribesEmpty: "Nessuna sotto-tribù creata.", + tribeActivityJoined: "ISCRITTO", + tribeActivityLeft: "USCITO", + tribeActivityFeed: "FEED", tribeActivityRefeed: "RICONDIVISIONE", + tribeGroupAnalytics: "Analisi", + tribeGroupCreative: "Creativo", tribeSectionActivity: "ATTIVITÀ", tribeSectionTrending: "TENDENZE", + tribeSectionOpinions: "OPINIONI", + tribeSectionPixelia: "PIXELIA", tribeSectionTags: "TAG", tribeSectionSearch: "RICERCA", tribeActivityEmpty: "Nessuna attività.", @@ -1941,15 +2170,25 @@ module.exports = { tribeTrendingPeriodWeek: "Questa settimana", tribeTrendingPeriodAll: "Sempre", tribeTrendingEngagement: "coinvolgimento", + tribeOpinionsEmpty: "Nessuna opinione.", + tribeOpinionsCast: "Vota", + tribeOpinionsRankings: "Classifiche", + tribeOpinionsAlreadyVoted: "Già votato", tribeTopCategory: "Più votato", + tribePixeliaTitle: "Pixelia", + tribePixeliaDescription: "Tela collaborativa di pixel art.", + tribePixeliaPaint: "Dipingi", + tribePixeliaContributors: "contributori", + tribePixeliaTotalPixels: "pixel dipinti", tribeTagsEmpty: "Nessun tag trovato.", + tribeTagsCloud: "Nuvola di tag", + tribeTagsContentWith: "Contenuto con tag", tribeSearchPlaceholder: "Cerca nei contenuti della tribù...", tribeSearchEmpty: "Nessun risultato trovato.", tribeSearchResults: "Risultati", tribeSearchMinChars: "Inserisci almeno 2 caratteri per cercare.", agendaTitle: "Agenda", agendaDescription: "Qui trovi tutti gli elementi assegnati a te.", - agendaSearchPlaceholder: "Cerca nell'agenda...", agendaFilterAll: "TUTTI", agendaFilterToday: "OGGI", agendaFilterUpcoming: "PROSSIMI", @@ -1963,14 +2202,23 @@ module.exports = { agendaFilterReports: "SEGNALAZIONI", agendaFilterTransfers: "TRASFERIMENTI", agendaFilterJobs: "LAVORI", - agendaFilterIndustry: "INDUSTRIA", - agendaFilterHousing: "ALLOGGI", agendaFilterProjects: "PROGETTI", agendaFilterCalendars: "CALENDARI", agendaNoItems: "Nessun incarico trovato.", + agendaAuthor: "Di", agendaDiscardButton: "Scarta", agendaRestoreButton: "Ripristina", + agendaCreatedAt: "Creato il", + agendaTitleLabel: "Titolo", agendaMembersCount: "Membri", + agendaDescriptionLabel: "Descrizione", + agendaStatus: "Stato", + agendaVisibility: "Visibilità", + agendaEventDate: "Data evento", + agendaEventLocation: "Posizione", + agendaEventPrice: "Prezzo", + agendaEventUrl: "URL", + agendaTaskStart: "Ora di inizio", agendaLocationLabel: "Posizione", agendaYes: "SÌ", agendaNo: "NO", @@ -1980,6 +2228,11 @@ module.exports = { agendareportCategory: "Categoria", agendareportSeverity: "Gravità", agendareportStatus: "Stato", + agendareportDescription: "Descrizione", + agendaTaskEnd: "Ora di fine", + agendaTaskPriority: "Priorità", + agendaTransferFrom: "Da", + agendaTransferTo: "A", agendaTransferConcept: "Concetto", agendaTransferAmount: "Importo", agendaTransferDeadline: "Scadenza", @@ -1989,7 +2242,47 @@ module.exports = { voteNow: "Vota ora", alreadyVoted: "Hai già espresso un'opinione.", noOpinionsFound: "Nessuna opinione trovata.", + RECENTButton: "RECENT", TOPButton: "TOP", + interestingButton: "INTERESSANTE", + necessaryButton: "NECESSARIO", + funnyButton: "DIVERTENTE", + disgustingButton: "DISGUSTOSO", + sensibleButton: "SENSIBILE", + propagandaButton: "PROPAGANDA", + adultOnlyButton: "SOLO ADULTI", + boringButton: "NOIOSO", + confusingButton: "CONFUSO", + usefulButton: "UTILE", + informativeButton: "INFORMATIVO", + wellResearchedButton: "BEN DOCUMENTATO", + accurateButton: "ACCURATO", + needsSourcesButton: "SERVONO FONTI", + wrongButton: "SBAGLIATO", + lowQualityButton: "BASSA QUALITÀ", + creativeButton: "CREATIVO", + insightfulButton: "PERSPICACE", + actionableButton: "ATTUABILE", + inspiringButton: "ISPIRANTE", + loveButton: "AMORE", + clearButton: "CHIARO", + upliftingButton: "EDIFICANTE", + unnecessaryButton: "INUTILE", + rejectedButton: "RIFIUTATO", + misleadingButton: "FUORVIANTE", + offTopicButton: "FUORI TEMA", + duplicateButton: "DUPLICATO", + clickbaitButton: "CLICKBAIT", + spamButton: "SPAM", + trollButton: "TROLL", + nsfwButton: "NSFW", + violentButton: "VIOLENTO", + toxicButton: "TOSSICO", + harassmentButton: "MOLESTIA", + hateButton: "ODIO", + scamButton: "TRUFFA", + triggeringButton: "DISTURBANTE", + opinionsCreatedAt: "Creato il", opinionsTotalCount: "Opinioni totali", voteInteresting: "Interessante", voteNecessary: "Necessario", @@ -2026,39 +2319,27 @@ module.exports = { voteCreative: "Creativo", voteSpam: "Spam", voteAdultOnly: "Solo adulti", + publishBlog: "Pubblica post", privateMessage: "PM", pmSendTitle: "Messaggi privati", pmSend: "Invia!", - pmCancel: "Annulla", - pmReset: "Reimposta", - pmSharedKeyHint: "Condividi questa chiave con il destinatario tramite un altro canale. È necessaria per decifrare.", - pmDescription: "Usa questo modulo per inviare un messaggio/file cifrato ad altri abitanti.", - pmComposeTitle: "Invia un messaggio", + pmDescription: "Usa questo modulo per inviare un messaggio crittografato ad altri abitanti.", pmRecipients: "Destinatari", pmRecipientsHint: "Inserisci Oasis ID separati da virgole", - pmLimitsHint: "Fino a 7 destinatari per messaggio.", - pmTextPlaceholder: "Corpo limitato a 7000 caratteri.", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "Oggetto", pmSubjectHint: "Inserisci l'oggetto del messaggio", pmText: "Messaggio", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Decifra", - fileShareTitle: "Condividi un file", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "File", - fileShareDownload: "Scarica", - fileShareMutualError: "Puoi condividere file solo con abitanti con supporto reciproco.", - fileShareNoFile: "Nessun file selezionato.", - fileShareTooLarge: "Il file supera la dimensione consentita.", - fileShareFailed: "Impossibile preparare il file.", - fileShareSendError: "Impossibile inviare il file.", - fileShareUnavailable: "Questo file non è disponibile al momento. Riprova più tardi.", pmCrypterBadKey: "La chiave condivisa è errata!", pmCrypterTooLong: "Il messaggio è troppo lungo per essere cifrato con il Crypter. Accorcialo e riprova.", pmCrypterCipherLabel: "Messaggio ricifrato", pmCrypterCharsLabel: "caratteri", pmInvalidRecipients: "Oasis ID non valido (dovrebbe essere tipo @…=.ed25519).", pmEncryptionLabel: "Cifratura", + pmFile: "Allegato", private: "Privato", privateDescription: "I tuoi messaggi crittografati.", privateInbox: "POSTA IN ARRIVO", @@ -2068,27 +2349,19 @@ module.exports = { noPrivateMessages: "Nessun messaggio privato.", pmFromLabel: "Da:", pmToLabel: "A:", + pmInvalidMessage: "Messaggio non valido", pmNoSubject: "(senza oggetto)", pmSubjectLabel: "Oggetto:", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "La casa al governo del L.A.R.P è cambiata.", - politicalBotParliamentTitle: "È iniziato un nuovo ciclo di governo nel Parlamento.", - politicalBotTribeTitle: "È iniziato un nuovo ciclo di governo in una delle tue Tribù.", - politicalBotLarpIntro: "Ora governa {house} durante questo ciclo: {cycle}", - politicalBotParliamentIntro: "Il governo attuale è {gov}", - politicalBotParliamentIntroLeader: "Il governo attuale è {gov}, esercitato da {leader}", - politicalBotTribeIntro: "Il governo attuale nella Tribù {tribe} è {gov}", - politicalBotTribeIntroLeader: "Il governo attuale nella Tribù {tribe} è {gov}, esercitato da {leader}", - politicalBotLeaderLabel: "Leader", + pmBodyLabel: "Messaggio", + pmBotJobs: "42-LavoriBOT", + pmBotProjects: "42-ProgettiBOT", + pmBotMarket: "42-MercatoBOT", inboxJobSubscribedTitle: "Nuova iscrizione alla tua offerta di lavoro", pmInhabitantWithId: "Abitante con Oasis ID:", pmHasSubscribedToYourJobOffer: "si è iscritto alla tua offerta di lavoro", + inboxProjectCreatedTitle: "Nuovo progetto creato", + pmHasCreatedAProject: "ha creato un progetto", inboxMarketItemSoldTitle: "Articolo venduto", - inboxShopSoldTitle: "Prodotto venduto", pmYourItem: "Il tuo articolo", pmHasBeenSoldTo: "è stato venduto a", pmFor: "per", @@ -2096,6 +2369,7 @@ module.exports = { pmHasPledged: "ha contribuito", pmToYourProject: "al tuo progetto", blockchain: 'BlockExplorer', + blockchainTitle: 'BlockExplorer', blockchainDescription: 'Esplora e visualizza i blocchi nella blockchain.', blockchainNoBlocks: 'Nessun blocco trovato nella blockchain.', blockchainStatsTitle: 'Statistiche', @@ -2107,36 +2381,42 @@ module.exports = { blockchainBlockCarbon: 'Impronta di carbonio', blockchainBlockType: 'Tipo', blockchainBlockTimestamp: 'Data e ora', + blockchainBlockContent: 'Blocco', + blockchainBlockURL: 'URL:', + blockchainContent: 'Blocco', + blockchainContentPreview: 'Anteprima del contenuto del blocco', blockchainLatestDatagram: 'Ultimo Datagramma', - blockchainDatagram: "Datagramma", - blockchainDetails: "Vedi i Dettagli del Blocco", - blockchainViewBlockexplorer: "Vedi nel Blockexplorer", + blockchainDatagram: 'Datagramma', + blockchainDetails: 'Vedi dettagli blocco', + blockchainViewBlockexplorer: "Vedi nel blockexplorer", + blockchainBlockInfo: 'Informazioni blocco', + blockchainBlockDetails: 'Dettagli del blocco selezionato', blockchainBack: 'Torna al BlockExplorer', blockchainContentDeleted: "Questo contenuto è stato rimosso (tombstone)", - visitContent: "Visita il Contenuto", - favoriteAdd: "Aggiungi ai Preferiti", - pmContentTooltip: "Invia un Messaggio Privato", - favoriteRemove: "Togli dai Preferiti", - reportContent: "Segnala il Contenuto", + visitContent: "Visita contenuto", banking: 'Banking', bankingTitle: 'Banking', - bankingDescription: "L'economia ECOin: saldo, reddito di base, tasse e valore dell'industria.", + bankingDescription: 'Esplora il valore attuale di ECOin e la corrispondente allocazione UBI, distribuita per epoca in base a partecipazione e fiducia.', bankOverview: 'Panoramica', bankEpochs: 'Epoche', bankRules: 'Regole', pending: 'In attesa', closed: 'Chiuso', + bankBack: 'Torna alla Banca', bankViewTx: 'Vedi Tx', bankClaimNow: 'Riscuoti ora', bankClaimUBI: 'Richiedi RBU!', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: 'Nessuna assegnazione RBU in sospeso per questa epoca.', bankEpoch: 'Epoca', bankPool: 'Pool (questa epoca)', bankWeightsSum: 'Somma dei pesi', + bankAllocations: 'Allocazioni', bankNoAllocations: 'Nessuna allocazione trovata.', bankNoEpochs: 'Nessuna epoca trovata.', bankEpochAllocations: 'Allocazioni epoca', @@ -2150,11 +2430,10 @@ module.exports = { bankEpochId: 'ID epoca', bankRuleHash: 'Hash snapshot regole', bankViewEpoch: 'Vedi epoca', - bankYourIndustryBalance: "La tua quota industriale", - bankYourUbiMonth: "Il tuo RBU (questo mese)", - bankYourFundsMonth: "I tuoi fondi (questo mese)", - bankIndustryBalance: "Produzione industriale (stimata)", bankUserBalance: 'Il tuo saldo', + ecoWalletNotConfigured: 'Portafoglio ECOin non configurato', + editWallet: 'Modifica portafoglio', + addWallet: 'Aggiungi portafoglio', bankAddresses: 'Indirizzi', bankNoAddresses: 'Nessun indirizzo trovato.', bankUser: 'Oasis ID', @@ -2179,10 +2458,18 @@ module.exports = { search: 'Cerca!', bankLocal: 'Locale', bankFromOasis: 'Oasis', + bankMyAddress: 'Il tuo indirizzo', + bankRemoveMyAddress: 'Rimuovi il mio indirizzo', + bankNotRemovableOasis: 'Gli indirizzi non possono essere rimossi localmente', + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "NESSUN FONDO!", bankUbiAvailableOk: "DISPONIBILE!", - bankUbiAvailability: "RBU (rete)", + bankUbiAvailability: "Disponibilità UBI", bankAlreadyClaimedThisMonth: "Già richiesto questo mese", bankUbiThisMonth: "RBU (questo mese)", bankUbiLastClaimed: "RBU (ultima richiesta)", @@ -2196,6 +2483,13 @@ module.exports = { shopsTitle: "Negozi", shopDescription: "Scopri e gestisci negozi nella rete.", shopTitle: "Negozio", + shopFilterAll: "TUTTI", + shopFilterMine: "MIEI", + shopFilterRecent: "RECENTI", + shopFilterTop: "TOP", + shopFilterProducts: "PRODOTTI", + shopFilterPrices: "PREZZI", + shopFilterFavorites: "PREFERITI", shopUpload: "Crea Negozio", shopAllSectionTitle: "Tutti i Negozi", shopMineSectionTitle: "I Miei Negozi", @@ -2212,10 +2506,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "Pubblica su Clearnet", + shopClearnetUnpublish: "Rimuovi dal Clearnet", + shopClearnetUrlLabel: "URL Clearnet", + shopClearnetWarning: "Pubblicare su Clearnet rende questo negozio indicizzabile da motori di ricerca e crawler esterni.", shopAddFavorite: "Aggiungi Preferito", shopRemoveFavorite: "Rimuovi Preferito", shopOpen: "APERTO", shopClosed: "CHIUSO", + shopOpenShop: "Apri Negozio", + shopCloseShop: "Chiudi Negozio", shopMakePrivate: "RENDI PRIVATO (E2E)", shopMakePublic: "RENDI PUBBLICO", shopProducts: "Prodotti", @@ -2243,6 +2543,8 @@ module.exports = { shopOrderPrice: "Prezzo", shopOrderBuyer: "Acquirente", shopBackToShop: "Torna al Negozio", + shopShareUrl: "URL di condivisione", + shopVisitShop: "VISITA NEGOZIO", marketVisitItem: "VEDI OGGETTO", shopStatus: "STATO", shopCreatedAt: "CREATO", @@ -2251,10 +2553,12 @@ module.exports = { shopLocation: "Posizione", shopTags: "Tag", shopVisibility: "Visibilità", + shopImage: "Immagine", shopShortDescription: "Descrizione Breve", shopShortDescriptionPlaceholder: "Descrizione breve per le schede (max 160 caratteri)", shopTitlePlaceholder: "Nome del tuo negozio", shopDescriptionPlaceholder: "Descrizione dettagliata del tuo negozio", + shopUrlPlaceholder: "https://url-del-tuo-negozio.com", shopLocationPlaceholder: "Città, Paese", shopTagsPlaceholder: "tag1, tag2, tag3", mapTitlePlaceholder: "Titolo della mappa", @@ -2272,6 +2576,7 @@ module.exports = { bankUnitMinutes: "minuti", bankUnitDays: "giorni", bankExchangeNoData: 'Nessun dato disponibile', + bankExchangeIndex: 'Valore ECOin (1h)', bankInflation: 'Inflazione ECOin (anual)', bankInflationMonthly: 'Inflazione ECOin (mensual)', bankExchangeChartTitle: 'Progressione del valore ECOin nel tempo', @@ -2285,17 +2590,38 @@ module.exports = { bankingSyncStatusOutdated: 'Non aggiornato', statsTitle: 'Statistiche', statistics: "Statistiche", + statsInhabitant: "Statistiche abitante", statsDescription: "Scopri le statistiche della tua rete.", ALLButton: "ALL", MINEButton: "MINE", TOMBSTONEButton: "TOMBSTONES", + statsYou: "Tu", + statsUserId: "Oasis ID", statsCreatedAt: "Creato il", statsYourContent: "Contenuto", statsYourOpinions: "Opinioni", + statsYourTombstone: "Tombstone", + statsNetwork: "Rete", + statsTotalInhabitants: "Abitanti", + statsDiscoveredTribes: "Tribù (Pubbliche)", + statsPrivateDiscoveredTribes: "Tribù (Private)", statsNetworkContent: "Contenuto", + statsYourMarket: "Mercato", + statsYourJob: "Lavori", + statsYourProject: "Progetti", + statsYourTransfer: "Trasferimenti", + statsYourForum: "Forum", statsNetworkOpinions: "Opinioni", + statsDiscoveredMarket: "Mercato", + statsDiscoveredJob: "Lavori", + statsDiscoveredProject: "Progetti", + statsBankingTitle: "Banca", statsEcoWalletLabel: "Portafoglio ECOIN", statsEcoWalletNotConfigured: "Non configurato!", + statsTotalEcoAddresses: "Indirizzi totali", + statsDiscoveredTransfer: "Trasferimenti", + statsDiscoveredForum: "Forum", + statsNetworkTombstone: "Tombstone", statsBookmark: "Segnalibri", statsEvent: "Eventi", statsTask: "Compiti", @@ -2317,12 +2643,16 @@ module.exports = { statsAiExchange: "AI", statsPUBs: 'PUBs', statsPost: "Post", + statsOasisID: "Oasis ID", statsSize: "Totale (dimensione)", statsBlockchainSize: "Blockchain (dimensione)", statsBlobsSize: "Blob (dimensione)", statsActivity7d: "Attività (ultimi 7 giorni)", statsActivity7dTotal: "Totale 7 giorni", statsActivity30dTotal: "Totale 30 giorni", + statsKarmaScore: "Punteggio KARMA", + statsPublic: "Pubblico", + statsPrivate: "Privato", day: "Giorno", messages: "Messaggi", statsProject: "Progetti", @@ -2334,14 +2664,30 @@ module.exports = { statsProjectsCancelled: "Annullato", statsProjectsGoalTotal: "Obiettivo totale", statsProjectsPledgedTotal: "Contribuito totale", + statsProjectsSuccessRate: "Tasso di successo", + statsProjectsAvgProgress: "Progresso medio", + statsProjectsMedianProgress: "Progresso mediano", + statsProjectsActiveFundingAvg: "Finanziamento attivo medio", + statsJobsTitle: "Lavori", + statsJobsTotal: "Lavori totali", + statsJobsOpen: "Aperti", + statsJobsClosed: "Chiusi", + statsJobsOpenVacants: "Posti vacanti aperti", + statsJobsSubscribersTotal: "Iscritti totali", + statsJobsAvgSalary: "Stipendio medio", + statsJobsMedianSalary: "Stipendio mediano", statsMarketTitle: "Mercato", statsMarketTotal: "Articoli totali", statsMarketForSale: "In vendita", statsMarketReserved: "Riservato", statsMarketClosed: "Chiuso", statsMarketSold: "Venduto", + statsMarketRevenue: "Entrate", + statsMarketAvgSoldPrice: "Prezzo medio venduto", statsUsersTitle: "Abitanti", user: "Abitante", + statsTombstoneTitle: "Tombstone", + statsNetworkTombstones: "Tombstone della rete", statsTombstoneRatio: "Rapporto tombstone (%)", statsParliamentCandidature: "Candidature parlamento", statsParliamentTerm: "Mandati parlamento", @@ -2368,21 +2714,30 @@ module.exports = { aiConfiguration: "Imposta prompt", aiPromptUsed: "Prompt", aiClearHistory: "Cancella cronologia chat", + aiSharePrompt: "Aggiungere questa risposta all'addestramento collettivo?", + aiShareYes: "Sì", + aiShareNo: "No", + aiSharedLabel: "Aggiunto all'addestramento", + aiRejectedLabel: "Non aggiunto all'addestramento", aiServerError: "L'AI non ha potuto rispondere. Riprova.", aiInputPlaceholder: "Cos'è Oasis?", typeAiExchange: "AI", aiApproveTrain: "Aggiungi all'addestramento collettivo", aiRejectTrain: "Non addestrare", + aiTrainPending: "In attesa di approvazione", aiTrainApproved: "Approvato per l'addestramento", aiTrainRejected: "Rifiutato per l'addestramento", aiSnippetsUsed: "Snippet utilizzati", aiSnippetsLearned: "Snippet appresi", statsAITraining: "Addestramento AI", + aiApproveCustomTrain: "Addestra usando questa risposta personalizzata", aiCustomAnswerPlaceholder: "Scrivi la tua risposta personalizzata…", + statsAIExchanges: "Scambi modello", marketMineSectionTitle: "I tuoi articoli", marketCreateSectionTitle: "Crea articolo", marketUpdateSectionTitle: "Aggiorna", marketAllSectionTitle: "Mercato", + marketRecentSectionTitle: "Mercato recente", marketTitle: "Mercato", marketDescription: "Un mercato per scambiare beni o servizi nella tua rete.", marketFilterAll: "TUTTI", @@ -2412,11 +2767,14 @@ module.exports = { marketItemDeadline: "Scadenza", marketItemIncludesShipping: "Spedizione inclusa?", marketItemHighestBid: "Offerta più alta", + marketItemHighestBidder: "Miglior offerente", marketItemStock: "Disponibilità", marketOutOfStock: "Esaurito", + marketItemBidTime: "Ora dell'offerta", marketActionsUpdate: "Aggiorna", marketUpdateButton: "Aggiorna", marketActionsDelete: "Elimina", + marketActionsSold: "Segna come venduto", marketActionsChangeStatus: "CAMBIA STATO", marketActionsBuy: "COMPRA!", marketAuctionBids: "Offerte attuali", @@ -2425,82 +2783,19 @@ module.exports = { marketNoItems: "Nessun articolo disponibile ancora.", marketYourBid: "La tua offerta", marketCreateFormImageLabel: "Carica media (max: 50MB)", + marketSearchLabel: "Cerca", marketSearchPlaceholder: "Cerca titolo o tag", marketMinPriceLabel: "Prezzo minimo", marketMaxPriceLabel: "Prezzo massimo", + marketSortLabel: "Ordina per", marketSortRecent: "Più recenti", marketSortPrice: "Prezzo", marketSortDeadline: "Scadenza", marketSearchButton: "Cerca", marketAuctionEndsIn: "Termina", marketAuctionEnded: "Terminata", + marketMyBidBadge: "Hai offerto", marketNoItemsMatch: "Nessun articolo corrisponde alla ricerca.", - housingTitle: "Alloggi", - housingDescriptionText: "Scopri e gestisci luoghi nella tua rete.", - modulesHousingLabel: "Alloggi", - modulesHousingDescription: "Modulo per scoprire e gestire luoghi.", - typeHousing: "ALLOGGI", - housingFilterAll: "TUTTI", - housingFilterMine: "MIEI", - housingFilterRequested: "RICHIESTI", - housingFilterSale: "VENDITA", - housingFilterRent: "AFFITTO", - housingFilterCouchsurfing: "COUCHSURFING", - housingFilterOpen: "DISPONIBILI", - housingFilterClosed: "NON DISPONIBILI", - housingFilterRecent: "RECENTI", - housingFilterTop: "TOP", - housingType: "Offerta", - housingTypeSALE: "In vendita", - housingTypeRENT: "In affitto", - housingTypeCOUCHSURFING: "Couchsurfing", - housingProperty: "Immobile", - housingPropertyAPARTMENT: "Appartamento", - housingPropertyHOUSE: "Casa", - housingPropertyROOM: "Stanza", - housingPropertyLAND: "Terreno", - housingPropertyOTHER: "Altro", - housingTitleLabel: "Titolo", - housingTitlePlaceholder: "Nome del luogo", - housingDescription: "Descrizione", - housingDescriptionPlaceholder: "Descrivi il luogo, il quartiere, cosa è incluso: bagni, giardino, letti…", - housingRules: "Regole", - housingRulesPlaceholder: "Animali, fumo, orari di silenzio, soggiorno minimo…", - housingPlace: "Luogo", - housingPlacePlaceholder: "Paese, città, indirizzo", - housingPrice: "Prezzo", - housingPriceLabel: "Prezzo (ECO)", - housingSizeLabel: "Superficie (m²)", - housingFree: "GRATIS", - housingPerMonth: "/mese", - housingRooms: "Stanze", - housingSize: "Superficie", - housingCapacity: "Ospiti", - housingAvailableFrom: "Disponibile dal", - housingAvailableTo: "Disponibile fino al", - housingTags: "Etichette", - spreadEditWarning: "Questo contenuto perderà gli spread dopo la modifica", - housingRequests: "Richieste", - housingRequestButton: "Richiedi", - housingCancelButton: "Annulla richiesta", - housingRequestedBadge: "RICHIESTO", - housingStatus: "Stato", - housingStatusOPEN: "DISPONIBILE", - housingStatusCLOSED: "NON DISPONIBILE", - housingSetOpen: "Rendi disponibile", - housingSetClosed: "Rendi non disponibile", - housingCreateButton: "Crea luogo", - housingUpdateButton: "Aggiorna", - housingDeleteButton: "Elimina", - housingSearchPlaceholder: "Cerca luoghi...", - housingSearchButton: "Cerca", - housingMinPrice: "Prezzo min", - housingMaxPrice: "Prezzo max", - housingSortRecent: "Più recenti", - housingSortPrice: "Prezzo più basso", - housingSortRequests: "Più richiesti", - housingSortRating: "Più votati", - housingNoItems: "Nessun luogo trovato.", jobsTitle: "Lavori", jobsCandidatesTitle: "Candidati suggeriti", jobsCandidatesDescription: "Abitanti le cui competenze corrispondono a questo lavoro. Inviate un messaggio privato per invitarli.", @@ -2548,6 +2843,7 @@ module.exports = { jobLocationPresencial: "In sede", jobLocationRemote: "Da remoto", jobVacantsPlaceholder: "Numero di posizioni", + jobSalaryPlaceholder: "Stipendio in ECO per 1 ora dedicata", jobImage: "Carica media (max: 50MB)", jobTasks: "Compiti", jobType: "Tipo lavoro", @@ -2557,6 +2853,7 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Lavori con stipendio più alto", createJobButton: "Pubblica lavoro", + viewDetailsButton: "Vedi dettagli", noJobsFound: "Nessuna offerta di lavoro trovata.", jobAuthor: "Di", jobTypeFreelance: "Freelance", @@ -2568,6 +2865,10 @@ module.exports = { jobsHoursOffered: "Ore offerte", jobsHoursRequested: "Ore richieste in cambio", jobsExchangeSkill: "Competenza richiesta in cambio", + jobsHoursOfferedPlaceholder: "es. 4", + jobsHoursRequestedPlaceholder: "es. 4", + jobsExchangeSkillPlaceholder: "es. falegnameria, design", + jobExchangeHint: "Per i lavori in scambio di ore, compila i campi seguenti invece dello stipendio.", jobTimePartial: "Part-time", jobTimeComplete: "Full-time", jobsDeleteButton: "ELIMINA", @@ -2575,201 +2876,31 @@ module.exports = { jobsFilterApplied: "CANDIDATURE", jobsAppliedTitle: "Le mie candidature", jobsAppliedBadge: "Candidato", + jobsFilterFavs: "Preferiti", + jobsFavsTitle: "Preferiti", + jobsFilterNeeds: "Serve aiuto", + jobsNeedsTitle: "Serve aiuto", + jobsSearchLabel: "Cerca", jobsSearchPlaceholder: "Cerca titolo, tag, descrizione...", jobsMinSalaryLabel: "Stipendio minimo", jobsMaxSalaryLabel: "Stipendio massimo", + jobsSortLabel: "Ordina per", jobsSortRecent: "Più recenti", jobsSortSalary: "Stipendio più alto", jobsSortSubscribers: "Più candidati", jobsSearchButton: "Cerca", + jobsFavoriteButton: "Preferito", + jobsUnfavoriteButton: "Rimuovi preferito", + jobsMessageAuthorButton: "PM", jobsApplicants: "Candidati", jobsUpdatedAt: "Aggiornato", jobSetOpen: "Segna come aperto", + jobNewBadge: "NUOVO", jobsTagsLabel: "Tag", + jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "Nessun lavoro corrisponde alla ricerca.", - industrySearchPlaceholder: "Cerca fabbriche…", - industryAllSectors: "Tutti i settori", - industryVisitButton: "Visita industria", - industryAdvanceTo: "Imposta a", - industryAmount: "Importo", - industryApproveBuild: "Approva", - industryBlueprint: "Blueprint", - industryBlueprints: "Blueprint", - industryBuild: "Build", - industryBuildNotes: "Note", - industryBuildStart: "Data di inizio", - industryVoteUpdateButton: "Vota aggiornamento", - industryVoteDeleteButton: "Vota eliminazione", - industryRevokeVote: "Revoca", - industryTimeLeft: "Tempo rimanente", - industryBuildEnd: "Data di fine", - industryBuilds: "Build", - industryBuildTitle: "Titolo", - industryContribute: "Contribuisci", - industryContributeButton: "Contribuisci", - industryContributions: "Contributi", - industryCreateBlueprintButton: "Crea blueprint", - industryDistribute: "Distribuisci", - industryDistributeButton: "Distribuisci per quote", - industryDistribution: "Distribuzione", - industryEcoAmount: "Importo (ECO)", - industryEditBlueprint: "Modifica blueprint", - industryFacility: "Fabbrica", - industryKindLabel: "Tipo", - industryKind_labor: "Lavoro", - industryKind_material: "Materiale", - industryKind_eco: "Fondi", - industryLaborHours: "Lavoro (ore)", - industryHours: "Ore", - industryMaterialItem: "Materiale", - industryMaterials: "Materiali", - industryEstMaterials: "Totale materiali", - industryEstLabor: "Totale lavoro", - industryEstTotal: "Prezzo stimato", - industryBuildingPrice: "Prezzo di costruzione", - industryFinalPrice: "Prezzo finale", - industryBlueprintDescPlaceholder: "Specifiche, dimensioni, peso, documentazione…", - industryMaterialValue: "Valore (ECO)", - industryMember: "Membro", - industryNewBlueprint: "Nuovo blueprint", - industryNewBuild: "Proponi un build", - industryEditBuild: "Modifica produzione", - industryNeedBlueprint: "Crea prima un progetto: ogni produzione ne fabbrica uno.", - industryNoBlueprints: "Ancora nessun blueprint.", - industryNoBuilds: "Ancora nessun build.", - industryNote: "Nota", - industryOutput: "Output", - industryOutputKind: "Tipo di prodotto", - industryOutputValue: "Valore di output (ECO, es. dalla vendita)", - industryPostJob: "Crea lavoro", - industryPot: "Monte", - industryProposeBuildButton: "Proponi build", - industryProposer: "Proponente", - industryAuthor: "Autore", - industrySellOnMarket: "Invia al mercato", - industrySendToProjects: "Crea progetto", - industrySkills: "Competenze", - industryTreasury: "Tesoreria", - industryKind_physical: "Fisico", - industryKind_digital: "Digitale", - industryBuildStatus_PROPOSED: "PROPOSTO", - industryBuildStatus_REJECTED: "RESPINTO", - industryBuildStatus_APPROVED: "APPROVATO", - industryBuildStatus_STOCKING: "APPROVVIGIONAMENTO", - industryBuildStatus_IN_PRODUCTION: "IN PRODUZIONE", - industryBuildStatus_COMPLETED: "COMPLETATO", - industryBuildStatus_FAILED: "FALLITO", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "Un lavoro corrisponde al tuo curriculum", - jobsBotMatchIntro: "Un lavoro corrisponde al tuo curriculum.", - jobsBotMatchJob: "Lavoro", - jobsBotMatchHint: "Ricevi questo perché il tuo curriculum è gestito dall'IA. Puoi disattivarlo nel tuo CV.", - cvAiManaged: "Gestito dall'IA", - switchOn: "ATTIVO", - switchOff: "DISATTIVO", - cvAiManagedHint: "Se attivo, JobsBot ti avvisa con un messaggio privato quando un lavoro corrisponde al tuo curriculum.", - cvMatchThreshold: "Avvisami da questo livello di corrispondenza", - housingBotRequestedTitle: "Qualcuno ha richiesto uno dei tuoi luoghi.", - housingBotCancelledTitle: "Una richiesta su uno dei tuoi luoghi è stata annullata.", - housingBotYouRequestedTitle: "Hai richiesto un luogo.", - housingBotYouCancelledTitle: "Hai annullato una richiesta.", - housingBotUnavailableTitle: "Un luogo che avevi richiesto non è più disponibile.", - housingBotUnavailableSentTitle: "Hai avvisato chi aveva richiesto questo luogo.", - industryBotAdmittedTitle: "Sei stato ammesso in una fabbrica.", - industryBotApplicationTitle: "Nuova richiesta di adesione nella tua fabbrica.", - industryBotInvitedTitle: "Sei stato invitato in una fabbrica.", - industryBotDissolvedTitle: "Una fabbrica a cui appartieni è stata sciolta.", - industryBotBuildApprovedTitle: "Un build è stato approvato.", - industryBotDistributedTitle: "L'output di un build è stato distribuito.", - industryFilterRules: "REGOLE", - industryRulesTitle: "Regole", - industryRulesIntro: "Industry permette agli abitanti di possedere collettivamente i mezzi di produzione: una Fabbrica è un'officina della rete che nessuno possiede privatamente.", - industryRulesSteward: "Il fondatore è l'amministratore — un custode, non un proprietario: non può vendere, privatizzare o sciogliere la Fabbrica da solo.", - industryRulesMembership: "La politica di adesione stabilisce come si entra: Aperta (subito), Per voto (ammessi dal voto dei membri) o Solo su invito (un membro deve invitare prima).", - industryRulesQuorum: "Il quorum è il numero minimo di membri che devono votare perché una decisione conti, così una piccola Fabbrica non è decisa da una sola persona.", - industryRulesMajority: "La maggioranza è la frazione di voti necessaria (0,5 = metà, 1 = unanimità); una decisione passa quando i SÌ raggiungono almeno max(quorum, membri × maggioranza).", - industryRulesDecisions: "I membri votano per ammettere nuovi arrivati, approvare i build e sciogliere la Fabbrica; l'amministratore non può scavalcare queste decisioni collettive.", - industryRulesBlueprints: "I Blueprint sono ricette libere (COPYLEFT) — gli input (materiali, ore di lavoro, competenze) e l'output (un bene fisico o digitale) — e chiunque può forkarle.", - industryRulesBuilds: "Un Build è un ordine di produzione: PROPOSTO → APPROVATO (per voto) → APPROVVIGIONAMENTO → IN PRODUZIONE → COMPLETATO, e accetta contributi solo una volta approvato.", - industryRulesContributions: "I membri contribuiscono con lavoro (ore), materiali (valore ECO dichiarato) o ECO a un build; ogni contributo guadagna karma.", - industryRulesLaborRate: "La tariffa di lavoro è il valore ECO di un'ora di lavoro in questa Fabbrica; converte le ore in karma per confrontare equamente lavoro e capitale: karma = ore × tariffa + valore materiali + ECO.", - industryRulesShares: "La tua quota di un build è il tuo karma ÷ karma totale, e stabilisce quanto del valore di output ricevi.", - industryRulesDistribution: "Quando una produzione è completata, lo steward distribuisce il monte (i fondi della produzione più il valore di vendita) tra i contributori, in proporzione alle loro quote.", - industryRulesTreasury: "I contributi in ECO sono custoditi dall'amministratore come tesoreria del build fino alla distribuzione; tutti i movimenti sono registrati sulla rete e le dispute possono andare alle Courts.", - industryJobs: "Lavori", - industryReportButton: "Segnala", - industryCreateTask: "Crea attività", - industryOpenDispute: "Apri controversia", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "Genera invito", - industryPauseVotes: "Voti di stato", - industryTitle: "Industria", - industryDescription: "Modulo per gestire collettivamente i mezzi di produzione.", - typeIndustryBuild: "BUILD", - typeIndustryBlueprint: "BLUEPRINT", - typeIndustryAllocation: "DISTRIBUZIONE", - typeIndustry: "INDUSTRIA", - modulesIndustryLabel: "Industria", - modulesIndustryDescription: "Modulo per gestire collettivamente i mezzi di produzione.", - industryFilterAll: "TUTTE", - industryFilterMember: "MEMBRO", - industryFilterBlueprints: "PROGETTI", - industryFilterBuilds: "PRODUZIONI", - industryFilterMine: "MIE", - industryFilterActive: "IN FUNZIONE", - industryFilterPaused: "IN PAUSA", - industryFilterDissolved: "SCIOLTE", - industryAllTitle: "Industria", - industryMemberTitle: "Fabbriche di cui faccio parte", - industryMineTitle: "Le mie fabbriche", - industryActiveTitle: "In funzione", - industryPausedTitle: "Fabbriche in pausa", - industryDissolvedTitle: "Fabbriche sciolte", - industryNoFacilitiesFound: "Nessuna fabbrica trovata.", - industryCreateFacility: "Crea fabbrica", - industryMembers: "Membri", - industryMemberBadge: "MEMBRO", - industryName: "Nome", - industryNamePlaceholder: "Nome della fabbrica", - industryDescriptionLabel: "Descrizione", - industryDescriptionPlaceholder: "Cosa produce questa fabbrica?", - industrySector: "Settore", - industryMembershipPolicy: "Politica di adesione", - industryQuorum: "Quorum", - industryMajority: "Maggioranza", - industryLaborRate: "Tariffa di lavoro", - industryCreateButton: "Crea fabbrica", - industryUpdateButton: "Aggiorna", - industrySteward: "Amministratore", - industryStatusActive: "IN FUNZIONE", - industryStatusPaused: "IN PAUSA", - industryStatusDissolved: "SCIOLTA", - industryStatusLabel: "Stato", - industryJoinButton: "Unisciti", - industryApplyButton: "Richiedi di unirti", - industryLeaveButton: "Esci", - industryInviteButton: "Invita", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "Serve un invito per unirti.", - industryPauseButton: "Vota la pausa", - industryResumeButton: "Vota la ripresa", - industryDeleteButton: "Elimina", - industryPendingApplicants: "Richieste in sospeso", - industryVotesYes: "sì", - industryVotesNo: "no", - industryAlreadyVoted: "votato", - industryAdmit: "Ammetti", - industryReject: "Rifiuta", - industryDissolveTitle: "Sciogli fabbrica", - industryDissolveVote: "Vota lo scioglimento", - industryPolicy_open: "Aperta", - industryPolicy_vote: "Per voto", - industryPolicy_invite: "Solo su invito", projectsTitle: "Progetti", projectsDescription: "Crea, finanzia e segui progetti comunitari nella tua rete.", - projectSearchPlaceholder: "Cerca progetti...", projectCreateProject: "Crea progetto", projectCreateButton: "Crea progetto", projectUpdateButton: "AGGIORNA", @@ -2813,8 +2944,14 @@ module.exports = { projectPledgeTitle: "Supporta questo progetto", projectPledgePlaceholder: "Importo in ECO", projectBounties: "Taglie", + projectBountiesInputLabel: "Taglie (una per riga: Titolo|Importo [ECO]|Descrizione)", + projectBountiesPlaceholder: "Correggi bug UI|100|Link al problema\nScrivi documentazione|250|Descrivi esempi d'uso", projectNoBounties: "Nessuna taglia trovata.", projectTitle: "Titolo", + projectAddBountyTitle: "Nuova taglia", + projectBountyTitle: "Titolo taglia", + projectBountyAmount: "Importo (ECO)", + projectBountyDescription: "Descrizione", projectMilestoneSelect: "Seleziona traguardo", projectBountyCreateButton: "Crea taglia", projectBountyStatus: "Stato taglia", @@ -2829,15 +2966,19 @@ module.exports = { projectMilestoneTitle: "Titolo traguardo", projectMilestoneTargetPercent: "Percentuale (%)", projectMilestoneDueDate: "Data", + projectMilestoneCreateButton: "Crea traguardo", projectMilestoneStatus: "Stato traguardo", projectMilestoneOpen: "aperto", projectMilestoneDone: "completato", projectMilestoneDue: "scadenza", + projectNoMilestones: "Nessun traguardo trovato.", projectMilestoneMarkDone: "Segna come completato", projectMilestoneTitlePlaceholder: "Inserisci titolo traguardo", projectMilestoneDescriptionPlaceholder: "Inserisci descrizione per questo traguardo", projectMilestoneDescription: "Descrizione traguardo", + projectBudgetGoal: "Budget (obiettivo)", projectBudgetAssigned: "Assegnato alle taglie", + projectBudgetRemaining: "Rimanente", projectBudgetOver: "⚠ Budget superato: l'assegnato supera l'obiettivo", projectFollowers: "Sostenitori", projectFollowersTitle: "Sostenitori", @@ -2850,6 +2991,7 @@ module.exports = { projectBackersTotalPledged: "Totale contribuito", projectBackersYourPledge: "Il tuo contributo", projectBackersNone: "Nessun contributo ancora.", + projectNoRemainingBudget: "Nessun budget rimanente.", projectFilterBackers: "FINANZIATORI", projectFilterApplied: "CANDIDATURE", projectAppliedTitle: "CANDIDATURE", @@ -2858,15 +3000,30 @@ module.exports = { projectBackerAmount: "Totale contribuito", projectBackerPledges: "Contributi", projectBackerProjects: "Progetti", + projectPledgeAmount: "Importo", projectSelectMilestoneOrBounty: "Seleziona traguardo o taglia", projectPledgeButton: "Contribuisci", footerLicense: "GPLv3", + footerPackage: "Pacchetto", + footerVersion: "Versione", modulesModuleName: "Nome", modulesModuleDescription: "Descrizione", modulesModuleStatus: "Stato", modulesTotalModulesLabel: "Moduli caricati", modulesEnabledModulesLabel: "Attivato", modulesDisabledModulesLabel: "Disattivato", + modulesPopularLabel: "In evidenza", + modulesPopularDescription: "Modulo per ricevere i post più popolari, più visti o più commentati.", + modulesTopicsLabel: "Argomenti", + modulesTopicsDescription: "Modulo per ricevere categorie di discussione basate su interessi comuni.", + modulesSummariesLabel: "Riassunti", + modulesSummariesDescription: "Modulo per ricevere riassunti di discussioni o post lunghi.", + modulesLatestLabel: "Più recenti", + modulesLatestDescription: "Modulo per ricevere i post e le discussioni più recenti.", + modulesThreadsLabel: "Discussioni", + modulesThreadsDescription: "Modulo per ricevere conversazioni raggruppate per argomento o domanda.", + modulesMultiverseLabel: "Multiverso", + modulesMultiverseDescription: "Modulo per ricevere contenuti da altri peer federati.", modulesInvitesLabel: "Inviti", modulesInvitesDescription: "Modulo per gestire e applicare codici invito.", modulesWalletLabel: "Portafoglio", @@ -2907,12 +3064,8 @@ module.exports = { modulesOpinionsDescription: "Modulo per scoprire e votare opinioni.", modulesTransfersLabel: "Trasferimenti", modulesTransfersDescription: "Modulo per scoprire e gestire i contratti intelligenti (trasferimenti).", - modulesFediverseLabel: "Multiverso", - modulesBlogsLabel: "Blog", - modulesBlogsDescription: "Modulo per scoprire e gestire i blog.", - modulesPollsLabel: "Sondaggi", - modulesPollsDescription: "Modulo per interrogare la rete e contare le risposte.", - modulesFediverseDescription: "Gestisci i tuoi altri account del multiverso, inclusi l'invio e la ricezione di contenuti.", + modulesFediverseLabel: "Fediverse", + modulesFediverseDescription: "Gestisci i tuoi altri account del fediverso, inclusi l'invio e la ricezione di contenuti.", modulesFeedLabel: "Feed", modulesFeedDescription: "Modulo per scoprire e condividere testi brevi (feed).", modulesParliamentLabel: "Parlamento", @@ -2948,33 +3101,37 @@ module.exports = { visibilityMakeHidden: "Rendi nascosta", invitesInhabitantsTitle: "Abitanti", invitesInhabitantsFollow: "Segui", - aiNavPlaceholder: "Descrivi cosa ti serve...", + aiNavPlaceholder: "Dove vuoi andare?", aiNavDisabled: "La navigazione con IA è disattivata.", - aiNavResultsTitle: "Suggerimenti AINav", + aiNavResultsTitle: "Suggerimenti di navigazione IA", aiNavQueryLabel: "Richiesta", + aiNavResultsDescription: "Scegli il percorso più adatto a ciò che stai cercando.", + aiNavResultPath: "Percorso", + aiNavResultDescription: "Cosa puoi fare", aiNavResultMatch: "Corrispondenza", aiNavResultsEmpty: "Nessun percorso corrispondente. Prova /search.", + aiNavSearchInstead: "Cerca invece", modulesAINavLabel: "AINav", modulesAINavDescription: "Modulo per query in linguaggio naturale sui contenuti della rete.", + directConnect: "Connessione diretta", directConnectDescription: "Connettiti direttamente a un peer inserendo indirizzo IP, porta e chiave pubblica.", peerHost: "IP", peerPort: "Porta (predefinita: 8008)", peerPublicKey: "Chiave pubblica (@...ed25519)", connectAndFollow: "Connetti", + deviceSourceLabel: "Dispositivo", modulesPresetTitle: "Configurazioni Comuni", - workflowsTitle: "Workflow", - workflowsDescription: "Un workflow imposta il tema e quali moduli sono attivi.", - workflowsSet: "Applica workflow", - workflow_default: "Predefinito", - workflow_jobs: "Lavoro", - workflow_ruling: "Governo", - workflow_politics: "Politica", - workflow_social: "Sociale", - workflow_business: "Affari", - workflow_night: "Notte", - workflow_mobile: "Mobile", + modulesPreset_minimal: "Minimo", + modulesPreset_basic: "Base", + modulesPreset_social: "Sociale", + modulesPreset_economy: "Economia", + modulesPreset_full: "Completo", statsCarbonFootprintTitle: "Impronta di carbonio", + statsCarbonFootprintNetwork: "Impronta di carbonio della rete", + statsCarbonFootprintYours: "La tua impronta di carbonio", statsCarbonTombstone: "Impronta del tombstoning", + feedSuccessMsg: "Feed pubblicato con successo!", + dominantOpinionLabel: "Opinione dominante", uploadMedia: "Carica media (max: 50MB)", courtsRespondentInvalid: "Deve essere un ID SSB valido (@...ed25519)", @@ -3008,13 +3165,14 @@ module.exports = { mapDescriptionLabel: "Descrizione", mapDescriptionPlaceholder: "Descrivi la mappa o la posizione...", mapTypeLabel: "Tipo di mappa", - mapInviteMode: "Invito", - mapTypeSingle: "SINGOLO", - mapTypeOpen: "APERTO", - mapTypeClosed: "CHIUSO", + mapTypeSingle: "SINGOLO (solo posizione iniziale)", + mapTypeOpen: "APERTO (chiunque può aggiungere marcatori)", + mapTypeClosed: "CHIUSO (solo il creatore aggiunge marcatori)", mapTagsLabel: "Tag", mapTagsPlaceholder: "Inserisci tag separati da virgole", mapUrlLabel: "URL della mappa", + mapPickCoordLabel: "Oppure seleziona una posizione sulla griglia:", + mapMarkersLabel: "marcatori", mapMarkersTitle: "Marcatori", mapMarkerDefault: "Marcatore", mapMarkerLatLabel: "Latitudine marcatore", @@ -3041,9 +3199,14 @@ module.exports = { padTitle: "Pad", modulesPadsLabel: "Pad", modulesPadsDescription: "Modulo per gestire editor di testo collaborativi.", + padFilterAll: "TUTTI", + padFilterMine: "MIO", + padFilterRecent: "RECENTE", + padFilterOpen: "APERTO", + padFilterClosed: "CHIUSO", padCreate: "Crea Pad", - padUpdate: "Aggiorna", - padDelete: "Elimina", + padUpdate: "Aggiorna Pad", + padDelete: "Elimina Pad", padTitleLabel: "Titolo", padTitlePlaceholder: "Inserisci il titolo del pad...", padStatusLabel: "Stato", @@ -3054,7 +3217,14 @@ module.exports = { padTagsLabel: "Tag", padTagsPlaceholder: "tag1, tag2, ...", padMembersLabel: "Membri", + padVisitPad: "Visita Pad", + padShareUrl: "Condividi URL", padCreated: "Creato", + padAuthor: "Autore", + padGenerateCode: "Genera Codice", + padInviteCodeLabel: "Codice Invito", + padInviteCodePlaceholder: "Inserisci il codice invito...", + padValidateInvite: "Valida", padStartEditing: "INIZIA A MODIFICARE!", padEditorPlaceholder: "Inizia a scrivere...", padSubmitEntry: "Invia", @@ -3067,11 +3237,12 @@ module.exports = { padClosedSectionTitle: "Pad Chiusi", padCreateSectionTitle: "Crea Nuovo Pad", padUpdateSectionTitle: "Aggiorna Pad", + padInviteGenerated: "Codice Invito Generato", typePad: "PADS", padNew: "NUOVO", padAddFavorite: "Aggiungi ai Preferiti", padRemoveFavorite: "Rimuovi dai Preferiti", - padClose: "Chiudi", + padClose: "Chiudi Pad", padBackToEditor: "Torna all'editor", padSearchPlaceholder: "Cerca pad...", @@ -3079,6 +3250,8 @@ module.exports = { modulesChatsDescription: "Modulo per scoprire e gestire chat cifrate.", typeChat: "CHATS", typeChatMessage: "MESSAGGIO CHAT", + chatLabel: "CHAT", + chatMessageLabel: "MESSAGGI CHAT", chatsTitle: "Chat", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3088,50 +3261,52 @@ module.exports = { chatDescription: "Descrizione", chatCategory: "Categoria", chatStatus: "STATO", + chatFilterAll: "TUTTI", + chatFilterMine: "MIO", + chatFilterRecent: "RECENTE", + chatFilterFavorites: "PREFERITI", + chatFilterOpen: "APERTO", + chatFilterClosed: "CHIUSO", chatCreate: "Crea Chat", - chatUpdate: "Aggiorna", - chatDelete: "Elimina", + chatUpdate: "Aggiorna Chat", + chatDelete: "Elimina Chat", chatClose: "Chiudi Chat", chatVisitChat: "VISITA CHAT", chatUntitled: "Chat senza titolo", chatNoItems: "Nessuna chat trovata.", chatParticipants: "Partecipanti", - chatMessagesLabel: "Messaggi", - chatThreadsLabel: "Discussioni", + chatStartChatting: "INIZIA A CHATTARE!", + chatGenerateCode: "Genera Codice", + chatShareUrl: "Condividi URL", chatCreatedAt: "CREATO", chatSearchPlaceholder: "Cerca chat...", chatStatusOpen: "APERTO", chatStatusInviteOnly: "SOLO SU INVITO", chatStatusClosed: "CHIUSO", chatSendMessage: "Invia", - chatJumpLatest: "Ultimo Messaggio", - chatPickChat: "Scegli una chat per aprirla", - chatNoneYet: "Nessuna chat disponibile, per ora.", - activitySearchPlaceholder: "Cerca nell'attività...", - trendingSearchPlaceholder: "Cerca nelle tendenze...", - opinionsSearchPlaceholder: "Cerca nelle opinioni...", - votesSearchPlaceholder: "Cerca nelle votazioni...", - welcomeStepUxTitle: "Scegli la tua interfaccia", - welcomeStepUxText: "Decidi l'aspetto di Oasis. Potrai cambiarlo nelle impostazioni.", - welcomeStepUxAction: "Usa questa vista", - chatRateLimitTitle: "Troppi messaggi", - chatRateLimitMessage: "Hai raggiunto il limite di messaggi che questa stanza accetta in un'ora. Riprova più tardi.", chatMessagePlaceholder: "Scrivi il tuo messaggio...", chatNoMessages: "Ancora nessun messaggio.", + chatLeave: "Abbandona Chat", + chatInviteCodeLabel: "Inserisci codice invito", + chatJoinByInvite: "Unisciti", chatTitlePlaceholder: "Titolo chat", chatDescriptionPlaceholder: "Descrizione chat", chatTagsPlaceholder: "tag1, tag2, tag3", chatImageLabel: "Seleziona un file immagine (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "Codice Invito", chatAuthor: "Autore", + chatCreated: "Creato", chatAddFavorite: "Aggiungi ai Preferiti", chatRemoveFavorite: "Rimuovi dai Preferiti", chatPM: "MP", chatStatusLabel: "Stato", chatCategoryLabel: "Categoria", + chatParticipantsLabel: "Partecipanti", gamesTitle: "Giochi", gamesDescription: "Scopri e gioca ad alcuni giochi.", gamesFilterAll: "TUTTI", gamesPlayButton: "GIOCA!", + gamesBackToGames: "Torna ai Giochi", modulesGamesLabel: "Giochi", modulesGamesDescription: "Modulo per scoprire e giocare ad alcuni giochi.", modulesGraphosLabel: "Graphos", @@ -3148,6 +3323,8 @@ module.exports = { gamesArkanoidDesc: "Rompi tutti i mattoni con la tua racchetta e la pallina. Una sfida arcade classica.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Ping-pong classico contro un'IA. Il primo a 5 punti vince.", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "Corsa contro il tempo! Evita il traffico e raggiungi il traguardo prima che scada.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Pilota la tua navicella in un campo di asteroidi. Sparali prima che ti colpiscano.", gamesRockPaperScissorsTitle: "Carta Forbici Sasso", @@ -3168,6 +3345,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3178,6 +3357,12 @@ module.exports = { modulesCalendarsLabel: "Calendari", modulesCalendarsDescription: "Modulo per scoprire e gestire i calendari.", typeCalendar: "CALENDARI", + calendarFilterAll: "TUTTI", + calendarFilterMine: "I MIEI", + calendarFilterOpen: "APERTO", + calendarFilterClosed: "CHIUSO", + calendarFilterRecent: "RECENTI", + calendarFilterFavorites: "PREFERITI", calendarCreate: "Crea calendario", calendarUpdate: "Aggiorna", calendarDelete: "Elimina", @@ -3190,17 +3375,24 @@ module.exports = { calendarTagsLabel: "Tag", calendarTagsPlaceholder: "tag1, tag2...", calendarParticipantsLabel: "Partecipanti", + calendarParticipantsCount: "Partecipanti", + calendarVisitCalendar: "Visita il calendario", calendarCreated: "Creato", + calendarAuthor: "Autore", calendarJoin: "Partecipa", calendarGenerateInvite: "Genera invito", + calendarInviteCodePlaceholder: "Inserisci il codice di invito...", + calendarValidateInvite: "Convalida codice", + calendarJoined: "Iscritto", + calendarAddDate: "Aggiungi data", + calendarAddNote: "Aggiungi nota", calendarDateLabel: "Data", calendarDatePlaceholder: "Descrivi questa data...", - calendarNoteLabel: "Note", + calendarNoteLabel: "Nota", calendarNotePlaceholder: "Aggiungi una nota...", calendarFirstDateLabel: "Data", calendarFirstNoteLabel: "Note", calendarIntervalLabel: "Interval", - calendarIntervalNone: "Senza ripetizione", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3211,6 +3403,8 @@ module.exports = { calendarsDescription: "Scopri e gestisci i calendari nella tua rete.", calendarMonthPrev: "← Precedente", calendarMonthNext: "Successivo →", + calendarMonthLabel: "Date", + calendarsShareUrl: "URL di condivisione", calendarAllSectionTitle: "Calendari", calendarRecentSectionTitle: "Calendari Recenti", calendarFavoritesSectionTitle: "Preferiti", @@ -3224,6 +3418,7 @@ module.exports = { calendarRemoveFavorite: "Rimuovi dai preferiti", calendarSearchPlaceholder: "Cerca calendari...", calendarAddEntry: "Aggiungi Voce", + calendarLeave: "Lascia Calendario", statsCalendar: "Calendari", statsCalendarDate: "Date calendario", statsCalendarNote: "Note calendario", @@ -3270,6 +3465,7 @@ module.exports = { torrentSortOldest: "Più vecchi", torrentSortTop: "Più votati", torrentNoMatch: "Nessun torrent corrispondente.", + torrentMessageAuthorButton: "Invia Messaggio all'Autore", torrentUpdatedAt: "Aggiornato", statsTorrent: "Torrent", typeTorrent: "TORRENT", @@ -3277,18 +3473,10 @@ module.exports = { modulesTorrentsDescription: "Modulo per scoprire e gestire i torrent.", favoritesFilterTorrents: "TORRENT", favoritesFilterMarket: "MERCATO", - favoritesFilterEvents: "EVENTI", - favoritesFilterForum: "FORUM", - favoritesFilterHousing: "ALLOGGI", - favoritesFilterJobs: "LAVORI", - favoritesFilterProjects: "PROGETTI", - favoritesFilterReports: "RAPPORTI", - favoritesFilterTasks: "COMPITI", - favoritesFilterTransfers: "TRASFERIMENTI", - favoritesFilterVotes: "VOTAZIONI", favoritesFilterShopProducts: "ARTICOLI NEGOZIO", tribeSectionTorrents: "TORRENT", tribeCreateTorrent: "Carica Torrent", + tribeMediaTypeTorrent: "Torrent", settingsWishTitle: "Desiderio", settingsWishDesc: "Configura il desiderio del tuo Avatar.", settingsWishWhole: "Multiverse", @@ -3299,10 +3487,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "L'invio è un guardrail UX. La ricezione è un filtro di attenzione.", + pmBlockedNonMutual: "Puoi inviare MP solo ad abitanti in supporto reciproco.", inhabitantsPendingFollowsTitle: "Richieste di supporto in attesa", inhabitantsPendingAccept: "Accetta", inhabitantsPendingReject: "Rifiuta", + bxEncrypted: "CRITTOGRAFATO", + bxEncryptedHexLabel: "Testo cifrato (anteprima)", tribeSectionGovernance: "GOVERNANCE", + tribeSubStatusPublic: "PUBBLICA", + tribeSubStatusPrivate: "PRIVATA", + tribeSubInheritedPrivate: "PRIVATA (ereditata dalla tribù principale)", tribePadInviteRequired: "Non hai accesso al pad. Richiedi un invito.", tribeChatInviteRequired: "Non hai accesso alla chat. Richiedi un invito.", tribeGovernanceDesc: "Governance interna di questa tribù.", @@ -3342,31 +3536,44 @@ module.exports = { tribeGovNoHistorical: "Ancora nessun record", logsTitle: "Diario", logsDescription: "Registra la tua esperienza nella rete.", + logsReadMore: "Leggi di più", logsViewTitle: "Diario", logsColumnType: "Tipo", logsView: "Vedi", + logsManualPrompt: "Scrivi il tuo diario", logsUpdateButton: "Aggiorna", - logsEditTitle: "Aggiorna registro", + logsEditTitle: "Modifica voce", + logsCreateDescription: "Registra la tua esperienza...", logsCreateTitle: "Crea voce", logsWriteButton: "Scrivi", logsTextPlaceholder: "Descrivi le tue esperienze...", + logsTextField: "Testo", + logsLabelPlaceholder: "Etichetta...", + logsLabelField: "Scrivi il tuo diario (etichetta)", + logsAiDisabledWarn: "Attiva il modulo IA in /modules per usare le voci scritte dall'IA.", + logsAiContextValue: "Giorno di blockchain", + logsAiContext: "Contesto", + logsAiModStatus: "AImod", + logsModeApply: "Applica!", logsModeAIWritten: "AI-Assistant", logsModeManual: "Manuale", logsModeAI: "IA", + logsColumnDelete: "Elimina", + logsColumnEdit: "Modifica", logsColumnLog: "Diario", logsColumnDate: "Data", logsDelete: "Elimina", - logsEdit: "Aggiorna", + logsEdit: "Modifica", logsFilterAlways: "SEMPRE", logsFilterYear: "ULTIMO ANNO", logsFilterMonth: "ULTIMO MESE", logsFilterWeek: "ULTIMA SETTIMANA", logsFilterToday: "OGGI", - logsComments: "Commenti", - logsAuthorEntries: "Voci", + logsFilterRecent: "RECENTI", + logsFilterAll: "TUTTI", logsCreate: "Crea voce", logsExport: "Esporta diario", - logsExportOne: "Esporta registro", + logsExportOne: "Esporta", logsViewDetails: "Vedi dettagli", logsGenerateButton: "Genera testo", logsSearchText: "Cerca nei diari...", @@ -3376,25 +3583,31 @@ module.exports = { modulesLogsLabel: "Diario", modulesLogsDescription: "Modulo per registrare (tramite assistente IA) le tue esperienze.", statsLogsTitle: "Diario", + statsLogsEntries: "Voci", typeLog: "DIARIO", + blockchainCycle: "Ciclo", larpTitle: "L.A.R.P.", larpDescription: "Uno strato di gioco di ruolo dal vivo per la sperimentazione collaborativa.", - larpNoHousesMatch: "Nessuna casa corrisponde alla tua ricerca.", - larpSearchPlaceholder: "Cerca case...", larpAllHouses: "Case:", larpFilterRuling: "GOVERNA", larpFilterHouses: "CASE", larpFilterRules: "FAQ", larpRulesTitle: "FAQ del L.A.R.P.", + larpRulesEntryTitle: "Casa iniziale", larpRulesEntryText: "Ogni abitante inizia in ACADEMIA per impostazione predefinita. Da ACADEMIA, scegli una casa dal pannello \"Unisciti a una casa\": questo apre il suo test d'ingresso. Rispondi alle 10 domande e invia. Se superi la soglia (7/10), sei ammesso automaticamente in quella casa; altrimenti vieni rifiutato e rimani in ACADEMIA. In entrambi i casi il sistema registra il tuo OasisID e il ciclo del tentativo e impedisce un nuovo tentativo fino allo scadere del periodo di attesa di 30 cicli.", + larpRulesLeaveText: "I membri di qualsiasi casa possono tornare ad ACADEMIA in qualsiasi momento dalla pagina della propria casa; ciò ripristina la loro appartenenza, ma non annulla il periodo di attesa del test.", + larpRulesGovernanceTitle: "Muro di governo", larpRulesGovernanceText: "Durante il suo ciclo, la casa governante porta l'insegna \"Governa\" ed è l'unica il cui Muro è visibile pubblicamente sotto la scheda GOVERNA.", - larpRulesWallText: "Ogni casa ha un Muro dove scrivono i suoi membri. Il Muro di una casa è privato, tranne quello della casa al governo e quello dell'ACADEMIA, che chiunque può leggere.", + larpRulesSignTitle: "Emblema L.A.R.P.", larpRulesSignText: "Gli abitanti possono attivarlo (in /profile/edit > Sensori) per mostrare l'immagine della loro casa attuale come sigillo sul profilo, sulla scheda autore e abitante. Il sigillo rimanda alla pagina della casa.", larpPostsTitle: "Muro", larpPostsEmpty: "Ancora nessuna pubblicazione.", + larpPostLabel: "Nuova pubblicazione", larpPostPlaceholder: "Cosa ha da dire questa casa?", + larpPostPublic: "Pubblico (visibile a chiunque, altrimenti solo ai membri)", larpPostSubmit: "Pubblica", + larpPostMembersOnly: "Solo membri", larpCycleLabel: "Ciclo:", bankRulesArchTaxFormula: "ARCH Tax(user) = max(0, (newestBlockTs − userFirstBlockTs) / 86400000) × ecoinPerDayOfHistory", bankRulesArchTaxNote: "ARCH Tax reflects the long-term cost of growing and maintaining the network archive: the older your data lives in the network, the larger your share of the maintenance cost.", @@ -3432,9 +3645,12 @@ module.exports = { bankTaxesLookupNote: "Inserisci un ID di blocco per cercarlo nel tuo feed locale.", bankTaxesUserNoteChipsClick: "Clicca su qualsiasi chip per ispezionare il suo blocco nel blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntry: "Codice d'invito", larpRulesInviteEntryText: "I membri delle case possono generare codici di invito che danno accesso diretto alla casa.", larpRulesOneHouseText: "Puoi appartenere al massimo a una casa in ogni momento. Se non appartieni a nessuna, sei in ACADEMIA. Ogni pagina di casa (non ACADEMIA) mostra ai suoi membri un pulsante \"Lascia la Casa\" che reimposta l'appartenenza ad ACADEMIA. Uscire NON annulla il periodo di attesa del test.", - larpRulesTestEntryText: "Nel test WILL ogni opzione pondera una o più case; all'invio il sistema somma i punteggi e ti ammette automaticamente nella casa con il punteggio più alto.", + larpRulesOneHouseTitle: "Una casa alla volta", + larpRulesTestEntry: "Test WILL", + larpRulesTestEntryText: "Ogni opzione pondera una o più case; all'invio, il sistema somma i punteggi e ti ammette automaticamente nella casa con il punteggio più alto.", larpWillTestHint: "Una volta completato, ti verrà assegnata la casa che meglio corrisponde alle tue risposte. Le tue risposte individuali vengono elaborate localmente e mai memorizzate — solo l'assegnazione di casa risultante viene pubblicata nel tuo feed.", larpWillTestTitle: "Test WILL", settingsLanDesc: "Annuncia periodicamente questo peer ad altre istanze di Oasis sulla stessa rete locale. Disabilita per interrompere le trasmissioni UDP.", @@ -3453,30 +3669,36 @@ module.exports = { aiExchangeMarkHelpful: "+1 utile", larpCyclesUntilRuling: "Prossimo ciclo di governo", larpVisitTribe: "Visita la tribù", - larpStartJourney: "Inizia il viaggio", larpGoverning: "Governa:", - larpStartHere: "Inizia qui:", larpMyHouse: "La mia Casa:", larpMembersCount: "Membri", + larpMembersTitle: "Membri", + larpNoMembers: "Ancora nessun membro.", larpRolesLabel: "Ruoli", larpFunctionLabel: "Funzione", larpMonthLabel: "Ciclo di governo", larpLeaveToAcademia: "Torna ad ACADEMIA", - larpAcademiaJoinTitle: "Case L.A.R.P.", + larpAcademiaJoinTitle: "Unisciti a una casa", + larpAcademiaJoinHint: "Sostieni il test psicologico di profilo per essere assegnato alla casa che meglio si adatta a te, oppure usa un codice di invito condiviso da un membro di una casa.", + larpTakeTestButton: "Sostieni il test di profilo", larpInviteRedeem: "Unisciti alla Casa", larpInviteCreate: "Genera codice di invito", larpInvitePlaceholder: "Codice di invito", larpInviteBannerTitle: "Nuovo codice di invito", larpInviteBannerHint: "Condividi questo codice con qualcuno in ACADEMIA. Scade tra 30 cicli o dopo un singolo uso.", + larpTestAssignedHouse: "Casa assegnata", larpTestRankingTitle: "Punteggio per casa", invitesHousesTitle: "L.A.R.P.", invitesHouseInviteCodePlaceholder: "Codice di invito di casa", invitesHouseJoinButton: "Unisciti alla Casa", ecoTaxLabel: "ECO Tax", + ecoinTaxLabel: "ECOin Tax", bankTaxes: "Tasse", bankOverviewYourTaxes: "Le tue tasse", + bankTaxesNetworkTitle: "Tasse della rete", bankTaxesEcoTaxTitle: "ECO Tax", bankTaxesArchTaxTitle: "ARCH Tax", + bankTaxesUserTitle: "Le tue tasse", bankTaxesUserAmount: "La tua ECO Tax (prezzo da restituire)", bankTaxesArchTaxUserAmount: "La tua ARCH Tax (prezzo da restituire)", bankTaxesArchTaxFirstBlock: "Età del tuo primo blocco", @@ -3527,13 +3749,16 @@ module.exports = { bankRulesEcoTaxRate: "Aliquota", bankRulesArchTaxRate: "Aliquota", bankRulesCarbonFactor: "Fattore di carbonio", + statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "Ancora nessun tombstone nella rete.", blockchainBlockNotAvailable: "Questo blocco non è disponibile nel tuo feed locale. Potrebbe appartenere a un peer da cui non stai ancora replicando.", blockchainBackToBlockexplorer: "Torna al blockexplorer", audioFilterBcs: "BCS", audioTranscodeButton: "TRANSCODE", + audioTranscodeTitle: "BCS Transcode", audioTranscodeDetailTitle: "Transcode", audioTranscodeDetailDescription: "Decodifica il payload incorporato e la mappa di composizione originale della blockchain.", + audioTranscodeStegoTitle: "Incorporato nella forma d'onda", audioTranscodeStegoOasisId: "Da", audioTranscodeStegoTimestamp: "Generato il", audioTranscodeStegoMessage: "TEXT", @@ -3541,7 +3766,9 @@ module.exports = { audioTranscodeStegoUnknown: "—", audioTranscodeStegoNotFound: "Nessun payload steganografico è stato decodificato da questo audio.", audioTranscodeCompositionEmpty: "Questo audio non include una composizione blockchain salvata.", + audioBackToAudio: "Torna all'audio", audioBackToBcs: "Torna a BCS", + audioAuthorLabel: "Autore", melodyCompositionTitle: "Mappa di composizione blockchain", melodyFilterMine: "MIO", melodyByLabel: "Da", @@ -3555,6 +3782,7 @@ module.exports = { melodyUploadBcsNameNote: "L'audio sarà pubblicato col nome auto-generato", larpLastAttemptTitle: "Il tuo ultimo tentativo", larpLastAttemptHouse: "Casa", + larpLastAttemptResult: "Risultato", larpLastAttemptWhen: "Quando", larpLastAttemptCooldown: "Prossimo tentativo in", larpTestTitle: "Test d'ingresso", @@ -3563,14 +3791,29 @@ module.exports = { larpTestCooldownActive: "Prossimo test disponibile tra", larpTestCooldownDays: "cicli", larpTestResultTitle: "Risultato del test", + larpTestPassed: "Test superato — benvenuto!", + larpTestFailed: "Test non superato", larpTestScore: "Punteggio", larpTestNextAttempt: "Puoi tentare un altro test tra 30 cicli.", larpGoToHouse: "Vai alla tua casa", larpBackToAcademia: "Torna ad ACADEMIA", + larpBackToHouses: "◀ Case", larpBadgeYou: "Tu", larpBadgeRuling: "Governa", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Modulo per lo strato di gioco di ruolo dal vivo di Oasis (le 9 Case).", + melodyUploadTitlePlaceholder: "Composition title", + melodyUploadDescPlaceholder: "Optional description", + bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", + bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", + bankTaxesExample: "Example", + bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", + audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + mapClickToCreate: "Click on the map to select a location", + mapClickToAddMarker: "Click on the map to add a marker", + gameScoreLabel: "GAME SCORES", larpProfileQ1: "Di fronte a un problema complesso, cosa fai per primo?", larpProfileQ1O1: "Parlare con altri per cercare il consenso", larpProfileQ1O2: "Costruire un prototipo da testare", diff --git a/src/client/assets/translations/oasis_pt.js b/src/client/assets/translations/oasis_pt.js index 4d3ecd8..07a52ad 100644 --- a/src/client/assets/translations/oasis_pt.js +++ b/src/client/assets/translations/oasis_pt.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { pt: { + bbAdd: "Adicionar", + bbQuickActions: "Ações rápidas", + bbPickTitle: "Fixar um módulo", + bbPickDesc: "Escolhe um módulo para o espaço + da barra inferior.", + bbPickNone: "Nenhum", + bbManage: "Editar", + bbYourBar: "Sua barra", + bbRemove: "Remover", + bbFull: "Máximo atingido", + bbDone: "Concluído", + activityChangeFilter: "Mudar filtro", + emptyConnectHint: "Liga-te a um pub para sincronizar com a rede.", + emptyConnectCta: "Ligar a um pub", + menuCommunity: "Comunidade", + activityGroupCommunity: "Comunidade e governança", + activityGroupOrg: "Organização", + activityGroupComm: "Comunicação", + activityGroupEconomy: "Economia", + activityGroupMedia: "Conteúdo e média", + activityGroupOther: "Outros", + languageName: "Português", shopOrderStatusPaid: "Pagamento recebido", shopOrderStatusReceived: "Recebido", shopOrderMarkPaid: "Marcar pagamento recebido", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "Ainda não tens compras.", shopOrderPmSeller: "PM Seller", shopOrderSeller: "Vendedor", + shopOrderPaymentConcept: "Pagamento", shopOrderInvoice: "Fatura", shopOrderInvoiceConcept: "Fatura", shopOrderStatusPending: "Pendente", shopOrderStatusAccepted: "Aceite", shopOrderStatusRejected: "Rejeitado", shopOrderStatusShipped: "Enviado", + shopOrderStatusDelivered: "Entregue", shopOrderAccept: "Aceitar", shopOrderReject: "Rejeitar", shopOrderMarkShipped: "Marcar enviado", + shopOrderMarkDelivered: "Marcar entregue", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "Contrato", shopOrderContractConcept: "Encomenda de loja", shopOrdersPending: "Encomendas pendentes", all: "Todos", @@ -50,13 +75,14 @@ module.exports = { viewJson: "Ver JSON", matchScore: "Pontuação de correspondência", preferencesLabel: "Preferências", - inhabitantProfileTitle: "Habitante", + inhabitantProfileTitle: "Perfil do habitante", contentWarningLabel: "Aviso de conteúdo", invalidPost: "Conteúdo inválido", invalidPostHint: "Esta mensagem tem texto vazio/inválido.", spreadBy: "Por", spreadChron: "Difusão", spreaded: "Difundido", + spreadMore: "mais", spreadContentUnavailable: "Conteúdo ainda não disponível (replicação pendente)", filteredByTag: "Filtrado por etiqueta", filteredByTagTitle: "Filtrado por etiqueta", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "Sem gravidade", calendarDeleteDate: "Eliminar data", calendarIntervalUntil: "Até", + bookmarkCategory: "Categoria", bookmarkLastVisit: "Última visita", profileClearnetBookmarksLabel: "Marcadores", + forumFilterHot: "Populares", invitesPort: "Porta", currentlyUnrecheable: "ERRO!", peerHostValidation: "IPv4 válido (ex. 192.168.1.100) ou nome de host (ex. pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "Estimativa máx. anual", statsCarbonNetwork: "Total da rede", statsCarbonOfEstMax: "da capacidade máx. estimada", - statsCarbonYearProgress: "do ano decorrido", - statsNetworkTitle: "Rede", - statsStorageTitle: "Armazenamento", - statsEcoTaxTitle: "Taxa ECO", - statsAccountTitle: "Conta", - statsAveragesTitle: "Médias", statsCarbonOfNetwork: "do total da rede", statsCarbonUser: "A tua pegada", statsContentCountColumn: "Quantidade", @@ -154,6 +176,10 @@ module.exports = { statsTopTagsTitle: "Principais etiquetas", statsTopTypesTitle: "Principais tipos de conteúdo", statsTotalMsgs: "Total de mensagens", + feedViewDetails: "Ver detalhes", + eventFileLabel: "Anexar imagem", + taskFileLabel: "Anexar imagem", + courtsRespondentRequired: "O ID do réu é obrigatório", extended: "Multiverso", extendedDescription: [ "Quando apoias alguém, podes descarregar publicações dos habitantes que apoiam, e estas aparecem aqui, ordenadas por data.", @@ -197,27 +223,36 @@ module.exports = { graphosShowing: "Mostrando", graphosYou: "Você", graphosTotalNodes: "Total de nós", + manualMode: "Modo manual", mentions: "Menções", mentionsDescription: [ - strong("Todas as mensagens que te @mencionam"), - ".", + strong("Publicações que te @mencionam"), + ", ordenadas por data.", ], + nextPage: "Seguinte", + previousPage: "Anterior", noMentions: "Ainda não recebeste @menções.", - mentionsSearchPlaceholder: "Procurar menções...", privateReminders: "LEMBRETES", inboxFiltersLabel: "Filtros:", inboxToggleToMutuals: "Mudar para mútuos", inboxToggleToWhole: "Mudar para todos", + privateFrom: "De", + privateTo: "Para", privateDate: "Data", pmReply: "Responder", pmReplies: "respostas", + pmNew: "novas", + pmMarkRead: "Marcar como lido", inReplyTo: "EM RESPOSTA A", pmPreview: "Pré-visualizar", pmPreviewTitle: "Pré-visualização da mensagem", peers: "Pares", searchDescription: "Descrição", + imageSearch: "Pesquisa de imagens", searchFromLabel: "De", searchToLabel: "Até", + searchMinOpinionsLabel: "Mín opiniões", + searchInhabitantLabel: "Habitante (Oasis ID)", searchQueryLabel: "Pesquisa", searchPerPageLabel: "Resultados por página", settings: "Definições", @@ -229,38 +264,67 @@ module.exports = { melodyTitle: 'Melody', melodyDescription: 'Toca a melodia da tua blockchain — cada bloco torna-se uma nota.', melodyEmpty: 'Ainda sem notas — a tua blockchain ainda não produziu nenhum bloco.', + melodyCompositionHelp: 'Cada bloco da tua blockchain torna-se uma nota musical segundo o seu tipo e tamanho.', + melodyStatsTitle: 'Distribuição por tipo', + melodyInhabitantLabel: 'Habitante', melodyTotalBlocks: 'Notas', + melodyType: 'Tipo', + melodyCount: 'Blocos', melodyFilterAll: 'TODAS', + melodyFilterShare: 'Carregar melodia', + melodyShareTitle: 'Partilha a tua melodia', + melodyShareHelp: 'Publica um instantâneo da tua melodia atual para que outros a oiçam e remixem.', + melodyShareTitleLabel: 'Título (opcional)', + melodyShareTitlePlaceholder: 'Um fantasma de blockchain', + melodyShareSubmit: 'Publicar melodia', + melodyNoShared: 'Ainda sem melodias de blockchain.', melodyRegenerate: 'Regenerar', melodyDownload: "Descarregar BCS", + profileActivityTitle: 'Atividade', + profileActivityMore: 'Ver toda a atividade', profileContentSectionTitle: 'Conteúdo do Avatar', profileContentHelp: 'Escolhe quais módulos serão exibidos no teu avatar.', profileSensorsHelp: 'Métricas opcionais exibidas no teu avatar.', profileClearnetHelp: 'Módulos acessíveis a partir de fora do Oasis.', profileHubAll: 'Tudo', + profileHubTitle: 'Conteúdo Público', + footerPulseTitle: 'Pulso', + footerPulsePeers: 'Pares', + footerPulseInbox: 'Caixa', + footerPulseSync: 'Última sinc.', + footerPulseEcoValue: 'ECO/h', + footerPulseLastActivity: 'Última atividade', footerLicenseLabel: 'Licença', profileSwitchToOasis: 'Voltar para Oasis', - profileVisibilityFediverse: "Multiverso", + profileSwitchToClearnet: 'Mergulha na Clearnet', + profileVisibilityFediverse: "Fediverse", + profileSwitchToFediverse: "Mergulhar no fediverso", + coordLabel: 'Coordenada (ex.: A3)', + coordPlaceholder: 'Introduz coordenada', contributorsTitle: "Contribuidores", + pixeliaBy: "por", colorLabel: 'Escolhe uma cor', paintButton: 'Pintar!', + invalidCoordinate: 'Coordenada incorreta', + goToMuralButton: "Ver mural", totalPixels: 'Total de pixéis', modules: "Módulos", modulesViewTitle: "Módulos", modulesViewDescription: "Configura o teu ambiente ativando ou desativando módulos.", inbox: "Caixa de entrada", multiverse: "Multiverso", - fediverse: "Multiverso", - fediverseDescription: "Faça a gestão das suas outras contas do multiverso, incluindo enviar e receber conteúdo.", + fediverse: "Fediverse", + fediverseDescription: "Faça a gestão das suas outras contas do fediverso, incluindo enviar e receber conteúdo.", fediverseStatus: "Estado", - fediverseDisconnected: "Multiverso desligado. Verifique %LINK% ou o estado da ligação.", - fediverseSettingsTitle: "Multiverso", + fediverseDisconnected: "Fediverso desligado. Verifique %LINK% ou o estado da ligação.", + fediverseSettingsTitle: "Fediverse", fediverseInstanceLabel: "Endereço", fediverseTokenLabel: "Token de acesso", fediverseTokenHelp: "Defina o seu token de acesso. Será usado para gerir a sua conta de Mastodon a partir do Oasis.", fediverseConnect: "Ligar", fediverseConnectedAs: "Conectado como", fediverseDisconnect: "Desconectar", + fediverseEmpty: "Quando ligar outras contas do fediverso nas %LINK%, poderá geri-las a partir daqui.", fediverseEmptyLink: "suas definições", fediverseComposePlaceholder: "No que está pensando?", fediverseReplyPlaceholder: "Escreva a sua resposta…", @@ -269,15 +333,21 @@ module.exports = { fediverseReply: "Responder", fediverseBoosted: "impulsionou", fediverseNoPosts: "A sua timeline está vazia.", + fediverseThread: "Tópico", fediverseTimeline: "Cronologias", + fediverseManageTimeline: "Gerir cronologia", fediverseFollowers: "Seguidores", fediverseFollowing: "A seguir", fediversePosts: "Publicações", fediverseJoined: "Aderiu", + fediverseOverview: "Resumo", fediverseRefresh: "Atualizar", fediverseWrite: "Escrever", fediversePreview: "Pré-visualizar", + fediverseEdit: "Editar", + fediverseOpenFeed: "Ver feed", fediverseManage: "Gerir", + fediverseStatusNotConnected: "Não conectado", fediverseError: "Não foi possível contactar o Mastodon.", fediverseErrInstance: "URL de instância inválido.", fediverseErrAuth: "Token inválido ou expirado.", @@ -288,6 +358,17 @@ module.exports = { fediverseErrPost: "Não foi possível publicar.", fediverseErrMedia: "Não foi possível enviar o ficheiro.", fediverseErrEmpty: "Escreva algo ou anexe um ficheiro.", + popularLabel: "Destaques", + topicsLabel: "Tópicos", + latestLabel: "Mais recentes", + summariesLabel: "Resumos", + threadsLabel: "Conversas", + multiverseLabel: "Multiverso", + inboxLabel: "Caixa de entrada", + invitesLabel: "Convites", + walletLabel: "Carteira", + legacyLabel: "Chaves", + cipherLabel: "Encriptação", bookmarksLabel: "Marcadores", videosLabel: "Vídeos", torrentsLabel: "Torrents", @@ -298,14 +379,18 @@ module.exports = { inhabitantsLabel: "Habitantes", trendingLabel: "Tendências", eventsLabel: "Eventos", + tasksLabel: "Tarefas", apply: "Aplicar", menuPersonal: "Pessoal", + menuContent: "Conteúdo", + menuBlogs: "Blogues", menuGovernance: "Governação", menuOffice: "Escritório", - menuNetwork: "Comunidade", + menuMultiverse: "Multiverso", + menuNetwork: "Rede", menuCreative: "Criativo", menuEconomy: "Economia", - menuMedia: "Biblioteca", + menuMedia: "Multimédia", menuTools: "Ferramentas", comment: "Comentário", subtopic: "Subtópico", @@ -315,6 +400,13 @@ module.exports = { follow: "Apoiar", block: "Bloquear", unblock: "Desbloquear", + newerPosts: "Publicações mais recentes", + olderPosts: "Publicações mais antigas", + feedRangeEmpty: "O intervalo indicado está vazio para este feed. Tenta ver o ", + seeFullFeed: "feed completo", + feedEmpty: "A rede Oasis nunca viu publicações desta conta.", + beginningOfFeed: "Este é o início do feed", + noNewerPosts: "Ainda não foram recebidas publicações mais recentes.", relationshipNotFollowing: "Não te apoia", relationshipTheyFollow: "Apoia-te", relationshipMutuals: "Apoio mútuo", @@ -324,20 +416,25 @@ module.exports = { relationshipBlockedBy: "Estás bloqueado", relationshipMutualBlock: "Bloqueio mútuo", relationshipNone: "Não estás a apoiar", + relationshipConflict: "Conflito", relationshipBlockingPost: "Publicação bloqueada", viewLikes: "Ver difusões", spreadedDescription: "Lista de publicações difundidas pelo habitante.", + totalspreads: "Total de difusões", + attachFiles: "Anexar ficheiros", preview: "Pré-visualizar", publish: "Publicar", contentWarningPlaceholder: "Adiciona um assunto à publicação (opcional)", - publishWarningPlaceholder: "Corpo limitado a 7000 caracteres.", - publishTooLong: "A tua publicação é demasiado longa. Encurta-a, por favor.", + privateWarningPlaceholder: "Adiciona habitantes para enviar uma publicação privada (opcional)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "LEMBRA-TE: Devido à tecnologia blockchain, uma vez publicado, não pode ser editado nem eliminado.", ], commentWarning: [ "LEMBRA-TE: Devido à tecnologia blockchain, uma vez publicado, não pode ser editado nem eliminado.", ], + commentPublic: "público", + commentPrivate: "privado", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -357,6 +454,7 @@ module.exports = { ".", ], publishCustom: "Escrever publicação avançada", + subtopicLabel: "Criar um subtópico desta publicação", messagePreview: "Pré-visualização da publicação", mentionsMatching: "Menções correspondentes", mentionsName: "Nome", @@ -379,93 +477,21 @@ module.exports = { ssbLogStream: "Blokchain", ssbLogStreamDescription: "Configura o limite de mensagens para os fluxos da Blockchain.", saveSettings: "Guardar definições", + exportTitle: "Exportar dados", exportDescription: "Define uma senha (mínimo 32 caracteres) para encriptar a tua chave", exportDataTitle: "Cópia de segurança", exportDataDescription: "Descarrega os teus dados (chave secreta excluída!)", exportDataButton: "Descarregar base de dados", + pubWallet: "Carteira PUB", + pubWalletDescription: "Define o URL da carteira PUB. Será usado para transações PUB (incluindo o UBI).", + pubWalletConfiguration: "Guardar configuração", + importTitle: "Importar dados", importDescription: "Importa o teu segredo encriptado (chave privada) para ativar o teu avatar", + importAttach: "Anexar ficheiro encriptado (.enc)", + passwordLengthInfo: "A senha deve ter pelo menos 32 caracteres.", passwordImport: "Escreve a tua senha para decifrar os dados que serão guardados no teu diretório pessoal (nome: secret)", exportPasswordPlaceholder: "Usa minúsculas, maiúsculas, números e símbolos", - fileInfo: "A tua chave secreta encriptada será descarregada para o teu dispositivo (nome: oasis.enc)", - developer: "Desenvolvimento", - devTitle: "Desenvolvimento", - welcomeBannerText: "Bem-vinda ao OASIS. Segue estes passos rápidos para configurar o teu avatar.", - aiSuggestionBanner: "Sugestão:", - aiSuggestionsEnable: "Sugestões da IA", - welcomeBannerAction: "Começar!", - welcomeTitle: "Bem-vinda", - welcomeStepGreetingAction: "Enviar", - welcomeJoinPubButton: "Juntar-se ao pub", - welcomeStepLarpAction: "Juntar-se à \"Academia\"", - welcomeStepLarpText: "Junta-te ao nosso jogo de representação ao vivo.", - welcomeStepLarpTitle: "Junta-te ao L.A.R.P.", - welcomeStepGreetingTitle: "Envia um \"Olá mundo!\"", - welcomeStepGreetingText: "Envia uma mensagem para que outros habitantes te possam descobrir.", - welcomeJoinPub: "Juntar-se a", - welcomeDescription: "Algumas coisas que vale a pena fazer antes de começar.", - welcomeProgress: "Concluído", - welcomeStepDone: "feito", - welcomeStepPending: "pendente", - welcomeSkip: "Agora não", - welcomeStepLanguageTitle: "Escolhe o teu idioma", - welcomeStepLanguageText: "O Oasis fala vários idiomas. Podes mudar quando quiseres.", - welcomeStepProfileTitle: "Edita o teu perfil", - welcomeStepProfileText: "Escolhe um nome, acrescenta uma descrição e põe uma imagem para que os outros habitantes te reconheçam.", - welcomeStepProfileAction: "Guardar perfil", - welcomeStepFederationTitle: "Junta-te à rede principal", - welcomeStepFederationText: "Liga-te ao nosso pub para encontrar outros habitantes e começar a replicar.", - welcomeStepFederationAction: "Ir aos convites", - welcomeStepBackupTitle: "Faz cópia do teu ID", - welcomeStepBackupText: "A tua identidade é um ficheiro de chaves neste dispositivo.", - welcomeStepBackupWarning: "SE O PERDERES, NINGUÉM O PODE RECUPERAR POR TI.", - welcomeStepBackupAction: "Cópia de segurança!", - welcomeCompleteTitle: "Tudo pronto.", - welcomeCompleteText: "O teu nó está pronto. A partir daqui a rede cresce com as pessoas que segues.", - welcomeFindPeople: "Procurar habitantes", - devFilterFiles: "FICHEIROS", - devFilterSearch: "PROCURAR", - devFilterModules: "MÓDULOS", - devFilterTranslations: "TRADUÇÕES", - devFilterStyles: "ESTILOS", - devFilterTests: "TESTES", - devVersion: "Versão", - devNodeVersion: "Node", - devStatsTests: "Suites de testes", - devParentFolder: "Pasta superior", - devOpenFolder: "abrir", - devDescription: "Explora o código-fonte em execução neste nó.", - devRoot: "raiz", - devSearchPlaceholder: "Texto a procurar no código", - devAnyExtension: "Qualquer ficheiro", - devStatsFiles: "Ficheiros", - devStatsLines: "Linhas", - devStatsModules: "Módulos", - devStatsLanguages: "Idiomas", - devNotViewable: "não visualizável", - devEmptyFolder: "Esta pasta está vazia.", - devSize: "Tamanho", - devShowing: "A mostrar", - devLine: "linha", - devReportBug: "Reportar Bug", - devCreateTask: "Criar Tarefa", - devRaw: "RAW", - devReportContext: "Encontrado ao ler o código-fonte do Oasis", - devTaskPrefix: "Rever", - devPrevPage: "Linhas anteriores", - devNextPage: "Linhas seguintes", - devMatches: "correspondências", - devFilesScanned: "ficheiros analisados", - devNoResults: "Ainda sem correspondências.", - devColModule: "Módulo", - devColState: "Estado", - devColModel: "Modelo", - devColView: "Vista", - devColRoutes: "Rotas", - devColTests: "Testes", - devOn: "ativo", - devOff: "inativo", - modulesDevLabel: "Desenvolvimento", - modulesDevDescription: "Módulo para gerir o código-fonte do Oasis.", + fileInfo: "A tua chave secreta encriptada será guardada no teu diretório pessoal (nome: oasis.enc)", themeIntro: "Escolhe um tema.", setTheme: "Definir tema", @@ -473,13 +499,16 @@ module.exports = { languageDescription: "Se preferires usar outro idioma, seleciona-o aqui.", setLanguage: "Definir idioma", + peerConnections: "Pares", peerConnectionsIntro: "Gere todas as tuas ligações com outros pares.", peerConnectionsTitle: "Ligações", online: "Online", offline: "Offline", discovered: 'Descobertos', unknown: 'Desconhecido', + lanBroadcastLabel: "Difusão LAN", lanBroadcastActive: "Ativa (multicast UDP na LAN)", + lanBroadcastDisabled: "Desativada (modo pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -492,6 +521,8 @@ module.exports = { noConnections: "Nenhum par conectado.", noDiscovered: "Nenhum par descoberto.", noUnknownPeers: "Sem pares desconhecidos no grafo de amizades.", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -501,6 +532,9 @@ module.exports = { peerImport: "Importar", peerImportTitle: "Importar lista de peers", peerImportPlaceholder: "Cola um endereço multiserver por linha, ou carrega um ficheiro .txt…", + noSupportedConnections: "Nenhum par apoiado.", + noBlockedConnections: "Nenhum par bloqueado.", + noRecommendedConnections: "Nenhum par recomendado.", connectionActionIntro: "", startNetworking: "Iniciar rede", @@ -514,19 +548,9 @@ module.exports = { homePageDescription: "Seleciona qual módulo queres como página inicial.", saveHomePage: "Definir início", invites: "Convites", + invitesTitle: "Convites", + invitesInvites: "Convites", invitesDescription: "Gere e aplica códigos de convite na tua rede.", - invitesPubsHint: "Cola um código de convite de um pub para te federares com ele e replicar os seus habitantes.", - invitesFederationsHint: "Pubs com que estás federado: atualiza-os, remove os inacessíveis ou exporta a lista.", - invitesHousesHint: "Introduz um código de casa para entrares numa casa do LARP e participar na sua economia.", - invitesTribesHint: "Introduz um código de tribo para te tornares membro e aceder ao conteúdo privado.", - invitesChatsHint: "Introduz um código de chat para entrares na conversa e nos seus tópicos.", - invitesPadsHint: "Introduz um código de pad para escreverem juntos num documento partilhado.", - invitesCalendarsHint: "Introduz um código de calendário para partilhar datas e lembretes.", - invitesEventsHint: "Introduz um código de evento para entrares num evento privado.", - invitesForumsHint: "Introduz um código de fórum para leres e responderes num fórum privado.", - invitesMapsHint: "Introduz um código de mapa para veres e acrescentares lugares num mapa partilhado.", - invitesShopsHint: "Introduz um código de loja para entrares numa loja e no seu catálogo.", - invitesInhabitantsHint: "Introduz um código de habitante para se seguirem e trocarem conteúdo.", invitesTribesTitle: "Tribos", invitesTribeInviteCodePlaceholder: "Introduz código de convite da tribo", invitesTribeJoinButton: "Aderir à tribo", @@ -557,6 +581,7 @@ module.exports = { invitesAlreadyFederated: "Você já está federado com este pub.", invitesFederationsTitle: "Federações", invitesAcceptedInvites: "Federadas", + invitesNoInvites: "Ainda sem convites aceites.", invitesUnfollow: "Deixar de seguir", invitesFollow: "Seguir", invitesUnfollowedInvites: "Não federadas", @@ -576,24 +601,39 @@ module.exports = { panicMode: "Modo pânico!", encryptData: "Define uma senha (mínimo 32 caracteres) para encriptar a tua blockchain", decryptData: "Introduz a senha para decifrar a tua blockchain", + panicModeDescription: "Encriptar/Decifrar ou ELIMINAR a tua blockchain", removeDataDescription: "AVISO: Este processo não pode ser revertido.", + encryptPanicButton: "Encriptar blockchain", + decryptPanicButton: "Decifrar blockchain", removePanicButton: "ELIMINAR TODOS OS TEUS DADOS!", searchTitle: "Pesquisa", searchDescriptionLabel: "Pesquisa conteúdo na tua rede.", + searchLanguagesLabel: "Idiomas", + searchSkillsLabel: "Competências", searchPlaceholder:"Pesquisar conteúdo...", + searchDateLabel:"Data", searchLocationLabel:"Localização", searchPriceLabel:"Preço", + searchUrlLabel:"URL", searchCategoryLabel:"Categoria", + searchStartLabel:"Hora de início", + searchEndLabel:"Hora de fim", searchPriorityLabel:"Prioridade", searchStatusLabel:"Estado", statusLabel:"Estado", + totalVotesLabel:"Total de votos", noResultsFound:"Nenhum resultado encontrado.", votesOption:"Opções de voto", voteYesLabel:"Sim", voteNoLabel:"Não", allTypesLabel: "ILIMITADO", + ABSTENTIONLabel:"ABSTENÇÃO", YESLabel:"SIM", NOLabel:"NÃO", + FOLLOW_MAJORITYLabel: "SEGUIR MAIORIA", + CONFUSEDLabel: "CONFUSO", + NOT_INTERESTEDLabel: "SEM INTERESSE", + StatusLabel:"Estado", votesOptionYesLabel:"Sim", votesOptionNoLabel:"Não", votesOptionAbstentionLabel:"Abstenção", @@ -601,6 +641,7 @@ module.exports = { votesCreatedByLabel:"Criado por", voteStatusOpen:"ABERTO", voteStatusClosed:"FECHADO", + imageSearchLabel: "Introduz palavras para pesquisar imagens etiquetadas com elas.", commentDescription: ({ parentUrl }) => [ " comentou em ", a({ href: parentUrl }, " conversa"), @@ -611,20 +652,53 @@ module.exports = { a({ href: parentUrl }, " uma publicação"), ], subtopicTitle: ({ authorName }) => [`Subtópico na publicação de @${authorName}`], + mysteryDescription: "publicou uma publicação misteriosa", oasisDescription: "Rede do Projeto OASIS", searchSubmit: "Pesquisar...", + postLabel: "PUBLICAÇÕES", + aboutLabel: "HABITANTES", + feedLabel: "FEEDS", votesLabel: "VOTAÇÕES", + reportLabel: "DENÚNCIAS", + imageLabel: "IMAGENS", + videoLabel: "VÍDEOS", + audioLabel: "ÁUDIOS", + documentLabel: "DOCUMENTOS", + torrentLabel: "TORRENTS", pdfFallbackLabel: "Documento PDF", + eventLabel: "EVENTOS", + taskLabel: "TAREFAS", + transferLabel: "TRANSFERÊNCIAS", + curriculumLabel: "CURRÍCULO", + bookmarkLabel: "MARCADORES", + tribeLabel: "TRIBOS", + marketLabel: "MERCADO", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "CARTEIRAS", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "Enviar", + subjectLabel: "Assunto", editProfile: "Editar avatar", + profileQrAlt: "Código QR do perfil", + profileQrHint: "Lê para copiar este Oasis ID.", oasisIdLabel: "OasisID", - spreadContent: "Replicar", + spreadLabel: "Difundir", + spreadHint: "Difunde isto aos teus apoiantes (replica pelo teu feed).", welcomePmSubject: "Hello World!", - welcomePmBody: "Bem-vindo ao Oasis — uma rede social livre, peer-to-peer, federada e cifrada, com uma arquitetura distribuída apenas por convite.\n\nAlguns locais interessantes por onde começar:\n\n- /profile — Edita o teu avatar, nome, descrição e quais módulos aparecem no teu lado público.\n- /invites — Adiciona um pub para te federares com o resto da rede.\n- /peers — Vê quem está ligado, faz nova varredura da LAN, exporta ou importa listas de peers.\n- /inhabitants — Descobre e visita os avatares de outras pessoas.\n- /tribes — Cria ou junta-te a grupos privados com cifragem ponta-a-ponta.\n- /inbox — Lê e envia mensagens privadas.\n- /activity — Vê a atividade recente, tua e da tua rede.\n- /publish — Escreve um post ou partilha uma imagem, áudio, vídeo ou documento.\n- /search — Pesquise o conteúdo da rede por tipo.\n\nAlguns lugares interessantes para conectar:\n\n- /multiverse — Faça a gestão das suas outras contas do multiverso, incluindo enviar e receber conteúdo.\n\nEsta mensagem foi enviada de forma privada de ti para ti mesmo, por isso não foi necessária qualquer ligação para a receber.", + welcomePmBody: "Bem-vindo ao Oasis — uma rede social livre, peer-to-peer, federada e cifrada, com uma arquitetura distribuída apenas por convite.\n\nAlguns locais interessantes por onde começar:\n\n- /profile — Edita o teu avatar, nome, descrição e quais módulos aparecem no teu lado público.\n- /invites — Adiciona um pub para te federares com o resto da rede.\n- /peers — Vê quem está ligado, faz nova varredura da LAN, exporta ou importa listas de peers.\n- /inhabitants — Descobre e visita os avatares de outras pessoas.\n- /tribes — Cria ou junta-te a grupos privados com cifragem ponta-a-ponta.\n- /inbox — Lê e envia mensagens privadas.\n- /activity — Vê a atividade recente, tua e da tua rede.\n- /publish — Escreve um post ou partilha uma imagem, áudio, vídeo ou documento.\n\nAlguns lugares interessantes para conectar:\n\n- /fediverse — Faça a gestão das suas outras contas do fediverso, incluindo enviar e receber conteúdo.\n\nEsta mensagem foi enviada de forma privada de ti para ti mesmo, por isso não foi necessária qualquer ligação para a receber.", + profileVisibilityTitle: "Visibilidade do perfil público", + profileVisibilityHint: "Escolhe que campos os outros habitantes veem no teu perfil. Por defeito apenas Karma é mostrado.", profileSensorsSectionTitle: "Sensores", profileVisibilityActivity: "Nível de atividade", + profileVisibilityDevice: "Dispositivo", profileDeviceLockedHint: "Este sensor está sempre ativo: permite que outros vejam de que dispositivo te conectas e não pode ser desativado.", profileVisibilityKarma: "Pontuação KARMA", profileVisibilityUbi: "UBI", @@ -632,6 +706,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "Chave GPG", + profileVisibilityClearnet: "Publicar o meu perfil", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Lojas", profileClearnetJobsLabel: "Empregos", @@ -685,6 +760,7 @@ module.exports = { " ou o estado da ligação.", ], walletSentToLine: ({ destination, amount }) => `Enviado ECO ${amount} para ${destination}`, + walletSettingsTitle: "Carteira", walletSettingsDescription: "Integra o Oasis com a tua carteira ECOin.", walletSettingsDocLink: "Guia de instalação ECOin", walletStatusMessages: { @@ -706,19 +782,37 @@ module.exports = { cipherTitle: "Encriptação", cipherDescription: "Encripta e decifra o teu texto simetricamente (usando uma senha partilhada).", randomPassword: "Senha aleatória", + cipherEncryptTitle: "Encriptar texto", + cipherEncryptDescription: "Introduz o texto a encriptar", + cipherTextLabel: "Texto a encriptar", cipherTextPlaceholder: "Introduz o texto a encriptar...", cipherPasswordLabel: "Define uma senha (mínimo 32 caracteres) para encriptar o teu texto", cipherPasswordDecryptLabel: "Define uma senha (mínimo 32 caracteres) para decifrar o seu texto", cipherPasswordPlaceholder: "Introduz uma senha...", cipherEncryptButton: "Encriptar", + cipherDecryptTitle: "Decifrar texto", + cipherDecryptDescription: "Introduz o texto a decifrar", cipherEncryptedMessageLabel: "Texto encriptado", cipherDecryptedMessageLabel: "Texto decifrado", + cipherPasswordUsedLabel: "Senha usada para encriptar (guarda-a!)", cipherEncryptedTextPlaceholder: "Introduz o texto encriptado...", + cipherIvLabel: "IV", + cipherIvPlaceholder: "Introduz o vetor de inicialização...", cipherDecryptButton: "Decifrar", password: "Senha", text: "Texto", encryptedText: "Texto encriptado", iv: "Vetor de inicialização (IV)", + encryptTitle: "Encriptar o teu texto", + encryptDescription: "Introduz o texto que queres encriptar e fornece uma senha.", + encryptButton: "Encriptar", + decryptTitle: "Decifrar o teu texto", + decryptDescription: "Introduz o texto encriptado e fornece a mesma senha usada para encriptar.", + decryptButton: "Decifrar", + passwordLengthError: "A senha deve ter pelo menos 32 caracteres.", + missingFieldsError: "Texto, senha ou IV não fornecidos.", + encryptionError: "Erro ao encriptar o texto.", + decryptionError: "Erro ao decifrar o texto.", bookmarkTitle: "Marcadores", bookmarkDescription: "Descobre e gere marcadores na tua rede.", bookmarkAllSectionTitle: "Marcadores", @@ -744,6 +838,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "Opcional", bookmarkTagsLabel: "Tags", bookmarkTagsPlaceholder: "Introduz tags separadas por vírgulas", + bookmarkCategoryLabel: "Categoria", + bookmarkCategoryPlaceholder: "Opcional", bookmarkLastVisitLabel: "Última visita", bookmarkSearchPlaceholder: "Pesquisar URL, tags, categoria, autor...", bookmarkSortRecent: "Mais recentes", @@ -758,6 +854,8 @@ module.exports = { noLastVisit: "Sem última visita", videoTitle: "Vídeos", videoDescription: "Explora e gere conteúdo de vídeo na tua rede.", + videoPluginTitle: "Título", + videoPluginDescription: "Descrição", videoMineSectionTitle: "Os teus vídeos", videoCreateSectionTitle: "Carregar vídeo", videoUpdateSectionTitle: "Atualizar vídeo", @@ -789,6 +887,7 @@ module.exports = { videoSortOldest: "Mais antigos", videoSortTop: "Mais votados", videoSearchButton: "Pesquisa", + videoMessageAuthorButton: "MP", videoUpdatedAt: "Atualizado", videoNoMatch: "Nenhum vídeo corresponde à tua pesquisa.", documentTitle: "Documentos", @@ -829,6 +928,8 @@ module.exports = { documentUpdatedAt: "Atualizado", audioTitle: "Áudios", audioDescription: "Explora e gere conteúdo de áudio na tua rede.", + audioPluginTitle: "Título", + audioPluginDescription: "Descrição", audioMineSectionTitle: "Os teus áudios", audioCreateSectionTitle: "Carregar áudio", audioUpdateSectionTitle: "Atualizar áudio", @@ -839,6 +940,7 @@ module.exports = { audioFilterMine: "MEUS", audioFilterRecent: "RECENTES", audioFilterTop: "TOP", + audioFilterBlockchain: "BLOCKCHAIN", audioCreateButton: "Carregar áudio", audioUpdateButton: "Atualizar", audioDeleteButton: "Eliminar", @@ -865,7 +967,6 @@ module.exports = { audioFilterFavorites: "FAVORITOS", favoritesTitle: "Favoritos", favoritesDescription: "Todos os teus conteúdos favoritos num só lugar.", - favoritesSearchPlaceholder: "Pesquisar nos favoritos...", favoritesFilterAll: "TODOS", favoritesFilterRecent: "RECENTES", favoritesFilterAudios: "ÁUDIOS", @@ -881,13 +982,18 @@ module.exports = { favoritesNoItems: "Ainda sem favoritos.", yourContacts: "Os teus contactos", allInhabitants: "Habitantes", - allCVs: "CVs", + allCVs: "Todos os CVs", discoverPeople: "Descobre habitantes na tua rede.", + allInhabitantsButton: "TODOS", + contactsButton: "APOIOS", + CVsButton: "CVs", + matchSkills: "Combinar competências", + matchSkillsButton: "COMBINAR COMPETÊNCIAS", + suggestedButton: "SUGERIDOS", searchInhabitantsPlaceholder: "FILTRAR habitantes POR NOME …", filterLocation: "FILTRAR habitantes POR LOCALIZAÇÃO …", filterLanguage: "FILTRAR habitantes POR IDIOMA …", filterSkills: "FILTRAR habitantes POR COMPETÊNCIAS …", - cvSkillsCount: "Competências", applyFilters: "Aplicar filtros", locationLabel: "Localização", languagesLabel: "Idiomas", @@ -896,18 +1002,17 @@ module.exports = { mutualFollowers: "Seguidores mútuos", suggestedFollowsYou: "Segue-te", latestInteractions: "Interações recentes", + viewAvatar: "Ver Avatar", + viewCV: "Ver CV", suggestedSectionTitle: "Sugeridos", topkarmaSectionTitle: "Melhor Karma", topecoSectionTitle: "Melhor Eco", topactivitySectionTitle: "Top Atividade", - "TOP INACTIVITYButton": "MAIS INATIVOS", - topinactivitySectionTitle: "Habitantes menos ativos", blockedSectionTitle: "Bloqueados", gallerySectionTitle: "GALERIA", - topSectionTitle: "TOP", + blockedButton: "BLOQUEADOS", blockedLabel: "Habitante bloqueado", inhabitantviewDetails: "Ver detalhes", - cvVisitButton: "Visitar CV", keepReading: "Continuar lendo...", oasisId: "ID", noInhabitantsFound: "Ainda sem habitantes encontrados.", @@ -920,7 +1025,6 @@ module.exports = { parliamentFilterCandidatures: "CANDIDATURAS", parliamentFilterProposals: "PROPOSTAS", parliamentFilterLaws: "LEIS", - parliamentFilterRevocations: "REVOGAÇÕES", parliamentFilterHistorical: "HISTÓRICO", parliamentFilterLeaders: "LÍDERES", parliamentFilterRules: "REGRAS", @@ -946,8 +1050,10 @@ module.exports = { parliamentPoliciesDeclined: "LEIS REJEITADAS", parliamentPoliciesDiscarded: "LEIS DESCARTADAS", parliamentEfficiency: "% EFICIÊNCIA", + parliamentFilterRevocations: 'REVOGAÇÕES', parliamentRevocationFormTitle: 'Revogar lei', parliamentRevocationLaw: 'Lei', + parliamentRevocationTitle: 'Título', parliamentRevocationReasons: 'Motivos', parliamentRevocationPublish: 'Publicar revogação', parliamentCurrentRevocationsTitle: 'Revogações atuais', @@ -960,12 +1066,23 @@ module.exports = { parliamentNoGovernments: "Ainda sem governos.", parliamentCandidatureFormTitle: "Propor candidatura", parliamentCandidatureIdPh: "Oasis ID (@...) ou nome da tribo", + parliamentCandidatureSlogan: "Slogan (máx 140 caracteres)", + parliamentCandidatureSloganPh: "Um lema curto", parliamentCandidatureMethod: "Método", parliamentCandidatureProposeBtn: "Publicar candidatura", + parliamentThDate: "Data da proposta", + parliamentThSlogan: "Slogan", + parliamentThSince: "Perfil desde", + parliamentThVotes: "Votos recebidos", + parliamentThVoteAction: "Votar", + parliamentTypeUser: "Habitante", + parliamentTypeTribe: "Tribo", parliamentVoteBtn: "Votar", parliamentProposalFormTitle: "Propor lei", parliamentProposalDescription: "Descrição (≤1000)", parliamentProposalPublish: "Publicar proposta", + parliamentFinalize: "Finalizar", + parliamentDeadline: "Prazo", parliamentNoProposals: "Ainda sem propostas de lei.", parliamentNoLaws: "Ainda sem leis aprovadas.", parliamentMethodDEMOCRACY: "Democracia", @@ -983,21 +1100,29 @@ module.exports = { parliamentVotesSlashTotal: "Votos/Total", parliamentVotesNeeded: "Votos necessários", parliamentFutureLawsTitle: "Leis futuras", + parliamentNoFutureLaws: "Ainda sem leis futuras.", parliamentVoteAction: "Votar", + parliamentLeadersTitle: "Líderes", parliamentThLeader: "AVATAR", parliamentPopulation: "População", + parliamentThType: "Tipo", + parliamentThInPower: "No poder", parliamentThPresented: "CANDIDATURAS", parliamentNoLeaders: "Ainda sem líderes.", parliamentCandidaturesListTitle: "Lista de candidaturas", parliamentTimeRemaining: "Tempo restante", + parliamentNoLeader: "Ainda sem candidatura vencedora.", parliamentElectionsStatusTitle: "Próximo governo", parliamentProposalDeadlineLabel: "Prazo", parliamentProposalTimeLeft: "Tempo restante", + parliamentProposalOnTrack: "A caminho da aprovação", + parliamentProposalOffTrack: "Apoio insuficiente", parliamentRulesTitle: "Como funciona o Parlamento", parliamentRulesIntro: "As eleições resolvem-se a cada 2 meses; as candidaturas são contínuas e reiniciam quando um governo é escolhido.", parliamentRulesCandidates: "Qualquer habitante pode propor-se a si, a outro habitante ou a qualquer tribo. Cada habitante pode propor até 3 candidaturas por ciclo; duplicados no mesmo ciclo são rejeitados.", parliamentRulesElection: "O vencedor é a candidatura com mais votos no momento da resolução.", parliamentRulesTies: "Desempate: maior karma do habitante; se empatado, perfil mais antigo; se ainda empatado, proposta mais antiga; depois ordem lexicográfica por ID.", + parliamentRulesFallback: "Se ninguém votar, a última candidatura proposta vence. Se não houver candidaturas, uma tribo aleatória é selecionada.", parliamentRulesTerm: "O separador Governo mostra o governo atual e estatísticas.", parliamentRulesMethods: "Formas: Anarquia (maioria simples), Democracia (50%+1), Maioria (80%), Minoria (20%), Karmatocracia (propostas com maior karma), Ditadura (aprovação instantânea).", parliamentRulesAnarchy: "A Anarquia é o modo predefinido: se nenhuma candidatura for eleita na resolução, a Anarquia é proclamada. Sob Anarquia, qualquer habitante pode propor leis.", @@ -1019,7 +1144,11 @@ module.exports = { courtsCaseFormTitle: "Abrir caso", courtsCaseRespondent: "Acusado / Demandado", courtsCaseRespondentPh: "Oasis ID (@...) ou nome da tribo", + courtsCaseMediatorsAccuser: "Mediadores (acusação)", courtsCaseMethod: "Método de resolução", + courtsCaseDescription: "Descrição (máx 1000 caracteres)", + courtsCaseEvidenceTitle: "Provas do caso", + courtsCaseEvidenceHelp: "Anexa imagens, áudios, documentos (PDF) ou vídeos que sustentem o teu caso.", courtsCaseSubmit: "Apresentar caso", courtsNominateJudge: "Nomear juiz", courtsJudgeId: "Juiz", @@ -1064,6 +1193,22 @@ module.exports = { courtsRulesMisconduct: "Assédio, manipulação ou provas fabricadas podem levar a resolução negativa imediata.", courtsRulesGlossary: "Caso: registo de um conflito. Provas: materiais que sustentam alegações. Veredicto: decisão com medida. Recurso: pedido de revisão do veredicto.", courtsFilterActions: "AÇÕES", + courtsNoActions: "Sem ações pendentes para o teu papel.", + courtsCaseTitlePlaceholder: "Breve descrição do conflito", + courtsCaseSeverity: "Gravidade", + courtsCaseSeverityNone: "Sem etiqueta de gravidade", + courtsCaseSeverityLOW: "Baixa", + courtsCaseSeverityMEDIUM: "Média", + courtsCaseSeverityHIGH: "Alta", + courtsCaseSeverityCRITICAL: "Crítica", + courtsCaseSubject: "Tópico", + courtsCaseSubjectNone: "Sem etiqueta de tópico", + courtsCaseSubjectBEHAVIOUR: "Comportamento", + courtsCaseSubjectCONTENT: "Conteúdo", + courtsCaseSubjectGOVERNANCE: "Governação / regras", + courtsCaseSubjectFINANCIAL: "Financeiro / recursos", + courtsCaseSubjectOTHER: "Outro", + courtsHiddenRespondent: "Oculto (visível apenas para os envolvidos).", courtsThRole: "Papel", courtsRoleAccuser: "Acusador", courtsRoleDefence: "Defesa", @@ -1074,19 +1219,51 @@ module.exports = { courtsAssignJudgeBtn: "Escolher juiz", trendingTitle: "Tendências", exploreTrending: "Explora o conteúdo mais popular na tua rede.", + bookmarkButton: "MARCADORES", + transferButton: "TRANSFERÊNCIAS", + eventButton: "EVENTOS", + taskButton: "TAREFAS", votesButton: "VOTAÇÕES", - shopProductButton: "LOJA", + reportButton: "RELATÓRIOS", + feedButton: "FEED", + marketButton: "MERCADO", + imageButton: "IMAGENS", + audioButton: "ÁUDIOS", + videoButton: "VÍDEOS", + documentButton: "DOCUMENTOS", + torrentButton: "TORRENTS", + noTrendingFound: "Nenhum conteúdo em tendência encontrado.", + noContentMessage: "Ainda sem conteúdo em tendência disponível.", + trendingDescription: "Descrição", + trendingDate: "Data", + trendingLocation: "Localização", + trendingPrice: "Preço", + trendingUrl: "URL", + trendingCategory: "Categoria", + trendingStart: "Início", + trendingEnd: "Fim", + trendingPriority: "Prioridade", + trendingStatus: "Estado", + trendingFrom: "De", + trendingTo: "Para", + trendingConcept: "Conceito", + trendingAmount: "Montante", + trendingDeadline: "Prazo", + trendingItemStatus: "Estado do item", + trendingTotalVotes: "Total de votos", trendingTotalOpinions: "Total de opiniões", - trendingNoContentMessage: "Ainda não há tendências.", + trendingNoContentMessage: "Ainda sem conteúdo em tendência disponível.", + trendingAuthor: "Por", + trendingCreatedAtLabel: "Criado em", trendingTotalCount: "Contagem total", tasksTitle: "Tarefas", tasksDescription: "Descobre e gere tarefas na tua rede.", - taskSearchPlaceholder: "Pesquisar tarefas...", taskTitleLabel: "Título", taskDescriptionLabel: "Descrição", taskStartTimeLabel: "Hora de início", taskEndTimeLabel: "Hora de fim", taskPriorityLabel: "Prioridade", + taskPrioritySelect: "Selecionar prioridade", taskPriorityUrgent: "Urgente", taskPriorityHigh: "Alta", taskPriorityMedium: "Média", @@ -1096,13 +1273,13 @@ module.exports = { taskVisibilityLabel: "Visibilidade", taskPublic: "público", taskPrivate: "privado", + taskCreatedAt: "Criado em", + taskBy: "Por", taskStatus: "Estado", taskStatusOpen: "Abrir", taskStatusInProgress: "Em progresso", taskStatusClosed: "Fechado", taskAssignedTo: "Atribuído a", - taskAssignedChip: "Atribuída", - taskUnassignedChip: "Por atribuir", taskAssignees: "Atribuídos", taskAssignButton: "Atribuir a mim", taskUnassignButton: "Remover", @@ -1115,6 +1292,7 @@ module.exports = { taskFilterInProgress: "EM PROGRESSO", taskFilterClosed: "FECHADO", taskFilterAssigned: "ATRIBUÍDO", + taskFilterArchived: "ARQUIVADO", taskFilterUrgent: "URGENTE", taskFilterHigh: "ALTA", taskFilterMedium: "MÉDIA", @@ -1127,21 +1305,23 @@ module.exports = { taskInProgressTitle: "Tarefas em progresso", taskClosedTitle: "Tarefas fechadas", taskAssignedTitle: "Tarefas atribuídas", + taskArchivedTitle: "Tarefas arquivadas", + taskPublicTitle: "Tarefas públicas", + taskPrivateTitle: "Tarefas privadas", notasks: "Sem tarefas disponíveis.", noLocation: "Sem localização especificada", taskSetStatus: "Definir estado", + eventTitle: "Eventos", eventDateLabel: "Data", - eventRecurrenceLabel: "Recorrência", - eventRecurrenceUntil: "Repetir até", - eventRecurrenceHint: "Deixa a data final vazia para um evento único.", - eventUpcomingDates: "Próximas datas", eventsTitle: "Eventos", eventsDescription: "Descobre e gere eventos na tua rede.", - eventSearchPlaceholder: "Pesquisar eventos...", + eventDescription: "Descrição", eventPrice: "Preço", eventStatus: "Estado", + eventOrganizer: "Organizador", eventAllSectionTitle: "Eventos", eventMineSectionTitle: "Os teus eventos", + eventArchivedTitle: "Eventos arquivados", eventCreateSectionTitle: "Criar evento", eventUpdateSectionTitle: "Atualizar evento", eventDeleteButton: "Eliminar", @@ -1149,26 +1329,11 @@ module.exports = { eventAddToCalendar: "Adicionar ao calendário", eventVisitCalendar: "Ver calendário", viewTask: "Ver tarefa", - viewEvent: "Ver evento", - viewPad: "Ver pad", - viewMap: "Ver mapa", - viewCalendar: "Ver calendário", viewReport: "Ver relatório", viewTransfer: "Ver transferência", viewItem: "Ver item", viewShop: "Ver loja", viewProject: "Ver projeto", - viewJob: "Ver Emprego", - viewPlace: "Ver Lugar", - generatePdf: "Gerar PDF", - sharePm: "Partilhar por MP", - galleryImages: "Imagens (tamanho máx: 50MB cada)", - galleryPhotos: "Imagens", - galleryAddPhoto: "Adicionar Imagem", - galleryRemovePhoto: "Remover", - galleryVideo: "Vídeo (tamanho máx: 50MB)", - galleryAddVideo: "Adicionar Vídeo", - galleryRemoveVideo: "Remover vídeo", viewVotation: "Ver votação", voteCastTitle: "Emitir Voto", voteResults: "Resultados", @@ -1176,15 +1341,29 @@ module.exports = { eventDescriptionLabel: "Descrição", eventDescriptionPlaceholder: "Introduz a descrição do evento...", eventUpdateButton: "Atualizar", + eventAttendeesLabel: "Participantes", + eventAttendeesPlaceholder: "Introduz participantes, separados por vírgulas...", eventTagsLabel: "Tags", + eventTagsPlaceholder: "Introduz tags do evento, separadas por vírgulas...", + eventTags: "Tags", eventPriceLabel: "Preço", eventUrlLabel: "URL", eventAttendees: "Participantes", + noAttendees: "Ainda sem participantes", + eventCreatedAt: "Criado em", eventLocation: "Localização", eventLocationLabel: "Localização", + eventNoLocation: "Sem localização especificada", + eventNoURL: "Sem URL especificado", + eventBy: "Por", noevents: "Sem eventos disponíveis.", eventDate: "Data", + eventDateFormat: "DD:MM:AAAA HH:mm", eventAttendButton: "Participar no evento", + eventUnattendButton: "Sair do evento", + eventCreatedBy: "Criado por", + eventAttendeesCount: "Número de participantes", + eventCreatedByYou: "Criaste este evento", eventFilterAll: "TODOS", eventFilterMine: "MEUS", eventFilterToday: "HOJE", @@ -1192,9 +1371,18 @@ module.exports = { eventFilterMonth: "ESTE MÊS", eventFilterYear: "ESTE ANO", eventFilterArchived: "ARQUIVADO", + eventTodayTitle: "Eventos de hoje", + eventThisWeekTitle: "Eventos desta semana", + eventThisMonthTitle: "Eventos deste mês", + eventThisYearTitle: "Eventos deste ano", eventPrivacyLabel: "Visibilidade", eventPublic: "Público", eventPrivate: "Privado", + eventPublicTitle: "Eventos públicos", + eventNoPrice: "Evento gratuito", + eventNoImage: "Sem imagem carregada", + eventAttendConfirmation: "You are now attending this event", + eventUnattendConfirmation: "You are no longer attending this event", eventAttended: "Presente", eventUnattended: "Ausente", eventStatusOpen: "Abrir", @@ -1205,14 +1393,9 @@ module.exports = { tagsTopSectionTitle: "Tags populares", tagsCloudSectionTitle: "Nuvem de tags", tagsFilterAll: "TODOS", - tagsFilterMine: "MINHAS", - tagsFilterRecent: "RECENTES", - tagsMineSectionTitle: "As minhas etiquetas", - tagsRecentSectionTitle: "Etiquetas recentes", tagsFilterTop: "TOP", tagsFilterCloud: "NUVEM", tagsNoItems: "Sem tags disponíveis.", - tagsSearchPlaceholder: "Procurar etiquetas...", tagsTableHeaderTag: "TAG/LINK:", tagsTableHeaderCount: "CONTADOR:", transfersTitle: "Transferências", @@ -1252,12 +1435,18 @@ module.exports = { transfersDeadline: "Prazo", transfersTags: "Tags", transfersStatus: "Estado", + transfersStatusUnconfirmed: "NÃO CONFIRMADO", + transfersStatusClosed: "FECHADO", + transfersStatusDiscarded: "DESCARTADO", + transfersCreatedAt: "Criado em", transfersConfirmations: "CONFIRMAÇÕES", - transfersExportContract: "Gerar Fatura", + transfersContainingBlock: "Bloco contendor", + transfersExportContract: "Criar contrato", transfersConfirmButton: "Confirmar transferência", transfersNoItems: "Sem transferências encontradas.", transfersMineSectionTitle: "As tuas transferências", transfersUBISectionTitle: "Transferências RBU", + transfersMarketSectionTitle: "Transferências de mercado", transfersTopSectionTitle: "Transferências mais votadas", transfersPendingSectionTitle: "Transferências pendentes", transfersUnconfirmedSectionTitle: "Transferências não confirmadas", @@ -1265,13 +1454,21 @@ module.exports = { transfersDiscardedSectionTitle: "Transferências descartadas", transfersCreateSectionTitle: "Criar transferência", transfersAllSectionTitle: "Transferências", + transfersFilterFavs: "Favoritos", + transfersFavsSectionTitle: "Transferências favoritas", + transfersSearchLabel: "Pesquisa", transfersSearchPlaceholder: "Pesquisar conceito, tags, utilizadores...", transfersMinAmountLabel: "Montante mínimo", transfersMaxAmountLabel: "Montante máximo", + transfersSortLabel: "Ordenar por", transfersSortRecent: "Mais recentes", transfersSortAmount: "Maior montante", transfersSortDeadline: "Prazo mais próximo", transfersSearchButton: "Pesquisa", + transfersFavoriteButton: "Favorito", + transfersUnfavoriteButton: "Remover favorito", + transfersMessageUserButton: "Mensagem", + transfersExpiringSoonBadge: "A EXPIRAR", transfersExpiredBadge: "EXPIRADO", transfersUpdatedAt: "Atualizado", transfersNoMatch: "Nenhuma transferência corresponde à tua pesquisa.", @@ -1284,9 +1481,6 @@ module.exports = { voteClosedTitle: "Votações fechadas", voteAllSectionTitle: "Votações", voteCreateButton: "Criar votação", - voteErrorTitle: "Verifica o formulário", - voteErrorDeadlineMin: "O prazo tem de ser de pelo menos {days} dias a partir de agora.", - voteErrorGeneric: "Não foi possível guardar a votação.", voteUpdateButton: "Atualizar", voteDeleteButton: "Eliminar", voteOptionYes: "YES", @@ -1316,6 +1510,16 @@ module.exports = { voteNoQuestion: "Sem questão fornecida", voteUnknownCreator: "Criador desconhecido", voteUnknownDate: "Data desconhecida", + errorVoteNotFound: "Votação não encontrada", + errorAlreadyVoted: "Já deste a tua opinião.", + errorVoteClosedCannotEdit: "Não podes editar uma votação fechada", + errorVoteDeadlinePassed: "O prazo desta votação já passou", + errorRetrievingVote: "Erro ao obter votação", + errorCreatingVote: "Erro ao criar votação", + errorVoteAlreadyVoted: "Não é possível editar após a opinião ter sido emitida", + errorDeletingOldVote: "Erro ao eliminar opinião anterior", + errorCreatingUpdatedVote: "Erro ao criar opinião atualizada", + errorCreatingTombstone: "Erro ao criar tombstone", voteDetailSectionTitle: 'Detalhes da votação', voteCommentsLabel: 'Comentários', voteCommentsForumButton: 'Abrir discussão', @@ -1325,6 +1529,7 @@ module.exports = { voteNewCommentButton: 'Publicar comentário', voteNewCommentLabel: 'Adicionar comentário', cvTitle: "Currículo", + cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Editar CV", cvCreateSectionTitle: "Criar CV", cvDescription: "Gere e partilha as tuas competências e informações profissionais.", @@ -1343,121 +1548,29 @@ module.exports = { cvLocationLabel: "Localização", cvStatusLabel: "Estado", cvPreferencesLabel: "Preferências", + cvOasisContributorLabel: "Contribuidor Oasis", cvPersonal: "Pessoal", cvOasis: "Contribuidor Oasis (opcional)", - cvOasisContributorView: "Contribuição", + cvOasisContributorView: "Contribuição Oasis", cvEducational: "Educativo (opcional)", cvEducationalView: "Educativo", cvProfessional: "Profissional (opcional)", cvProfessionalView: "Profissional", cvAvailability: "Disponibilidade (opcional)", + cvAvailabilityView: "Disponibilidade", cvUpdateButton: "Atualizar", cvCreateButton: "Criar CV", + cvContactLabel: "Contacto", cvCreatedAt: "Criado em", cvUpdatedAt: "Atualizado em", cvEditButton: "Atualizar", cvDeleteButton: "Eliminar", cvNoCV: "Sem CV encontrado.", blogSubject: "Assunto", - blogMessage: "Mensagem", + blogMessage: "Corpo limitado a 8096 caracteres.", blogImage: "Carregar mídia (máx: 50MB)", - blogMedia: "Anexos (até 8 imagens e um vídeo)", blogPublish: "Pré-visualizar", - pollsTitle: "Sondagens", - dataTitle: "Correspondências", - dataDescription: "Explora e visualiza as correspondências da tua rede.", - dataFilterAll: "TUDO", - dataFilterMine: "MEUS", - dataFilterRecent: "RECENTES", - dataFilterTop: "TOP", - dataKindInhabitants: "HABITANTES", - dataKindJobs: "EMPREGOS", - dataKindProjects: "PROJETOS", - dataKindEvents: "EVENTOS", - dataKindTribes: "TRIBOS", - dataKindMarket: "MERCADO", - dataKindHousing: "HABITAÇÃO", - dataKindIndustry: "INDÚSTRIA", - dataKindTasks: "TAREFAS", - dataKindReports: "RELATÓRIOS", - dataKindVotes: "VOTAÇÕES", - dataSearchPlaceholder: "Procurar nos dados cruzados...", - dataCohesionTitle: "Coeficiente de coesão (CC)", - dataCcShort: "CC", - dataCohesionHint: "Afinidade média entre tudo o que a rede publicou.", - dataAffinity: "Afinidade", - dataCommonTerms: "Palavra-chave", - dataStatPeople: "CVs", - dataStatConnected: "Ligados", - dataStatSkills: "Habilidades", - dataBestMatch: "Melhor match", - dataStatIsolated: "Isolados", - dataStatEntities: "Entidades comparadas", - dataStatTerms: "Termos distintos", - dataStatPairs: "Pares ligados", - dataMatchesTitle: "Correspondências", - dataMatchesHint: "Sugestões personalizadas do algoritmo.", - dataTermsTitle: "Palavras-chave", - dataTermsHint: "Os termos que aparecem em mais conteúdos.", - dataConnections: "Correspondências ligadas", - dataTopicTitle: "Tudo o que se relaciona com", - dataTopicHint: "O que o algoritmo relaciona com este tema", - dataNoMatches: "Sem dados cruzados para mostrar.", - dataNoProfile: "Publica conteúdo ou escreve o teu CV para que o algoritmo aprenda o que te interessa.", - pollsDescription: "Módulo para perguntar à rede e contar as respostas.", - pollFilterAll: "TODAS", - pollFilterMine: "MINHAS", - pollFilterRecent: "RECENTES", - pollFilterTop: "TOP", - pollFilterVoted: "VOTADAS", - pollFilterOpen: "ABERTAS", - pollFilterClosed: "FECHADAS", - pollCreateButton: "Criar Sondagem", - pollCreateTitle: "Criar Sondagem", - pollEditTitle: "Editar Sondagem", - pollQuestion: "Pergunta", - pollQuestionPlaceholder: "O que queres perguntar?", - pollOptions: "Opções (uma por linha)", - pollOutcome: "Resultado", - pollNoVotesYet: "Ainda sem votos", - pollTie: "Empate", - pollOptionsPlaceholder: "Praça\nParque\nBar", - pollAnonymous: "Anónima", - pollAnonymousLabel: "Sondagem anónima", - pollMultiple: "Escolha múltipla", - pollMultipleLabel: "Permitir várias respostas", - pollDeadline: "Prazo", - pollTags: "Etiquetas", - pollPublishButton: "Publicar Sondagem", - pollUpdateButton: "Atualizar", - pollCloseButton: "Fechar", - pollDeleteButton: "Eliminar", - pollVoteButton: "Votar", - pollChangeVote: "Mudar Voto", - pollVoters: "Votantes", - pollComments: "Comentários", - pollStatusLabel: "Estado", - pollStatusOpen: "ABERTA", - pollStatusClosed: "FECHADA", - pollSearchPlaceholder: "Procurar sondagens...", - pollsNoItems: "Nenhuma sondagem encontrada.", - viewPoll: "Ver Sondagem", - pollChatCreate: "Criar Sondagem", - pollInChat: "Sondagem", - blogTitle: "Blogs", - blogDescription: "Módulo para descobrir e gerir blogs.", - blogFilterAll: "TODOS", - blogFilterMine: "MEUS", - blogFilterRecent: "RECENTES", - blogFilterFavorites: "FAVORITOS", - blogFilterTop: "TOP", - blogCreateButton: "Criar Blog", - blogSearchPlaceholder: "Procurar blogs...", - blogComments: "Comentários", - blogAllowComments: "Permitir comentários", - blogCommentsClosed: "O autor fechou os comentários deste blog.", - blogNoItems: "Nenhum blog encontrado.", - viewBlog: "Ver Blog", + noPopularMessages: "Ainda sem mensagens populares publicadas", forumTitle: "Fóruns", forumCategoryLabel: "Categoria", forumTitleLabel: "Título", @@ -1465,22 +1578,43 @@ module.exports = { forumCreateButton: "Criar fórum", forumCreateSectionTitle: "Criar fórum", forumDescription: "Conversa abertamente com outros habitantes na tua rede.", - forumSearchPlaceholder: "Pesquisar fóruns...", forumFilterAll: "TODOS", forumFilterMine: "MEUS", forumFilterRecent: "RECENTES", forumFilterTop: "TOP", + forumMineSectionTitle: "Os teus fóruns", + forumRecentSectionTitle: "Fóruns recentes", + forumAllSectionTitle: "Fóruns", forumDeleteButton: "Eliminar", forumParticipants: "participantes", forumMessages: "mensagens", + forumLastMessage: "Última mensagem", forumMessageLabel: "Mensagem", forumMessagePlaceholder: "Escreve a tua mensagem...", forumSendButton: "Enviar", + forumVisitForum: "Visitar fórum", noForums: "Sem fóruns encontrados.", - forumVisitButton: "Ver Fórum", - forumTopicLabel: "Tópico", + forumVisitButton: "Visitar fórum", + forumCatGENERAL: "Geral", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", + forumCatPOLITICS: "Política", + forumCatTECH: "Tecnologia", + forumCatSCIENCE: "Ciência", + forumCatMUSIC: "Música", + forumCatART: "Arte", + forumCatGAMING: "Jogos", + forumCatBOOKS: "Livros", + forumCatFILMS: "Filmes", + forumCatPHILOSOPHY: "Filosofia", + forumCatSOCIETY: "Sociedade", + forumCatPRIVACY: "Privacidade", + forumCatCYBERWARFARE: "Guerra cibernética", + forumCatSURVIVALISM: "Sobrevivencialismo", imageTitle: "Imagens", imageDescription: "Explora e gere conteúdo de imagens na tua rede.", + imagePluginTitle: "Título", + imagePluginDescription: "Descrição", imageMineSectionTitle: "As tuas imagens", imageCreateSectionTitle: "Carregar imagem", imageUpdateSectionTitle: "Atualizar imagem", @@ -1489,12 +1623,14 @@ module.exports = { imageTopSectionTitle: "Imagens mais votadas", imageFavoritesSectionTitle: "Favoritos", imageGallerySectionTitle: "Galeria", + imageMemeSectionTitle: "Memes", imageFilterAll: "TODOS", imageFilterMine: "MEUS", imageFilterRecent: "RECENTES", imageFilterTop: "TOP", imageFilterFavorites: "FAVORITOS", imageFilterGallery: "GALERIA", + imageFilterMeme: "MEMES", imageCreateButton: "Carregar imagem", imageUpdateButton: "Atualizar", imageDeleteButton: "Eliminar", @@ -1507,6 +1643,7 @@ module.exports = { imageTitlePlaceholder: "Opcional", imageDescriptionLabel: "Descrição", imageDescriptionPlaceholder: "Opcional", + imageMemeLabel: "MEME?", imageNoFile: "Nenhum ficheiro de imagem fornecido", noImages: "Sem imagens disponíveis.", imageSearchPlaceholder: "Pesquisar título, tags, descrição, autor...", @@ -1514,6 +1651,7 @@ module.exports = { imageSortOldest: "Mais antigos", imageSortTop: "Mais votados", imageSearchButton: "Pesquisa", + imageMessageAuthorButton: "Mensagem", imageUpdatedAt: "Atualizado", imageNoMatch: "Nenhuma imagem corresponde à tua pesquisa.", feedTitle: "Feed", @@ -1521,6 +1659,7 @@ module.exports = { createFeedButton: "Enviar Feed!", feedPlaceholder: "O que se passa? (máx 280 caracteres)", TODAYButton: "HOJE", + CREATEButton: "Criar Feed", totalOpinions: "Total de opiniões", moreVoted: "Mais votado", noFeedsFound: "Sem feeds encontrados.", @@ -1543,12 +1682,20 @@ module.exports = { concept: "Conceito", description: "Descrição", meme: "Meme", + activityContact: "Contacto", + activityBy: "Nome", + activityPixelia: "Novo pixel adicionado", + viewImage: "Ver imagem", + playAudio: "Reproduzir áudio", + playVideo: "Reproduzir vídeo", typeRecent: "RECENTES", - typeTop: "TOP", + errorActivity: "Erro ao obter atividade", typePost: "BLOGS", + recentButton: "RECENTES", typeTribe: "TRIBOS", typeLarp: "L.A.R.P.", typeInhabitants: "HABITANTES", + activityProfileUpdated: "atualizou seu perfil", typeAbout: "HABITANTES", typeCurriculum: "CVS", typeImage: "IMAGENS", @@ -1592,6 +1739,8 @@ module.exports = { typeCourtsSettlementAccepted: "Tribunais · Acordo aceite", typeCourtsNomination: "Tribunais · Nomeação", typeCourtsNominationVote: "Tribunais · Voto de nomeação", + activitySupport: "Nova aliança formada", + activityJoin: "Novo PUB aderido", question: "Pergunta", deadline: "Prazo", status: "Estado", @@ -1605,8 +1754,12 @@ module.exports = { date: "Data", category: "Categoria", attendees: "Participantes", + activitySpread: "->", + visitLink: "Visitar link", + viewDocument: "Ver documento", location: "Localização", contentWarning: "Assunto", + personName: "Nome do habitante", bankWalletConnected: "Carteira ECOin", bankUbiReceived: "UBI recebido", bankTx: "Tx", @@ -1616,6 +1769,7 @@ module.exports = { link: "Link", activityProjectFollow: "%OASIS% está agora a %ACTION% este projeto %PROJECT%", activityProjectUnfollow: "%OASIS% está agora a %ACTION% este projeto %PROJECT%", + activityProjectPledged: "%OASIS% %ACTION% %AMOUNT% ao projeto %PROJECT%", following: "A SEGUIR", unfollowing: "A DEIXAR DE SEGUIR", pledged: "COMPROMETIDO", @@ -1661,17 +1815,26 @@ module.exports = { courtsVotesSlashTotal: "YES/TOTAL", courtsOpenVote: "Open vote", courtsAnswerTitle: "Answer", + courtsStanceADMIT: "Admit", + courtsStanceDENY: "Deny", + courtsStancePARTIAL: "Partial", + courtsStanceCOUNTERCLAIM: "Reconvenção", + courtsStanceNEUTRAL: "Neutro", courtsVerdictResult: "Result", courtsVerdictOrders: "Orders", courtsSettlementText: "Settlement", courtsSettlementAccepted: "Aceite", + courtsSettlementPending: "Pendente", courtsJudge: "Juiz", courtsThSupports: "Supports", courtsFilterOpenCase: 'Open Case', + courtsEvidenceFileLabel: 'Ficheiro de prova (imagem, áudio, vídeo ou PDF)', + courtsCaseMediators: 'Mediadores', courtsCaseMediatorsPh: 'Mediator Oasis IDs, separated by comma', courtsMediatorsLabel: 'Mediadores', courtsThCase: 'Case', courtsThCreatedAt: 'Start date', + courtsThActions: 'Actions', courtsPublicPrefLabel: 'Visibility after resolution', courtsPublicPrefYes: 'I agree this case can be fully public', courtsPublicPrefNo: 'I prefer to keep the details private', @@ -1679,11 +1842,8 @@ module.exports = { courtsMethodMEDIATION: 'Mediation', courtsNoCases: 'No cases.', reportsTitle: "Relatórios", - reportsDescription: "Gere e acompanha os relatos da tua rede.", - reportsSearchPlaceholder: "Pesquisar relatórios...", + reportsDescription: "Gere e acompanha relatórios relacionados com problemas, bugs, abusos e avisos de conteúdo na tua rede.", reportsFilterAll: "TODOS", - reportsFilterRecent: "RECENTES", - reportsFilterTop: "TOP", reportsFilterMine: "MEUS", reportsFilterFeatures: "FUNCIONALIDADES", reportsFilterBugs: "BUGS", @@ -1696,13 +1856,18 @@ module.exports = { reportsFilterInvalid: "INVÁLIDO", reportsCreateButton: "Criar relatório", reportsTitleLabel: "Título", + reportsDescriptionLabel: "Descrição", + reportsDescriptionPlaceholder: "Por favor, fornece uma descrição detalhada.", reportsCategory: "Categoria", + reportsCategoryLabel: "Categoria", reportsCategoryFeatures: "Funcionalidades", reportsCategoryBugs: "Bugs", reportsCategoryAbuse: "Abuso", - reportsCategoryContent: "Conteúdo", + reportsCategoryContent: "Problemas de conteúdo", reportsUpdateButton: "Atualizar", reportsDeleteButton: "Eliminar", + reportsDateLabel: "Data", + reportsUploadFile: "Carregar mídia (máx: 50MB)", reportsMineSectionTitle: "Os teus relatórios", reportsFeaturesSectionTitle: "Pedidos de funcionalidades", reportsBugsSectionTitle: "Bugs", @@ -1710,6 +1875,11 @@ module.exports = { reportsContentSectionTitle: "Problemas de conteúdo", reportsAllSectionTitle: "Relatórios", reportsNoItems: "Sem relatórios disponíveis.", + reportsValidationTitle: "Por favor, introduz um título válido.", + reportsValidationDescription: "A descrição não pode estar vazia.", + reportsValidationCategory: "Por favor, seleciona uma categoria.", + reportsCreatedAt: "Criado em", + reportsCreatedBy: "By", reportsSeverity: "Gravidade", reportsSeverityLow: "Baixa", reportsSeverityMedium: "Média", @@ -1720,10 +1890,13 @@ module.exports = { reportsStatusUnderReview: "Em análise", reportsStatusResolved: "Resolvido", reportsStatusInvalid: "Inválido", + reportsUpdateStatusButton: "Atualizar estado", + reportsAnonymityOption: "Enviar anonimamente", + reportsAnonymousAuthor: "Anónimo", reportsConfirmButton: "CONFIRMAR RELATÓRIO!", reportsConfirmations: "Confirmações", reportsConfirmedSectionTitle: "Relatórios confirmados", - reportsCreateTaskButton: "Criar Tarefa", + reportsCreateTaskButton: "CRIAR TAREFA", reportsOpenSectionTitle: "Relatórios abertos", reportsUnderReviewSectionTitle: "Relatórios em análise", reportsResolvedSectionTitle: "Relatórios resolvidos", @@ -1767,20 +1940,6 @@ module.exports = { reportsRequestedActionLabel: 'Ação solicitada', reportsRequestedActionPlaceholder: 'Remover, ocultar, etiquetar, avisar, etc.', tribesTitle: "Tribos", - tribeFilterAll: "TODAS", - tribeFilterRecent: "RECENTES", - tribeFilterMine: "MINHAS", - tribeFilterMembership: "ADESÃO", - tribeFilterSubtribes: "SUBTRIBOS", - tribeFilterTop: "TOP", - tribeFilterGallery: "GALERIA", - tribeFeedFilterTOP: "TOP", - tribeFeedFilterMINE: "MEUS", - tribeFeedFilterALL: "TODOS", - tribeFeedFilterRECENT: "RECENTES", - courtsStanceDENY: "Negar", - courtsStanceADMIT: "Admitir", - courtsStancePARTIAL: "Admitir em parte", tribeAllSectionTitle: "Tribos", tribeMineSectionTitle: "As tuas tribos", tribeCreateSectionTitle: "Criar tribo", @@ -1792,6 +1951,13 @@ module.exports = { tribeviewSubTribeButton: "Visitar sub-tribo", tribeRootLabel: "RAIZ", tribeDescription: "Explora ou cria tribos na tua rede.", + tribeFilterAll: "TODOS", + tribeFilterMine: "MEUS", + tribeFilterMembership: "MEMBROS", + tribeFilterRecent: "RECENTES", + tribeFilterTop: "TOP", + tribeFilterSubtribes: "SUB-TRIBOS", + tribeFilterGallery: "GALERIA", tribeMainTribeLabel: "TRIBO PRINCIPAL", tribeCreateButton: "Criar tribo", tribeUpdateButton: "Atualizar", @@ -1810,8 +1976,13 @@ module.exports = { tribeModeLabel: "MODE", tribeIsAnonymousLabel: "ESTADO", tribeMembersCount: "Members", + tribeInviteCodePlaceholder: "Introduz código de convite", + tribeJoinByCodeButton: "Aderir com código", + tribeJoinButton: "ADERIR", tribeEnterInvite: "INSERIR CONVITE", tribeLeaveButton: "SAIR", + tribeYes: "SIM", + tribeNo: "NÃO", tribePublic: "PÚBLICO", tribePrivate: "PRIVADO", tribeGenerateInvite: "CONVITE ÚNICO", @@ -1820,8 +1991,13 @@ module.exports = { tribeRemoveInvitation: "REMOVER CONVITE", tribeCreatedAt: "Criado em", tribeAuthor: "Por", + tribeAuthorLabel: "AUTOR", tribeStrict: "Restrito", tribeOpen: "Abrir", + tribeFeedFilterRECENT: "RECENTES", + tribeFeedFilterMINE: "MEUS", + tribeFeedFilterALL: "TODOS", + tribeFeedFilterTOP: "TOP", tribeFeedRefeeds: "Republicações", tribeFeedRefeed: "Republicar", tribeFeedMessagePlaceholder: "Escreve um feed…", @@ -1831,12 +2007,17 @@ module.exports = { tribeNotFound: "Tribo não encontrada!", createTribeTitle: "Criar tribo", updateTribeTitle: "Atualizar tribo", + tribeSectionOverview: "Visão geral", tribeSectionInhabitants: "Habitantes", tribeSectionVotations: "VOTAÇÕES", tribeSectionEvents: "EVENTOS", + tribeSectionReports: "Relatórios", tribeSectionTasks: "TAREFAS", tribeSectionFeed: "FEED", tribeSectionForum: "FÓRUM", + tribeSectionMarket: "Mercado", + tribeSectionJobs: "Empregos", + tribeSectionProjects: "Projetos", tribeSectionMedia: "Média", tribeSectionImages: "IMAGENS", tribeSectionAudios: "ÁUDIOS", @@ -1874,6 +2055,11 @@ module.exports = { tribeTaskStatusClosed: "FECHAR", tribeTaskAssign: "ATRIBUIR", tribeTaskUnassign: "REMOVER", + tribeReportCreate: "Criar relatório", + tribeReportsEmpty: "Ainda sem relatórios.", + tribeReportTitle: "Título", + tribeReportDescription: "Descrição", + tribeReportCategory: "Categoria", tribeVotationCreate: "Criar votação", tribeVotationsEmpty: "Ainda sem votações.", tribeVotationTitle: "Título", @@ -1891,6 +2077,27 @@ module.exports = { tribeForumCategory: "Categoria", tribeForumReply: "Responder", tribeForumReplies: "Respostas", + tribeMarketCreate: "Criar anúncio", + tribeMarketEmpty: "Ainda sem anúncios.", + tribeMarketTitle: "Título", + tribeMarketDescription: "Descrição", + tribeMarketPrice: "Preço", + tribeMarketImage: "Imagem", + tribeMarketCategory: "Categoria", + tribeJobCreate: "Criar emprego", + tribeJobsEmpty: "Ainda sem empregos.", + tribeJobTitle: "Título", + tribeJobDescription: "Descrição", + tribeJobLocation: "Localização", + tribeJobSalary: "Salário", + tribeJobDeadline: "Prazo", + tribeProjectCreate: "Criar projeto", + tribeProjectsEmpty: "Ainda sem projetos.", + tribeProjectTitle: "Título", + tribeProjectDescription: "Descrição", + tribeProjectGoal: "Objetivo", + tribeProjectFunded: "Financiado", + tribeProjectDeadline: "Prazo", tribeMediaUpload: "Carregar média", readDocument: "Ler Documento", tribeCreateImage: "Criar Imagem", @@ -1901,6 +2108,7 @@ module.exports = { tribeMediaEmpty: "Ainda sem média.", tribeMediaTitle: "Título", tribeMediaDescription: "Descrição", + tribeMediaType: "Tipo", tribeMediaTypeImage: "Imagem", tribeMediaTypeVideo: "Vídeo", tribeMediaTypeAudio: "Áudio", @@ -1910,6 +2118,11 @@ module.exports = { tribeInviteCodeText: "Código de convite: ", oasisVersionLabel: "Versão do Oasis", tribeInviteCodeHint: "Partilha este código com quem queres convidar. Pode entrar via /invites → Tribes.", + tribeGroupTribe: "Tribo", + tribeGroupOffice: "Escritório", + tribeGroupNetwork: "Rede", + tribeGroupEconomy: "Economia", + tribeGroupMedia: "Média", tribeStatusOpen: "ABERTO", tribeStatusClosed: "FECHADO", tribeStatusInProgress: "EM PROGRESSO", @@ -1919,17 +2132,33 @@ module.exports = { tribePriorityCritical: "CRÍTICA", tribeTaskFilterAll: "TODOS", tribeMediaFilterAll: "TODOS", + tribeReportCatBug: "BUG", + tribeReportCatAbuse: "ABUSO", + tribeReportCatContent: "CONTEÚDO", + tribeReportCatOther: "OUTRO", tribeForumCatGeneral: "GERAL", tribeForumCatProposal: "PROPOSTA", tribeForumCatQuestion: "PERGUNTA", tribeForumCatAnnouncement: "ANÚNCIO", + tribeMarketCatGoods: "BENS", + tribeMarketCatServices: "SERVIÇOS", + tribeMarketCatFood: "ALIMENTAÇÃO", + tribeMarketCatOther: "OUTRO", tribeStatusLabel: "Estado", tribeSubTribes: "SUB-TRIBOS", tribeSubTribesCreate: "Criar Sub-Tribo", tribeSubTribesStrictDenied: "O modo estrito da tribo não permite criar novas sub-tribos. Por favor, entre em contato com o administrador.", + tribeSubTribesEmpty: "Ainda sem sub-tribos criadas.", + tribeActivityJoined: "ADERIU", + tribeActivityLeft: "SAIU", + tribeActivityFeed: "FEED", tribeActivityRefeed: "REPUBLICAÇÃO", + tribeGroupAnalytics: "Análises", + tribeGroupCreative: "Criativo", tribeSectionActivity: "ATIVIDADE", tribeSectionTrending: "TENDÊNCIAS", + tribeSectionOpinions: "OPINIÕES", + tribeSectionPixelia: "PIXELIA", tribeSectionTags: "TAGS", tribeSectionSearch: "PESQUISA", tribeActivityEmpty: "Ainda sem atividade.", @@ -1941,15 +2170,25 @@ module.exports = { tribeTrendingPeriodWeek: "Esta semana", tribeTrendingPeriodAll: "Todo o tempo", tribeTrendingEngagement: "envolvimento", + tribeOpinionsEmpty: "Ainda sem opiniões.", + tribeOpinionsCast: "Votar", + tribeOpinionsRankings: "Classificações", + tribeOpinionsAlreadyVoted: "Já votado", tribeTopCategory: "Mais votado", + tribePixeliaTitle: "Pixelia", + tribePixeliaDescription: "Tela colaborativa de pixel art.", + tribePixeliaPaint: "Pintar", + tribePixeliaContributors: "contribuidores", + tribePixeliaTotalPixels: "pixeis pintados", tribeTagsEmpty: "Nenhum tag encontrado.", + tribeTagsCloud: "Nuvem de tags", + tribeTagsContentWith: "Conteúdo com tag", tribeSearchPlaceholder: "Pesquisar conteúdo da tribo...", tribeSearchEmpty: "Nenhum resultado encontrado.", tribeSearchResults: "Resultados", tribeSearchMinChars: "Introduz pelo menos 2 caracteres para pesquisar.", agendaTitle: "Agenda", agendaDescription: "Aqui podes encontrar todos os teus itens atribuídos.", - agendaSearchPlaceholder: "Pesquisar na agenda...", agendaFilterAll: "TODOS", agendaFilterToday: "HOJE", agendaFilterUpcoming: "PRÓXIMOS", @@ -1963,14 +2202,23 @@ module.exports = { agendaFilterReports: "RELATÓRIOS", agendaFilterTransfers: "TRANSFERÊNCIAS", agendaFilterJobs: "EMPREGOS", - agendaFilterIndustry: "INDÚSTRIA", - agendaFilterHousing: "HABITAÇÃO", agendaFilterProjects: "PROJETOS", agendaFilterCalendars: "CALENDÁRIOS", agendaNoItems: "Sem atribuições encontradas.", + agendaAuthor: "Por", agendaDiscardButton: "Descartar", agendaRestoreButton: "Restaurar", + agendaCreatedAt: "Criado em", + agendaTitleLabel: "Título", agendaMembersCount: "Membros", + agendaDescriptionLabel: "Descrição", + agendaStatus: "Estado", + agendaVisibility: "Visibilidade", + agendaEventDate: "Data do evento", + agendaEventLocation: "Localização", + agendaEventPrice: "Preço", + agendaEventUrl: "URL", + agendaTaskStart: "Hora de início", agendaLocationLabel: "Localização", agendaYes: "SIM", agendaNo: "NÃO", @@ -1980,6 +2228,11 @@ module.exports = { agendareportCategory: "Categoria", agendareportSeverity: "Gravidade", agendareportStatus: "Estado", + agendareportDescription: "Descrição", + agendaTaskEnd: "Hora de fim", + agendaTaskPriority: "Prioridade", + agendaTransferFrom: "De", + agendaTransferTo: "Para", agendaTransferConcept: "Concept", agendaTransferAmount: "Montante", agendaTransferDeadline: "Deadline", @@ -1989,7 +2242,47 @@ module.exports = { voteNow: "Votar agora", alreadyVoted: "You have already opined.", noOpinionsFound: "Sem opiniões encontradas.", + RECENTButton: "RECENT", TOPButton: "TOP", + interestingButton: "INTERESSANTE", + necessaryButton: "NECESSÁRIO", + funnyButton: "ENGRAÇADO", + disgustingButton: "REPUGNANTE", + sensibleButton: "SENSÍVEL", + propagandaButton: "PROPAGANDA", + adultOnlyButton: "PARA ADULTOS", + boringButton: "ABORRECIDO", + confusingButton: "CONFUSO", + usefulButton: "ÚTIL", + informativeButton: "INFORMATIVO", + wellResearchedButton: "BEM PESQUISADO", + accurateButton: "PRECISO", + needsSourcesButton: "PRECISA FONTES", + wrongButton: "ERRADO", + lowQualityButton: "BAIXA QUALIDADE", + creativeButton: "CRIATIVO", + insightfulButton: "PERSPICAZ", + actionableButton: "ACIONÁVEL", + inspiringButton: "INSPIRING", + loveButton: "AMOR", + clearButton: "CLARO", + upliftingButton: "EDIFICANTE", + unnecessaryButton: "DESNECESSÁRIO", + rejectedButton: "REJEITADO", + misleadingButton: "ENGANOSO", + offTopicButton: "FORA DO TÓPICO", + duplicateButton: "DUPLICADO", + clickbaitButton: "CLICKBAIT", + spamButton: "SPAM", + trollButton: "TROLL", + nsfwButton: "NSFW", + violentButton: "VIOLENTO", + toxicButton: "TÓXICO", + harassmentButton: "ASSÉDIO", + hateButton: "ÓDIO", + scamButton: "FRAUDE", + triggeringButton: "PERTURBANTE", + opinionsCreatedAt: "Criado em", opinionsTotalCount: "Total de opiniões", voteInteresting: "Interessante", voteNecessary: "Necessário", @@ -2026,39 +2319,27 @@ module.exports = { voteCreative: "Criativo", voteSpam: "Spam", voteAdultOnly: "Para adultos", + publishBlog: "Publicar blog", privateMessage: "MP", pmSendTitle: "Mensagens privadas", pmSend: "Enviar!", - pmCancel: "Cancelar", - pmReset: "Repor", - pmSharedKeyHint: "Partilha esta chave com o destinatário por outro canal. É necessária para decifrar.", - pmDescription: "Use este formulário para enviar uma mensagem/ficheiro cifrado a outros habitantes.", - pmComposeTitle: "Enviar uma mensagem", + pmDescription: "Usa este formulário para enviar uma mensagem encriptada a outros habitantes.", pmRecipients: "Destinatários", pmRecipientsHint: "Introduz Oasis IDs separados por vírgulas", - pmLimitsHint: "Até 7 destinatários por mensagem.", - pmTextPlaceholder: "Corpo limitado a 7000 caracteres.", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "Assunto", pmSubjectHint: "Introduz o assunto da mensagem", pmText: "Mensagem", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Decifrar", - fileShareTitle: "Compartilhar um ficheiro", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "Ficheiro", - fileShareDownload: "Baixar", - fileShareMutualError: "Só pode compartilhar ficheiros com habitantes com apoio mútuo.", - fileShareNoFile: "Nenhum ficheiro selecionado.", - fileShareTooLarge: "O ficheiro excede o tamanho permitido.", - fileShareFailed: "Não foi possível preparar o ficheiro.", - fileShareSendError: "Não foi possível enviar o ficheiro.", - fileShareUnavailable: "Este ficheiro não está disponível agora. Tenta mais tarde.", pmCrypterBadKey: "A chave partilhada está incorreta!", pmCrypterTooLong: "A mensagem é demasiado longa para ser cifrada com o Crypter. Encurta-a e tenta de novo.", pmCrypterCipherLabel: "Mensagem recifrada", pmCrypterCharsLabel: "caracteres", pmInvalidRecipients: "Oasis ID inválido (deveria ser como @…=.ed25519).", pmEncryptionLabel: "Cifragem", + pmFile: "Anexo", private: "Privado", privateDescription: "Your encrypted messages.", privateInbox: "CAIXA DE ENTRADA", @@ -2068,27 +2349,19 @@ module.exports = { noPrivateMessages: "No private messages.", pmFromLabel: "De:", pmToLabel: "Para:", + pmInvalidMessage: "Mensagem inválida", pmNoSubject: "(sem assunto)", pmSubjectLabel: "Assunto:", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "A casa governante do L.A.R.P mudou.", - politicalBotParliamentTitle: "Começou um novo ciclo de governo no Parlamento.", - politicalBotTribeTitle: "Começou um novo ciclo de governo numa das tuas Tribos.", - politicalBotLarpIntro: "Agora {house} governa durante este ciclo: {cycle}", - politicalBotParliamentIntro: "O governo atual é {gov}", - politicalBotParliamentIntroLeader: "O governo atual é {gov}, exercido por {leader}", - politicalBotTribeIntro: "O governo atual na Tribo {tribe} é {gov}", - politicalBotTribeIntroLeader: "O governo atual na Tribo {tribe} é {gov}, exercido por {leader}", - politicalBotLeaderLabel: "Líder", + pmBodyLabel: "Corpo", + pmBotJobs: "42-EmpregoBOT", + pmBotProjects: "42-ProjetosBOT", + pmBotMarket: "42-MercadoBOT", inboxJobSubscribedTitle: "Nova subscrição à tua oferta de emprego", pmInhabitantWithId: "Habitante com Oasis ID:", pmHasSubscribedToYourJobOffer: "subscreveu a tua oferta de emprego", + inboxProjectCreatedTitle: "Novo projeto criado", + pmHasCreatedAProject: "criou um projeto", inboxMarketItemSoldTitle: "Item vendido", - inboxShopSoldTitle: "Produto vendido", pmYourItem: "O teu item", pmHasBeenSoldTo: "foi vendido a", pmFor: "por", @@ -2096,6 +2369,7 @@ module.exports = { pmHasPledged: "comprometeu", pmToYourProject: "ao teu projeto", blockchain: 'BlockExplorer', + blockchainTitle: 'BlockExplorer', blockchainDescription: 'Explore and visualize the blocks in the blockchain.', blockchainNoBlocks: 'No blocks found in the blockchain.', blockchainStatsTitle: 'Estatísticas', @@ -2107,36 +2381,42 @@ module.exports = { blockchainBlockCarbon: 'Pegada de carbono', blockchainBlockType: 'Type', blockchainBlockTimestamp: 'Timestamp', + blockchainBlockContent: 'Block', + blockchainBlockURL: 'URL:', + blockchainContent: 'Block', + blockchainContentPreview: 'Preview of the block content', blockchainLatestDatagram: 'Último Datagrama', - blockchainDatagram: "Datagrama", - blockchainDetails: "Ver Detalhes do Bloco", - blockchainViewBlockexplorer: "Ver no Blockexplorer", + blockchainDatagram: 'Datagrama', + blockchainDetails: 'View block details', + blockchainViewBlockexplorer: "Ver no blockexplorer", + blockchainBlockInfo: 'Block Information', + blockchainBlockDetails: 'Details of the selected block', blockchainBack: 'Back to Blockexplorer', blockchainContentDeleted: "Este conteúdo foi eliminado (tombstone)", - visitContent: "Visitar Conteúdo", - favoriteAdd: "Fixar nos Favoritos", - pmContentTooltip: "Enviar Mensagem Privada", - favoriteRemove: "Remover dos Favoritos", - reportContent: "Denunciar Conteúdo", + visitContent: "Visitar conteúdo", banking: 'Banking', bankingTitle: 'Banking', - bankingDescription: "A economia ECOin: saldo, RBU, taxas e valor da indústria.", + bankingDescription: 'Explore the current value of ECOin and the corresponding UBI allocation, distributed per epoch based on participation and trust.', bankOverview: 'Overview', bankEpochs: 'Epochs', bankRules: 'Rules', pending: 'Pending', closed: 'Closed', + bankBack: 'Back to Banking', bankViewTx: 'View Tx', bankClaimNow: 'Claim now', bankClaimUBI: 'Reivindicar RBU!', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: 'Nenhuma alocação RBU pendente para esta época.', bankEpoch: 'Epoch', bankPool: 'Pool (this epoch)', bankWeightsSum: 'Sum of weights', + bankAllocations: 'Allocations', bankNoAllocations: 'No allocations found.', bankNoEpochs: 'No epochs found.', bankEpochAllocations: 'Epoch allocations', @@ -2150,11 +2430,10 @@ module.exports = { bankEpochId: 'Epoch ID', bankRuleHash: 'Rules Snapshot Hash', bankViewEpoch: 'View Epoch', - bankYourIndustryBalance: "A tua parte da indústria", - bankYourUbiMonth: "O teu RBU (este mês)", - bankYourFundsMonth: "Os teus fundos (este mês)", - bankIndustryBalance: "Produção industrial (estimada)", bankUserBalance: 'Your Balance', + ecoWalletNotConfigured: 'ECOin Wallet not configured', + editWallet: 'Edit wallet', + addWallet: 'Add wallet', bankAddresses: 'Addresses', bankNoAddresses: 'No addresses found.', bankUser: 'Oasis ID', @@ -2179,10 +2458,18 @@ module.exports = { search: 'Search!', bankLocal: 'Local', bankFromOasis: 'Oasis', + bankMyAddress: 'Your address', + bankRemoveMyAddress: 'Remove my address', + bankNotRemovableOasis: 'Addresses cannot be removed locally', + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "SEM FUNDOS!", bankUbiAvailableOk: "DISPONÍVEL!", - bankUbiAvailability: "RBU (rede)", + bankUbiAvailability: "Disponibilidade UBI", bankAlreadyClaimedThisMonth: "Já reclamado este mês", bankUbiThisMonth: "RBU (este mês)", bankUbiLastClaimed: "RBU (última reclamada)", @@ -2196,6 +2483,13 @@ module.exports = { shopsTitle: "Lojas", shopDescription: "Descubra e gerencie lojas na rede.", shopTitle: "Loja", + shopFilterAll: "TODAS", + shopFilterMine: "MINHAS", + shopFilterRecent: "RECENTES", + shopFilterTop: "TOP", + shopFilterProducts: "PRODUTOS", + shopFilterPrices: "PREÇOS", + shopFilterFavorites: "FAVORITAS", shopUpload: "Criar Loja", shopAllSectionTitle: "Todas as Lojas", shopMineSectionTitle: "Minhas Lojas", @@ -2212,10 +2506,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "Publicar na Clearnet", + shopClearnetUnpublish: "Remover da Clearnet", + shopClearnetUrlLabel: "URL Clearnet", + shopClearnetWarning: "Publicar na Clearnet torna esta loja indexável por motores de busca e rastreadores externos.", shopAddFavorite: "Adicionar Favorito", shopRemoveFavorite: "Remover Favorito", shopOpen: "ABERTA", shopClosed: "FECHADA", + shopOpenShop: "Abrir Loja", + shopCloseShop: "Fechar Loja", shopMakePrivate: "TORNAR PRIVADA (E2E)", shopMakePublic: "TORNAR PÚBLICA", shopProducts: "Produtos", @@ -2243,6 +2543,8 @@ module.exports = { shopOrderPrice: "Preço", shopOrderBuyer: "Comprador", shopBackToShop: "Voltar à Loja", + shopShareUrl: "URL de partilha", + shopVisitShop: "VISITAR LOJA", marketVisitItem: "VER ARTIGO", shopStatus: "ESTADO", shopCreatedAt: "CRIADO", @@ -2251,10 +2553,12 @@ module.exports = { shopLocation: "Localização", shopTags: "Tags", shopVisibility: "Visibilidade", + shopImage: "Imagem", shopShortDescription: "Descricao Curta", shopShortDescriptionPlaceholder: "Descricao breve para os cartoes (max 160 caracteres)", shopTitlePlaceholder: "Nome da sua loja", shopDescriptionPlaceholder: "Descricao detalhada da sua loja", + shopUrlPlaceholder: "https://url-da-sua-loja.com", shopLocationPlaceholder: "Cidade, Pais", shopTagsPlaceholder: "tag1, tag2, tag3", mapTitlePlaceholder: "Titulo do mapa", @@ -2272,6 +2576,7 @@ module.exports = { bankUnitMinutes: "minutos", bankUnitDays: "dias", bankExchangeNoData: 'No data available', + bankExchangeIndex: 'ECOin Value (1h)', bankInflation: 'ECOin Inflation (anual)', bankInflationMonthly: 'ECOin Inflation (mensual)', bankExchangeChartTitle: 'Progressão do valor de ECOin ao longo do tempo', @@ -2285,17 +2590,38 @@ module.exports = { bankingSyncStatusOutdated: 'Outdated', statsTitle: 'Statistics', statistics: "Estatísticas", + statsInhabitant: "Estatísticas do habitante", statsDescription: "Descobre estatísticas sobre a tua rede.", ALLButton: "ALL", MINEButton: "MINE", TOMBSTONEButton: "TOMBSTONES", + statsYou: "Tu", + statsUserId: "Oasis ID", statsCreatedAt: "Criado em", statsYourContent: "Conteúdo", statsYourOpinions: "Opiniões", + statsYourTombstone: "Tombstones", + statsNetwork: "Rede", + statsTotalInhabitants: "Habitantes", + statsDiscoveredTribes: "Tribos (públicas)", + statsPrivateDiscoveredTribes: "Tribos (privadas)", statsNetworkContent: "Conteúdo", + statsYourMarket: "Mercado", + statsYourJob: "Empregos", + statsYourProject: "Projetos", + statsYourTransfer: "Transferências", + statsYourForum: "Fóruns", statsNetworkOpinions: "Opiniões", + statsDiscoveredMarket: "Mercado", + statsDiscoveredJob: "Empregos", + statsDiscoveredProject: "Projetos", + statsBankingTitle: "Banca", statsEcoWalletLabel: "Carteira ECOIN", statsEcoWalletNotConfigured: "Não configurada!", + statsTotalEcoAddresses: "Total de endereços", + statsDiscoveredTransfer: "Transferências", + statsDiscoveredForum: "Fóruns", + statsNetworkTombstone: "Tombstones", statsBookmark: "Marcadores", statsEvent: "Eventos", statsTask: "Tarefas", @@ -2317,12 +2643,16 @@ module.exports = { statsAiExchange: "IA", statsPUBs: 'PUBs', statsPost: "Publicações", + statsOasisID: "Oasis ID", statsSize: "Total (tamanho)", statsBlockchainSize: "Blockchain (tamanho)", statsBlobsSize: "Blobs (tamanho)", statsActivity7d: "Atividade (últimos 7 dias)", statsActivity7dTotal: "Total 7 dias", statsActivity30dTotal: "Total 30 dias", + statsKarmaScore: "Pontuação KARMA", + statsPublic: "Público", + statsPrivate: "Privado", day: "Day", messages: "Mensagens", statsProject: "Projetos", @@ -2334,14 +2664,30 @@ module.exports = { statsProjectsCancelled: "Cancelado", statsProjectsGoalTotal: "Objetivo total", statsProjectsPledgedTotal: "Total comprometido", + statsProjectsSuccessRate: "Taxa de sucesso", + statsProjectsAvgProgress: "Progresso médio", + statsProjectsMedianProgress: "Progresso mediano", + statsProjectsActiveFundingAvg: "Financiamento ativo médio", + statsJobsTitle: "Empregos", + statsJobsTotal: "Total de empregos", + statsJobsOpen: "Abrir", + statsJobsClosed: "Fechado", + statsJobsOpenVacants: "Vagas abertas", + statsJobsSubscribersTotal: "Total de subscritores", + statsJobsAvgSalary: "Salário médio", + statsJobsMedianSalary: "Salário mediano", statsMarketTitle: "Mercado", statsMarketTotal: "Total de itens", statsMarketForSale: "À venda", statsMarketReserved: "Reservado", statsMarketClosed: "Fechado", statsMarketSold: "Vendido", + statsMarketRevenue: "Receita", + statsMarketAvgSoldPrice: "Preço médio de venda", statsUsersTitle: "Habitantes", user: "Habitante", + statsTombstoneTitle: "Tombstones", + statsNetworkTombstones: "Tombstones da rede", statsTombstoneRatio: "Rácio de tombstones (%)", statsParliamentCandidature: "Candidaturas parlamentares", statsParliamentTerm: "Mandatos parlamentares", @@ -2368,21 +2714,30 @@ module.exports = { aiConfiguration: "Definir prompt", aiPromptUsed: "Prompt", aiClearHistory: "Limpar histórico de conversa", + aiSharePrompt: "Adicionar esta resposta ao treino coletivo?", + aiShareYes: "Sim", + aiShareNo: "Não", + aiSharedLabel: "Adicionado ao treino", + aiRejectedLabel: "Não adicionado ao treino", aiServerError: "A IA não conseguiu responder. Por favor, tenta novamente.", aiInputPlaceholder: "What is Oasis?", typeAiExchange: "IA", aiApproveTrain: "Adicionar ao treino coletivo", aiRejectTrain: "Não treinar", + aiTrainPending: "Aprovação pendente", aiTrainApproved: "Aprovado para treino", aiTrainRejected: "Rejeitado para treino", aiSnippetsUsed: "Excertos usados", aiSnippetsLearned: "Snippets learned", statsAITraining: "AI training", + aiApproveCustomTrain: "Treinar com esta resposta personalizada", aiCustomAnswerPlaceholder: "Escreve a tua resposta personalizada…", + statsAIExchanges: "Model Exchanges", marketMineSectionTitle: "Os teus itens", marketCreateSectionTitle: "Criar item", marketUpdateSectionTitle: "Atualizar", marketAllSectionTitle: "Mercado", + marketRecentSectionTitle: "Mercado recente", marketTitle: "Mercado", marketDescription: "Um mercado para trocar bens ou serviços na tua rede.", marketFilterAll: "TODOS", @@ -2412,11 +2767,14 @@ module.exports = { marketItemDeadline: "Prazo", marketItemIncludesShipping: "Inclui envio?", marketItemHighestBid: "Maior licitação", + marketItemHighestBidder: "Maior licitante", marketItemStock: "Disponibilidade", marketOutOfStock: "Esgotado", + marketItemBidTime: "Hora da licitação", marketActionsUpdate: "Atualizar", marketUpdateButton: "Atualizar", marketActionsDelete: "Eliminar", + marketActionsSold: "Marcar como vendido", marketActionsChangeStatus: "ALTERAR ESTADO", marketActionsBuy: "COMPRAR!", marketAuctionBids: "Licitações atuais", @@ -2425,82 +2783,19 @@ module.exports = { marketNoItems: "Ainda sem itens disponíveis.", marketYourBid: "A tua licitação", marketCreateFormImageLabel: "Carregar mídia (máx: 50MB)", + marketSearchLabel: "Pesquisa", marketSearchPlaceholder: "Pesquisar título ou tags", marketMinPriceLabel: "Preço mínimo", marketMaxPriceLabel: "Preço máximo", + marketSortLabel: "Ordenar por", marketSortRecent: "Mais recentes", marketSortPrice: "Preço", marketSortDeadline: "Prazo", marketSearchButton: "Pesquisa", marketAuctionEndsIn: "Termina", marketAuctionEnded: "Terminado", + marketMyBidBadge: "Licitaste", marketNoItemsMatch: "Nenhum item corresponde à tua pesquisa.", - housingTitle: "Habitação", - housingDescriptionText: "Descobre e gere lugares na tua rede.", - modulesHousingLabel: "Habitação", - modulesHousingDescription: "Módulo para descobrir e gerir lugares.", - typeHousing: "HABITAÇÃO", - housingFilterAll: "TUDO", - housingFilterMine: "MEUS", - housingFilterRequested: "PEDIDOS", - housingFilterSale: "VENDA", - housingFilterRent: "ARRENDAMENTO", - housingFilterCouchsurfing: "COUCHSURFING", - housingFilterOpen: "DISPONÍVEIS", - housingFilterClosed: "INDISPONÍVEIS", - housingFilterRecent: "RECENTES", - housingFilterTop: "TOP", - housingType: "Oferta", - housingTypeSALE: "À venda", - housingTypeRENT: "Para arrendar", - housingTypeCOUCHSURFING: "Couchsurfing", - housingProperty: "Imóvel", - housingPropertyAPARTMENT: "Apartamento", - housingPropertyHOUSE: "Casa", - housingPropertyROOM: "Quarto", - housingPropertyLAND: "Terreno", - housingPropertyOTHER: "Outro", - housingTitleLabel: "Título", - housingTitlePlaceholder: "Nome do lugar", - housingDescription: "Descrição", - housingDescriptionPlaceholder: "Descreve o lugar, o bairro, o que está incluído: casas de banho, jardim, camas…", - housingRules: "Regras", - housingRulesPlaceholder: "Animais, fumar, horas de silêncio, estadia mínima…", - housingPlace: "Lugar", - housingPlacePlaceholder: "País, cidade, morada", - housingPrice: "Preço", - housingPriceLabel: "Preço (ECO)", - housingSizeLabel: "Área (m²)", - housingFree: "GRÁTIS", - housingPerMonth: "/mês", - housingRooms: "Quartos", - housingSize: "Área", - housingCapacity: "Hóspedes", - housingAvailableFrom: "Disponível desde", - housingAvailableTo: "Disponível até", - housingTags: "Etiquetas", - spreadEditWarning: "Este conteúdo perderá os spreads ao editá-lo", - housingRequests: "Pedidos", - housingRequestButton: "Pedir", - housingCancelButton: "Cancelar pedido", - housingRequestedBadge: "PEDIDO", - housingStatus: "Estado", - housingStatusOPEN: "DISPONÍVEL", - housingStatusCLOSED: "INDISPONÍVEL", - housingSetOpen: "Tornar disponível", - housingSetClosed: "Tornar indisponível", - housingCreateButton: "Criar lugar", - housingUpdateButton: "Atualizar", - housingDeleteButton: "Apagar", - housingSearchPlaceholder: "Procurar lugares...", - housingSearchButton: "Procurar", - housingMinPrice: "Preço mín.", - housingMaxPrice: "Preço máx.", - housingSortRecent: "Mais recentes", - housingSortPrice: "Preço mais baixo", - housingSortRequests: "Mais pedidos", - housingSortRating: "Melhor avaliados", - housingNoItems: "Não foram encontrados lugares.", jobsTitle: "Empregos", jobsCandidatesTitle: "Candidatos sugeridos", jobsCandidatesDescription: "Habitantes cujas competências combinam com este emprego. Envie uma mensagem privada para convidá-los.", @@ -2548,6 +2843,7 @@ module.exports = { jobLocationPresencial: "Presencial", jobLocationRemote: "Remoto", jobVacantsPlaceholder: "Número de posições", + jobSalaryPlaceholder: "Salário em ECO por 1 hora dedicada", jobImage: "Carregar mídia (máx: 50MB)", jobTasks: "Tarefas", jobType: "Tipo de emprego", @@ -2557,6 +2853,7 @@ module.exports = { jobsFilterTop: "TOP", jobsTopTitle: "Empregos com melhor salário", createJobButton: "Publicar emprego", + viewDetailsButton: "Ver detalhes", noJobsFound: "Sem ofertas de emprego encontradas.", jobAuthor: "Por", jobTypeFreelance: "Freelancer", @@ -2568,6 +2865,10 @@ module.exports = { jobsHoursOffered: "Horas oferecidas", jobsHoursRequested: "Horas pedidas em troca", jobsExchangeSkill: "Habilidade pedida em troca", + jobsHoursOfferedPlaceholder: "p. ex. 4", + jobsHoursRequestedPlaceholder: "p. ex. 4", + jobsExchangeSkillPlaceholder: "p. ex. carpintaria, design", + jobExchangeHint: "Para empregos de troca de horas, preencha os campos abaixo em vez do salário.", jobTimePartial: "Part-time", jobTimeComplete: "Full-time", jobsDeleteButton: "ELIMINAR", @@ -2575,201 +2876,31 @@ module.exports = { jobsFilterApplied: "CANDIDATADO", jobsAppliedTitle: "As minhas candidaturas", jobsAppliedBadge: "Candidatado", + jobsFilterFavs: "Favoritos", + jobsFavsTitle: "Favoritos", + jobsFilterNeeds: "Precisa ajuda", + jobsNeedsTitle: "Precisa ajuda", + jobsSearchLabel: "Pesquisa", jobsSearchPlaceholder: "Pesquisar título, tags, descrição...", jobsMinSalaryLabel: "Salário mínimo", jobsMaxSalaryLabel: "Salário máximo", + jobsSortLabel: "Ordenar por", jobsSortRecent: "Mais recentes", jobsSortSalary: "Maior salário", jobsSortSubscribers: "Mais candidatos", jobsSearchButton: "Pesquisa", + jobsFavoriteButton: "Favorito", + jobsUnfavoriteButton: "Remover favorito", + jobsMessageAuthorButton: "MP", jobsApplicants: "Candidatos", jobsUpdatedAt: "Atualizado", jobSetOpen: "Set open", + jobNewBadge: "NOVO", jobsTagsLabel: "Tags", + jobsTagsPlaceholder: "tag1, tag2, tag3", noJobsMatch: "Nenhum emprego corresponde à tua pesquisa.", - industrySearchPlaceholder: "Procurar fábricas…", - industryAllSectors: "Todos os setores", - industryVisitButton: "Visitar indústria", - industryAdvanceTo: "Definir para", - industryAmount: "Quantia", - industryApproveBuild: "Aprovar", - industryBlueprint: "Blueprint", - industryBlueprints: "Blueprints", - industryBuild: "Build", - industryBuildNotes: "Notas", - industryBuildStart: "Data de início", - industryVoteUpdateButton: "Votar atualização", - industryVoteDeleteButton: "Votar exclusão", - industryRevokeVote: "Revogar", - industryTimeLeft: "Tempo restante", - industryBuildEnd: "Data de fim", - industryBuilds: "Builds", - industryBuildTitle: "Título", - industryContribute: "Contribuir", - industryContributeButton: "Contribuir", - industryContributions: "Contribuições", - industryCreateBlueprintButton: "Criar blueprint", - industryDistribute: "Distribuir", - industryDistributeButton: "Distribuir por quotas", - industryDistribution: "Distribuição", - industryEcoAmount: "Quantia (ECO)", - industryEditBlueprint: "Editar blueprint", - industryFacility: "Fábrica", - industryKindLabel: "Tipo", - industryKind_labor: "Mão de obra", - industryKind_material: "Material", - industryKind_eco: "Fundos", - industryLaborHours: "Mão de obra (horas)", - industryHours: "Horas", - industryMaterialItem: "Material", - industryMaterials: "Materiais", - industryEstMaterials: "Total materiais", - industryEstLabor: "Total mão de obra", - industryEstTotal: "Preço estimado", - industryBuildingPrice: "Preço de construção", - industryFinalPrice: "Preço final", - industryBlueprintDescPlaceholder: "Especificações, dimensões, peso, documentação…", - industryMaterialValue: "Valor (ECO)", - industryMember: "Membro", - industryNewBlueprint: "Novo blueprint", - industryNewBuild: "Propor um build", - industryEditBuild: "Editar produção", - industryNeedBlueprint: "Cria primeiro um plano: cada produção fabrica um.", - industryNoBlueprints: "Ainda não há blueprints.", - industryNoBuilds: "Ainda não há builds.", - industryNote: "Nota", - industryOutput: "Saída", - industryOutputKind: "Tipo de produto", - industryOutputValue: "Valor de saída (ECO, ex. da venda)", - industryPostJob: "Criar Emprego", - industryPot: "Bolo", - industryProposeBuildButton: "Propor build", - industryProposer: "Proponente", - industryAuthor: "Autor", - industrySellOnMarket: "Enviar ao Mercado", - industrySendToProjects: "Criar Projeto", - industrySkills: "Competências", - industryTreasury: "Tesouraria", - industryKind_physical: "Físico", - industryKind_digital: "Digital", - industryBuildStatus_PROPOSED: "PROPOSTO", - industryBuildStatus_REJECTED: "REJEITADO", - industryBuildStatus_APPROVED: "APROVADO", - industryBuildStatus_STOCKING: "ABASTECENDO", - industryBuildStatus_IN_PRODUCTION: "EM PRODUÇÃO", - industryBuildStatus_COMPLETED: "CONCLUÍDO", - industryBuildStatus_FAILED: "FALHADO", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "Um emprego corresponde ao teu currículo", - jobsBotMatchIntro: "Um emprego corresponde ao teu currículo.", - jobsBotMatchJob: "Emprego", - jobsBotMatchHint: "Recebes isto porque o teu currículo é gerido por IA. Podes desativar no teu CV.", - cvAiManaged: "Gerido por IA", - switchOn: "LIGADO", - switchOff: "DESLIGADO", - cvAiManagedHint: "Se ativo, o JobsBot avisa-te por mensagem privada quando um emprego corresponde ao teu currículo.", - cvMatchThreshold: "Avisar a partir deste nível de correspondência", - housingBotRequestedTitle: "Alguém pediu um dos teus lugares.", - housingBotCancelledTitle: "Um pedido sobre um dos teus lugares foi cancelado.", - housingBotYouRequestedTitle: "Pediste um lugar.", - housingBotYouCancelledTitle: "Cancelaste um pedido.", - housingBotUnavailableTitle: "Um lugar que pediste já não está disponível.", - housingBotUnavailableSentTitle: "Avisaste quem pediu este lugar.", - industryBotAdmittedTitle: "Você foi admitido numa fábrica.", - industryBotApplicationTitle: "Nova solicitação de adesão na sua fábrica.", - industryBotInvitedTitle: "Você foi convidado para uma fábrica.", - industryBotDissolvedTitle: "Uma fábrica a que pertence foi dissolvida.", - industryBotBuildApprovedTitle: "Um build foi aprovado.", - industryBotDistributedTitle: "A saída de um build foi distribuída.", - industryFilterRules: "REGRAS", - industryRulesTitle: "Regras", - industryRulesIntro: "Industry permite aos habitantes possuir coletivamente os meios de produção: uma Fábrica é uma oficina da rede que ninguém possui de forma privada.", - industryRulesSteward: "O fundador é o administrador — um guardião, não um dono: não pode vender, privatizar nem dissolver a Fábrica sozinho.", - industryRulesMembership: "A política de adesão define como se entra: Aberta (na hora), Por voto (admitido pelo voto dos membros) ou Apenas convite (um membro deve convidar primeiro).", - industryRulesQuorum: "O quórum é o número mínimo de membros que devem votar para uma decisão contar, para que uma Fábrica pequena não seja decidida por uma só pessoa.", - industryRulesMajority: "A maioria é a fração de votos necessária (0,5 = metade, 1 = unanimidade); uma decisão passa quando os SIM atingem pelo menos max(quórum, membros × maioria).", - industryRulesDecisions: "Os membros votam para admitir novos, aprovar builds e dissolver a Fábrica; o administrador não pode ignorar estas decisões coletivas.", - industryRulesBlueprints: "Os Blueprints são receitas livres (COPYLEFT) — as entradas (materiais, horas de trabalho, competências) e a saída (um bem físico ou digital) — e qualquer um pode forká-las.", - industryRulesBuilds: "Um Build é uma ordem de produção: PROPOSTO → APROVADO (por voto) → ABASTECENDO → EM PRODUÇÃO → CONCLUÍDO, e só aceita contribuições depois de aprovado.", - industryRulesContributions: "Os membros contribuem com trabalho (horas), materiais (valor ECO declarado) ou ECO a um build; cada contribuição ganha karma.", - industryRulesLaborRate: "A tarifa de trabalho é o valor ECO de uma hora de trabalho nesta Fábrica; converte horas em karma para comparar esforço e capital de forma justa: karma = horas × tarifa + valor material + ECO.", - industryRulesShares: "A tua quota de um build é o teu karma ÷ karma total, e define quanto do valor de saída recebes.", - industryRulesDistribution: "Quando uma produção é concluída, o steward distribui o fundo (os fundos da produção mais o valor de venda) entre os contribuidores, proporcionalmente às suas quotas.", - industryRulesTreasury: "As contribuições em ECO são guardadas pelo administrador como tesouraria do build até à distribuição; todos os movimentos ficam registados na rede e as disputas podem ir aos Courts.", - industryJobs: "Empregos", - industryReportButton: "Denunciar", - industryCreateTask: "Criar Tarefa", - industryOpenDispute: "Abrir disputa", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "Gerar convite", - industryPauseVotes: "Votos de estado", - industryTitle: "Indústria", - industryDescription: "Módulo para gerir os meios de produção de forma coletiva.", - typeIndustryBuild: "BUILD", - typeIndustryBlueprint: "BLUEPRINT", - typeIndustryAllocation: "DISTRIBUIÇÃO", - typeIndustry: "INDÚSTRIA", - modulesIndustryLabel: "Indústria", - modulesIndustryDescription: "Módulo para gerir os meios de produção de forma coletiva.", - industryFilterAll: "TODAS", - industryFilterMember: "MEMBRO", - industryFilterBlueprints: "PLANOS", - industryFilterBuilds: "PRODUÇÕES", - industryFilterMine: "MINHAS", - industryFilterActive: "EM FUNCIONAMENTO", - industryFilterPaused: "PAUSADAS", - industryFilterDissolved: "DISSOLVIDAS", - industryAllTitle: "Indústria", - industryMemberTitle: "Fábricas de que faço parte", - industryMineTitle: "Minhas fábricas", - industryActiveTitle: "Em funcionamento", - industryPausedTitle: "Fábricas pausadas", - industryDissolvedTitle: "Fábricas dissolvidas", - industryNoFacilitiesFound: "Nenhuma fábrica encontrada.", - industryCreateFacility: "Criar fábrica", - industryMembers: "Membros", - industryMemberBadge: "MEMBRO", - industryName: "Nome", - industryNamePlaceholder: "Nome da fábrica", - industryDescriptionLabel: "Descrição", - industryDescriptionPlaceholder: "O que esta fábrica produz?", - industrySector: "Setor", - industryMembershipPolicy: "Política de adesão", - industryQuorum: "Quórum", - industryMajority: "Maioria", - industryLaborRate: "Tarifa de trabalho", - industryCreateButton: "Criar fábrica", - industryUpdateButton: "Atualizar", - industrySteward: "Administrador", - industryStatusActive: "EM FUNCIONAMENTO", - industryStatusPaused: "PAUSADA", - industryStatusDissolved: "DISSOLVIDA", - industryStatusLabel: "Estado", - industryJoinButton: "Juntar-se", - industryApplyButton: "Pedir para entrar", - industryLeaveButton: "Sair", - industryInviteButton: "Convidar", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "Precisas de um convite para entrar.", - industryPauseButton: "Votar pausar", - industryResumeButton: "Votar retomar", - industryDeleteButton: "Eliminar", - industryPendingApplicants: "Candidaturas pendentes", - industryVotesYes: "sim", - industryVotesNo: "não", - industryAlreadyVoted: "votado", - industryAdmit: "Admitir", - industryReject: "Rejeitar", - industryDissolveTitle: "Dissolver fábrica", - industryDissolveVote: "Votar dissolução", - industryPolicy_open: "Aberta", - industryPolicy_vote: "Por voto", - industryPolicy_invite: "Apenas convite", projectsTitle: "Projetos", projectsDescription: "Cria, financia e acompanha projetos comunitários na tua rede.", - projectSearchPlaceholder: "Pesquisar projetos...", projectCreateProject: "Criar projeto", projectCreateButton: "Criar projeto", projectUpdateButton: "ATUALIZAR", @@ -2813,8 +2944,14 @@ module.exports = { projectPledgeTitle: "Apoiar este projeto", projectPledgePlaceholder: "Montante em ECO", projectBounties: "Recompensas", + projectBountiesInputLabel: "Recompensas (uma por linha: Título|Montante [ECO]|Descrição)", + projectBountiesPlaceholder: "Corrigir bug UI|100|Link para o problema\nEscrever docs|250|Exemplos de utilização", projectNoBounties: "Sem recompensas encontradas.", projectTitle: "Título", + projectAddBountyTitle: "Nova recompensa", + projectBountyTitle: "Título da recompensa", + projectBountyAmount: "Montante (ECO)", + projectBountyDescription: "Descrição", projectMilestoneSelect: "Selecionar marco", projectBountyCreateButton: "Criar recompensa", projectBountyStatus: "Estado da recompensa", @@ -2829,15 +2966,19 @@ module.exports = { projectMilestoneTitle: "Título do marco", projectMilestoneTargetPercent: "Percentagem (%)", projectMilestoneDueDate: "Data", + projectMilestoneCreateButton: "Criar marco", projectMilestoneStatus: "Estado do marco", projectMilestoneOpen: "aberto", projectMilestoneDone: "concluído", projectMilestoneDue: "prazo", + projectNoMilestones: "Sem marcos encontrados.", projectMilestoneMarkDone: "Mark as Done", projectMilestoneTitlePlaceholder: "Introduz o título do marco", projectMilestoneDescriptionPlaceholder: "Introduz a descrição para este marco", projectMilestoneDescription: "Descrição do marco", + projectBudgetGoal: "Orçamento (objetivo)", projectBudgetAssigned: "Atribuído a recompensas", + projectBudgetRemaining: "Restante", projectBudgetOver: "Acima do orçamento: atribuído excede o objetivo", projectFollowers: "Seguidores", projectFollowersTitle: "Seguidores", @@ -2850,6 +2991,7 @@ module.exports = { projectBackersTotalPledged: "Total comprometido", projectBackersYourPledge: "O teu compromisso", projectBackersNone: "Ainda sem compromissos.", + projectNoRemainingBudget: "Sem orçamento restante.", projectFilterBackers: "APOIANTES", projectFilterApplied: "CANDIDATADO", projectAppliedTitle: "CANDIDATADO", @@ -2858,15 +3000,30 @@ module.exports = { projectBackerAmount: "Total contribuído", projectBackerPledges: "Compromissos", projectBackerProjects: "Projetos", + projectPledgeAmount: "Montante", projectSelectMilestoneOrBounty: "Selecionar marco ou recompensa", projectPledgeButton: "Comprometer", footerLicense: "GPLv3", + footerPackage: "Pacote", + footerVersion: "Versão", modulesModuleName: "Nome", modulesModuleDescription: "Descrição", modulesModuleStatus: "Estado", modulesTotalModulesLabel: "Módulos carregados", modulesEnabledModulesLabel: "Ativado", modulesDisabledModulesLabel: "Desativado", + modulesPopularLabel: "Destaques", + modulesPopularDescription: "Módulo para receber publicações em tendência, mais vistas ou mais comentadas.", + modulesTopicsLabel: "Tópicos", + modulesTopicsDescription: "Módulo para receber categorias de discussão baseadas em interesses comuns.", + modulesSummariesLabel: "Resumos", + modulesSummariesDescription: "Módulo para receber resumos de discussões ou publicações longas.", + modulesLatestLabel: "Mais recentes", + modulesLatestDescription: "Módulo para receber as publicações e discussões mais recentes.", + modulesThreadsLabel: "Conversas", + modulesThreadsDescription: "Módulo para receber conversas agrupadas por tópico ou questão.", + modulesMultiverseLabel: "Multiverso", + modulesMultiverseDescription: "Módulo para receber conteúdo de outros pares federados.", modulesInvitesLabel: "Convites", modulesInvitesDescription: "Módulo para gerir e aplicar códigos de convite.", modulesWalletLabel: "Carteira", @@ -2907,12 +3064,8 @@ module.exports = { modulesOpinionsDescription: "Módulo para descobrir e votar em opiniões.", modulesTransfersLabel: "Transferências", modulesTransfersDescription: "Módulo para descobrir e gerir contratos inteligentes (transferências).", - modulesFediverseLabel: "Multiverso", - modulesBlogsLabel: "Blogs", - modulesBlogsDescription: "Módulo para descobrir e gerir blogs.", - modulesPollsLabel: "Sondagens", - modulesPollsDescription: "Módulo para perguntar à rede e contar as respostas.", - modulesFediverseDescription: "Faça a gestão das suas outras contas do multiverso, incluindo enviar e receber conteúdo.", + modulesFediverseLabel: "Fediverse", + modulesFediverseDescription: "Faça a gestão das suas outras contas do fediverso, incluindo enviar e receber conteúdo.", modulesFeedLabel: "Feed", modulesFeedDescription: "Módulo para descobrir e partilhar textos curtos (feeds).", modulesParliamentLabel: "Parlamento", @@ -2948,33 +3101,37 @@ module.exports = { visibilityMakeHidden: "Ocultar", invitesInhabitantsTitle: "Habitantes", invitesInhabitantsFollow: "Seguir", - aiNavPlaceholder: "Descreve o que precisas...", + aiNavPlaceholder: "Para onde queres ir?", aiNavDisabled: "A navegação por IA está desativada.", - aiNavResultsTitle: "Sugestões AINav", + aiNavResultsTitle: "Sugestões de navegação IA", aiNavQueryLabel: "Consulta", + aiNavResultsDescription: "Escolhe a rota que melhor corresponde ao que procuras.", + aiNavResultPath: "Rota", + aiNavResultDescription: "O que podes fazer", aiNavResultMatch: "Correspondência", aiNavResultsEmpty: "Sem rotas correspondentes. Tenta /search.", + aiNavSearchInstead: "Pesquisar em vez disso", modulesAINavLabel: "AINav", modulesAINavDescription: "Módulo para consultas em linguagem natural sobre o conteúdo da rede.", + directConnect: "Ligação direta", directConnectDescription: "Conecta-te diretamente a um par inserindo o endereço IP, porta e chave pública.", peerHost: "IP", peerPort: "Porta (predefinida: 8008)", peerPublicKey: "Chave pública (@...ed25519)", connectAndFollow: "Conectar", + deviceSourceLabel: "Dispositivo", modulesPresetTitle: "Configurações Comuns", - workflowsTitle: "Workflows", - workflowsDescription: "Um workflow define o tema e quais módulos estão ativos.", - workflowsSet: "Aplicar workflow", - workflow_default: "Predefinido", - workflow_jobs: "Emprego", - workflow_ruling: "Governo", - workflow_politics: "Política", - workflow_social: "Social", - workflow_business: "Negócios", - workflow_night: "Noite", - workflow_mobile: "Móvel", + modulesPreset_minimal: "Mínimo", + modulesPreset_basic: "Básico", + modulesPreset_social: "Social", + modulesPreset_economy: "Economia", + modulesPreset_full: "Completo", statsCarbonFootprintTitle: "Pegada de carbono", + statsCarbonFootprintNetwork: "Pegada de carbono da rede", + statsCarbonFootprintYours: "A tua pegada de carbono", statsCarbonTombstone: "Pegada do tombstoning", + feedSuccessMsg: "Feed publicado com sucesso!", + dominantOpinionLabel: "Opinião dominante", uploadMedia: "Carregar mídia (máx: 50MB)", courtsRespondentInvalid: "Demandado inválido", @@ -3008,13 +3165,14 @@ module.exports = { mapDescriptionLabel: "Descrição", mapDescriptionPlaceholder: "Descreva o mapa ou localização...", mapTypeLabel: "Tipo de mapa", - mapInviteMode: "Convite", - mapTypeSingle: "ÚNICO", - mapTypeOpen: "ABERTO", - mapTypeClosed: "FECHADO", + mapTypeSingle: "ÚNICO (apenas localização inicial)", + mapTypeOpen: "ABERTO (qualquer um pode adicionar marcadores)", + mapTypeClosed: "FECHADO (apenas o criador adiciona marcadores)", mapTagsLabel: "Tags", mapTagsPlaceholder: "Insira tags separadas por vírgulas", mapUrlLabel: "URL do mapa", + mapPickCoordLabel: "Ou selecione uma localização na grade:", + mapMarkersLabel: "marcadores", mapMarkersTitle: "Marcadores", mapMarkerDefault: "Marcador", mapMarkerLatLabel: "Latitude do marcador", @@ -3041,9 +3199,14 @@ module.exports = { padTitle: "Pad", modulesPadsLabel: "Pads", modulesPadsDescription: "Módulo para gerir editores de texto colaborativos.", + padFilterAll: "TODOS", + padFilterMine: "MEU", + padFilterRecent: "RECENTE", + padFilterOpen: "ABERTO", + padFilterClosed: "FECHADO", padCreate: "Criar Pad", - padUpdate: "Atualizar", - padDelete: "Eliminar", + padUpdate: "Atualizar Pad", + padDelete: "Eliminar Pad", padTitleLabel: "Título", padTitlePlaceholder: "Escreva o título do pad...", padStatusLabel: "Estado", @@ -3054,7 +3217,14 @@ module.exports = { padTagsLabel: "Tags", padTagsPlaceholder: "tag1, tag2, ...", padMembersLabel: "Membros", + padVisitPad: "Visitar Pad", + padShareUrl: "Partilhar URL", padCreated: "Criado", + padAuthor: "Autor", + padGenerateCode: "Gerar Código", + padInviteCodeLabel: "Código de Convite", + padInviteCodePlaceholder: "Introduza o código de convite...", + padValidateInvite: "Validar", padStartEditing: "COMEÇAR A EDITAR!", padEditorPlaceholder: "Comece a escrever...", padSubmitEntry: "Enviar", @@ -3067,11 +3237,12 @@ module.exports = { padClosedSectionTitle: "Pads Fechados", padCreateSectionTitle: "Criar Novo Pad", padUpdateSectionTitle: "Atualizar Pad", + padInviteGenerated: "Código de Convite Gerado", typePad: "PADS", padNew: "NOVO", padAddFavorite: "Adicionar aos Favoritos", padRemoveFavorite: "Remover dos Favoritos", - padClose: "Fechar", + padClose: "Fechar Pad", padBackToEditor: "Voltar ao editor", padSearchPlaceholder: "Pesquisar pads...", @@ -3079,6 +3250,8 @@ module.exports = { modulesChatsDescription: "Módulo para descobrir e gerir chats cifrados.", typeChat: "CHATS", typeChatMessage: "MENSAGEM CHAT", + chatLabel: "CHATS", + chatMessageLabel: "MENSAGENS CHAT", chatsTitle: "Chats", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3088,50 +3261,52 @@ module.exports = { chatDescription: "Descrição", chatCategory: "Categoria", chatStatus: "ESTADO", + chatFilterAll: "TODOS", + chatFilterMine: "MEU", + chatFilterRecent: "RECENTE", + chatFilterFavorites: "FAVORITOS", + chatFilterOpen: "ABERTO", + chatFilterClosed: "FECHADO", chatCreate: "Criar Chat", - chatUpdate: "Atualizar", - chatDelete: "Apagar", + chatUpdate: "Atualizar Chat", + chatDelete: "Eliminar Chat", chatClose: "Fechar Chat", chatVisitChat: "VER CHAT", chatUntitled: "Chat sem título", chatNoItems: "Nenhum chat encontrado.", chatParticipants: "Participantes", - chatMessagesLabel: "Mensagens", - chatThreadsLabel: "Tópicos", + chatStartChatting: "COMEÇAR A CONVERSAR!", + chatGenerateCode: "Gerar Código", + chatShareUrl: "Partilhar URL", chatCreatedAt: "CRIADO", chatSearchPlaceholder: "Pesquisar chats...", chatStatusOpen: "ABERTO", chatStatusInviteOnly: "SÓ POR CONVITE", chatStatusClosed: "FECHADO", chatSendMessage: "Enviar", - chatJumpLatest: "Última Mensagem", - chatPickChat: "Escolhe um chat para o abrir", - chatNoneYet: "Ainda não há nenhum chat disponível.", - activitySearchPlaceholder: "Procurar na atividade...", - trendingSearchPlaceholder: "Procurar nas tendências...", - opinionsSearchPlaceholder: "Procurar nas opiniões...", - votesSearchPlaceholder: "Procurar nas votações...", - welcomeStepUxTitle: "Escolhe a tua interface", - welcomeStepUxText: "Decide como se vê o Oasis. Podes mudar depois nas definições.", - welcomeStepUxAction: "Usar esta vista", - chatRateLimitTitle: "Demasiadas mensagens", - chatRateLimitMessage: "Atingiste o limite de mensagens que esta sala aceita numa hora. Tenta mais tarde.", chatMessagePlaceholder: "Escreva a sua mensagem...", chatNoMessages: "Sem mensagens ainda.", + chatLeave: "Sair do Chat", + chatInviteCodeLabel: "Introduza o código de convite", + chatJoinByInvite: "Aderir", chatTitlePlaceholder: "Título do chat", chatDescriptionPlaceholder: "Descrição do chat", chatTagsPlaceholder: "tag1, tag2, tag3", chatImageLabel: "Seleciona um ficheiro de imagem (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "Código de Convite", chatAuthor: "Autor", + chatCreated: "Criado", chatAddFavorite: "Adicionar aos Favoritos", chatRemoveFavorite: "Remover dos Favoritos", chatPM: "MP", chatStatusLabel: "Estado", chatCategoryLabel: "Categoria", + chatParticipantsLabel: "Participantes", gamesTitle: "Jogos", gamesDescription: "Descubra e jogue alguns jogos.", gamesFilterAll: "TODOS", gamesPlayButton: "JOGAR!", + gamesBackToGames: "Voltar aos Jogos", modulesGamesLabel: "Jogos", modulesGamesDescription: "Módulo para descobrir e jogar alguns jogos.", modulesGraphosLabel: "Graphos", @@ -3148,6 +3323,8 @@ module.exports = { gamesArkanoidDesc: "Quebre todos os tijolos com sua raquete e bola. Um clássico desafio arcade.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Ping-pong clássico contra uma IA. O primeiro a 5 pontos ganha.", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "Corrida contra o tempo! Desvie do tráfego e chegue à meta antes do tempo acabar.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Pilote sua nave por um campo de asteroides mortais. Destrua-os antes que te atinjam.", gamesRockPaperScissorsTitle: "Pedra Papel Tesoura", @@ -3168,6 +3345,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3178,6 +3357,12 @@ module.exports = { modulesCalendarsLabel: "Calendários", modulesCalendarsDescription: "Módulo para descobrir e gerir calendários.", typeCalendar: "CALENDÁRIOS", + calendarFilterAll: "TODOS", + calendarFilterMine: "OS MEUS", + calendarFilterOpen: "ABERTO", + calendarFilterClosed: "FECHADO", + calendarFilterRecent: "RECENTES", + calendarFilterFavorites: "FAVORITOS", calendarCreate: "Criar calendário", calendarUpdate: "Atualizar", calendarDelete: "Eliminar", @@ -3190,17 +3375,24 @@ module.exports = { calendarTagsLabel: "Etiquetas", calendarTagsPlaceholder: "etiqueta1, etiqueta2...", calendarParticipantsLabel: "Participantes", + calendarParticipantsCount: "Participantes", + calendarVisitCalendar: "Visitar calendário", calendarCreated: "Criado", + calendarAuthor: "Autor", calendarJoin: "Participar", calendarGenerateInvite: "Gerar convite", + calendarInviteCodePlaceholder: "Digite o código de convite...", + calendarValidateInvite: "Validar código", + calendarJoined: "Inscrito", + calendarAddDate: "Adicionar data", + calendarAddNote: "Adicionar nota", calendarDateLabel: "Data", calendarDatePlaceholder: "Descrever esta data...", - calendarNoteLabel: "Notas", + calendarNoteLabel: "Nota", calendarNotePlaceholder: "Adicionar uma nota...", calendarFirstDateLabel: "Data", calendarFirstNoteLabel: "Notas", calendarIntervalLabel: "Interval", - calendarIntervalNone: "Sem repetição", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3211,6 +3403,8 @@ module.exports = { calendarsDescription: "Descubra e gerencie calendários na sua rede.", calendarMonthPrev: "← Anterior", calendarMonthNext: "Seguinte →", + calendarMonthLabel: "Datas", + calendarsShareUrl: "URL de partilha", calendarAllSectionTitle: "Calendários", calendarRecentSectionTitle: "Calendários Recentes", calendarFavoritesSectionTitle: "Favoritos", @@ -3224,6 +3418,7 @@ module.exports = { calendarRemoveFavorite: "Remover dos favoritos", calendarSearchPlaceholder: "Pesquisar calendários...", calendarAddEntry: "Adicionar Entrada", + calendarLeave: "Sair do Calendário", statsCalendar: "Calendários", statsCalendarDate: "Datas de calendário", statsCalendarNote: "Notas de calendário", @@ -3270,6 +3465,7 @@ module.exports = { torrentSortOldest: "Mais antigos", torrentSortTop: "Mais votados", torrentNoMatch: "Nenhum torrent correspondente.", + torrentMessageAuthorButton: "Enviar Mensagem ao Autor", torrentUpdatedAt: "Atualizado", statsTorrent: "Torrents", typeTorrent: "TORRENTS", @@ -3277,18 +3473,10 @@ module.exports = { modulesTorrentsDescription: "Módulo para descobrir e gerenciar torrents.", favoritesFilterTorrents: "TORRENTS", favoritesFilterMarket: "MERCADO", - favoritesFilterEvents: "EVENTOS", - favoritesFilterForum: "FÓRUM", - favoritesFilterHousing: "HABITAÇÃO", - favoritesFilterJobs: "EMPREGOS", - favoritesFilterProjects: "PROJETOS", - favoritesFilterReports: "RELATÓRIOS", - favoritesFilterTasks: "TAREFAS", - favoritesFilterTransfers: "TRANSFERÊNCIAS", - favoritesFilterVotes: "VOTAÇÕES", favoritesFilterShopProducts: "ITENS DA LOJA", tribeSectionTorrents: "TORRENTS", tribeCreateTorrent: "Enviar Torrent", + tribeMediaTypeTorrent: "Torrent", settingsWishTitle: "Desejo", settingsWishDesc: "Configure o desejo do seu Avatar.", settingsWishWhole: "Multiverse", @@ -3299,10 +3487,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "O envio é uma barreira de UX. O recebimento é um filtro de atenção.", + pmBlockedNonMutual: "Só pode enviar MPs a habitantes em apoio mútuo.", inhabitantsPendingFollowsTitle: "Pedidos de apoio pendentes", inhabitantsPendingAccept: "Aceitar", inhabitantsPendingReject: "Rejeitar", + bxEncrypted: "CRIPTOGRAFADO", + bxEncryptedHexLabel: "Texto cifrado (pré-visualização)", tribeSectionGovernance: "GOVERNANÇA", + tribeSubStatusPublic: "PÚBLICA", + tribeSubStatusPrivate: "PRIVADA", + tribeSubInheritedPrivate: "PRIVADA (herdada da tribo principal)", tribePadInviteRequired: "Sem acesso ao pad. Peça um convite.", tribeChatInviteRequired: "Sem acesso ao chat. Peça um convite.", tribeGovernanceDesc: "Governança interna desta tribo.", @@ -3342,31 +3536,44 @@ module.exports = { tribeGovNoHistorical: "Ainda nenhum registo", logsTitle: "Diário", logsDescription: "Regista a sua experiência na rede.", + logsReadMore: "Ler mais", logsViewTitle: "Diário", logsColumnType: "Tipo", logsView: "Ver", + logsManualPrompt: "Escreva o seu diário", logsUpdateButton: "Atualizar", - logsEditTitle: "Atualizar registo", + logsEditTitle: "Editar entrada", + logsCreateDescription: "Regista a sua experiência...", logsCreateTitle: "Criar entrada", logsWriteButton: "Escrever", logsTextPlaceholder: "Descreva as suas experiências...", + logsTextField: "Texto", + logsLabelPlaceholder: "Rótulo...", + logsLabelField: "Escreva o seu diário (rótulo)", + logsAiDisabledWarn: "Ative o módulo de IA em /modules para usar entradas escritas pela IA.", + logsAiContextValue: "Dia de blockchain", + logsAiContext: "Contexto", + logsAiModStatus: "AImod", + logsModeApply: "Aplicar!", logsModeAIWritten: "AI-Assistant", logsModeManual: "Manual", logsModeAI: "IA", + logsColumnDelete: "Eliminar", + logsColumnEdit: "Editar", logsColumnLog: "Diário", logsColumnDate: "Data", logsDelete: "Eliminar", - logsEdit: "Atualizar", + logsEdit: "Editar", logsFilterAlways: "SEMPRE", logsFilterYear: "ÚLTIMO ANO", logsFilterMonth: "ÚLTIMO MÊS", logsFilterWeek: "ÚLTIMA SEMANA", logsFilterToday: "HOJE", - logsComments: "Comentários", - logsAuthorEntries: "Entradas", + logsFilterRecent: "RECENTES", + logsFilterAll: "TODOS", logsCreate: "Criar entrada", logsExport: "Exportar diário", - logsExportOne: "Exportar registo", + logsExportOne: "Exportar", logsViewDetails: "Ver detalhes", logsGenerateButton: "Gerar texto", logsSearchText: "Procurar nos diários...", @@ -3376,25 +3583,31 @@ module.exports = { modulesLogsLabel: "Diário", modulesLogsDescription: "Módulo para registar (via assistente de IA) as suas experiências.", statsLogsTitle: "Diário", + statsLogsEntries: "Entradas", typeLog: "DIÁRIO", + blockchainCycle: "Ciclo", larpTitle: "L.A.R.P.", larpDescription: "Uma camada de jogo de interpretação ao vivo para experimentação colaborativa.", - larpNoHousesMatch: "Nenhuma casa corresponde à sua pesquisa.", - larpSearchPlaceholder: "Pesquisar casas...", larpAllHouses: "Casas:", larpFilterRuling: "GOVERNA", larpFilterHouses: "CASAS", larpFilterRules: "FAQ", larpRulesTitle: "FAQ do L.A.R.P.", + larpRulesEntryTitle: "Casa inicial", larpRulesEntryText: "Cada habitante começa em ACADEMIA por defeito. A partir de ACADEMIA, escolhe uma casa no painel \"Entrar numa casa\": isto abre o seu teste de entrada. Responde às 10 perguntas e submete. Se passares o limiar (7/10), és automaticamente admitido nessa casa; caso contrário, és rejeitado e permaneces em ACADEMIA. Em qualquer caso o sistema regista o teu OasisID e o ciclo da tentativa e impede nova tentativa até decorrer o período de espera de 30 ciclos.", + larpRulesLeaveText: "Os membros de qualquer casa podem regressar a ACADEMIA a qualquer momento a partir da página da sua casa; isso repõe a sua participação, mas não anula o período de espera do teste.", + larpRulesGovernanceTitle: "Mural de governança", larpRulesGovernanceText: "Durante o seu ciclo, a casa governante exibe a insígnia \"Governa\" e é a única cujo Mural é exibido publicamente no separador GOVERNA.", - larpRulesWallText: "Cada casa tem um Muro onde os seus membros escrevem. O Muro de uma casa é privado, exceto o da casa governante e o da ACADEMIA, que qualquer pessoa pode ler.", + larpRulesSignTitle: "Emblema L.A.R.P.", larpRulesSignText: "Os habitantes podem ativá-lo (em /profile/edit > Sensores) para mostrar a imagem da sua casa atual como um selo no perfil, no cartão de autor e de habitante. O selo liga à página da casa.", larpPostsTitle: "Mural", larpPostsEmpty: "Ainda sem publicações.", + larpPostLabel: "Nova publicação", larpPostPlaceholder: "O que tem esta casa a dizer?", + larpPostPublic: "Público (visível para qualquer pessoa, senão só para membros)", larpPostSubmit: "Publicar", + larpPostMembersOnly: "Apenas membros", larpCycleLabel: "Ciclo:", audioTranscodeCompositionEmpty: "This audio does not include a stored blockchain composition.", audioTranscodeDetailDescription: "Decode the embedded payload and the original blockchain composition map.", @@ -3447,9 +3660,12 @@ module.exports = { bankTaxesLookupNote: "Introduz um ID de bloco para o procurar no teu feed local.", bankTaxesUserNoteChipsClick: "Clica em qualquer chip para inspecionar o seu bloco no blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntry: "Código de convite", larpRulesInviteEntryText: "Os membros das casas podem gerar códigos de convite que dão acesso direto à casa.", larpRulesOneHouseText: "Só podes pertencer a uma casa de cada vez. Se não pertenceres a nenhuma, estás em ACADEMIA. Cada página de casa (não ACADEMIA) mostra um botão \"Sair da Casa\" aos seus membros que reinicia a pertença a ACADEMIA. Sair NÃO cancela o tempo de espera do teste.", - larpRulesTestEntryText: "No teste WILL cada opção pondera uma ou mais casas; ao enviar, o sistema soma as pontuações e admite-te automaticamente na casa com mais pontos.", + larpRulesOneHouseTitle: "Uma casa de cada vez", + larpRulesTestEntry: "Teste WILL", + larpRulesTestEntryText: "Cada opção pondera uma ou mais casas; ao submeter, o sistema soma os pontos e admite-te automaticamente na casa com a maior pontuação.", larpWillTestHint: "Uma vez concluído, ser-te-á atribuída a casa que melhor corresponde às tuas respostas. As tuas respostas individuais são processadas localmente e nunca armazenadas — apenas a atribuição de casa resultante é publicada no teu feed.", larpWillTestTitle: "Teste WILL", settingsLanDesc: "Anuncia periodicamente este peer a outras instâncias do Oasis na mesma rede local. Desativar para parar as difusões UDP.", @@ -3468,30 +3684,36 @@ module.exports = { aiExchangeMarkHelpful: "+1 útil", larpCyclesUntilRuling: "Próximo ciclo de governança", larpVisitTribe: "Visitar tribo", - larpStartJourney: "Começar a jornada", larpGoverning: "Governa:", - larpStartHere: "Começa aqui:", larpMyHouse: "A minha Casa:", larpMembersCount: "Membros", + larpMembersTitle: "Membros", + larpNoMembers: "Ainda sem membros.", larpRolesLabel: "Funções", larpFunctionLabel: "Função", larpMonthLabel: "Ciclo de governança", larpLeaveToAcademia: "Voltar a ACADEMIA", - larpAcademiaJoinTitle: "Casas L.A.R.P.", + larpAcademiaJoinTitle: "Entrar numa casa", + larpAcademiaJoinHint: "Faz o teste psicológico de perfil para que te seja atribuída a casa que melhor encaixa contigo, ou usa um código de convite partilhado por um membro de uma casa.", + larpTakeTestButton: "Fazer o teste de perfil", larpInviteRedeem: "Entrar na Casa", larpInviteCreate: "Gerar código de convite", larpInvitePlaceholder: "Código de convite", larpInviteBannerTitle: "Novo código de convite", larpInviteBannerHint: "Partilha este código com alguém em ACADEMIA. Expira em 30 ciclos ou após um único uso.", + larpTestAssignedHouse: "Casa atribuída", larpTestRankingTitle: "Pontuação por casa", invitesHousesTitle: "L.A.R.P.", invitesHouseInviteCodePlaceholder: "Código de convite de casa", invitesHouseJoinButton: "Entrar na Casa", ecoTaxLabel: "ECO Tax", + ecoinTaxLabel: "ECOin Tax", bankTaxes: "Impostos", bankOverviewYourTaxes: "Os teus impostos", + bankTaxesNetworkTitle: "Impostos da rede", bankTaxesEcoTaxTitle: "ECO Tax", bankTaxesArchTaxTitle: "ARCH Tax", + bankTaxesUserTitle: "Os teus impostos", bankTaxesUserAmount: "A tua ECO Tax (preço a devolver)", bankTaxesArchTaxUserAmount: "A tua ARCH Tax (preço a devolver)", bankTaxesArchTaxFirstBlock: "Idade do teu primeiro bloco", @@ -3530,18 +3752,22 @@ module.exports = { bankRulesEcoTaxTitle: "ECO Tax", bankRulesArchTaxTitle: "ARCH Tax", bankRulesChipTitle: "Faixa de cor do chip ECO Tax", + statsEcoTaxTitle: "ECO Tax", statsTombstoneEmpty: "Ainda sem tombstones na rede.", blockchainBlockNotAvailable: "Este bloco não está disponível no teu feed local. Pode pertencer a um peer do qual ainda não replicas.", blockchainBackToBlockexplorer: "Voltar ao blockexplorer", audioFilterBcs: "BCS", audioTranscodeButton: "TRANSCODE", + audioTranscodeTitle: "BCS Transcode", audioTranscodeDetailTitle: "Transcodificar", audioTranscodeStegoOasisId: "Por", audioTranscodeStegoTimestamp: "Gerado em", audioTranscodeStegoMessage: "TEXT", audioTranscodeStegoEmpty: "(nenhum)", audioTranscodeStegoUnknown: "—", + audioBackToAudio: "Voltar ao áudio", audioBackToBcs: "Voltar ao BCS", + audioAuthorLabel: "Autor", melodyCompositionTitle: "Mapa de composição da blockchain", melodyFilterMine: "MEU", melodyByLabel: "Por", @@ -3555,6 +3781,7 @@ module.exports = { melodyUploadBcsNameNote: "O áudio será publicado com o nome auto-gerado", larpLastAttemptTitle: "A tua última tentativa", larpLastAttemptHouse: "Casa", + larpLastAttemptResult: "Resultado", larpLastAttemptWhen: "Quando", larpLastAttemptCooldown: "Próxima tentativa em", larpTestTitle: "Teste de entrada", @@ -3563,14 +3790,30 @@ module.exports = { larpTestCooldownActive: "Próximo teste disponível em", larpTestCooldownDays: "ciclos", larpTestResultTitle: "Resultado do teste", + larpTestPassed: "Teste passado — bem-vindo!", + larpTestFailed: "Teste não passado", larpTestScore: "Pontuação", larpTestNextAttempt: "Podes tentar outro teste em 30 ciclos.", larpGoToHouse: "Ir para a tua casa", larpBackToAcademia: "Voltar a ACADEMIA", + larpBackToHouses: "◀ Casas", larpBadgeYou: "Tu", larpBadgeRuling: "Governa", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Módulo para a camada de jogo de interpretação ao vivo do Oasis (as 9 Casas).", + melodyUploadTitlePlaceholder: "Composition title", + melodyUploadDescPlaceholder: "Optional description", + bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", + bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", + bankTaxesExample: "Example", + bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", + audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", + audioTranscodeStegoTitle: "Embedded in the waveform", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + mapClickToCreate: "Click on the map to select a location", + mapClickToAddMarker: "Click on the map to add a marker", + gameScoreLabel: "GAME SCORES", larpProfileQ1: "Diante de um problema complexo, o que fazes primeiro?", larpProfileQ1O1: "Falar com outras pessoas para procurar consenso", larpProfileQ1O2: "Construir um protótipo para testar", diff --git a/src/client/assets/translations/oasis_ru.js b/src/client/assets/translations/oasis_ru.js index cd9fee9..d422c1d 100644 --- a/src/client/assets/translations/oasis_ru.js +++ b/src/client/assets/translations/oasis_ru.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { ru: { + bbAdd: "Добавить", + bbQuickActions: "Быстрые действия", + bbPickTitle: "Закрепить модуль", + bbPickDesc: "Выберите модуль для слота + в нижней панели.", + bbPickNone: "Нет", + bbManage: "Изменить", + bbYourBar: "Ваша панель", + bbRemove: "Убрать", + bbFull: "Достигнут максимум", + bbDone: "Готово", + activityChangeFilter: "Сменить фильтр", + emptyConnectHint: "Подключитесь к пабу для синхронизации с сетью.", + emptyConnectCta: "Подключиться к пабу", + menuCommunity: "Сообщество", + activityGroupCommunity: "Сообщество и управление", + activityGroupOrg: "Организация", + activityGroupComm: "Коммуникация", + activityGroupEconomy: "Экономика", + activityGroupMedia: "Контент и медиа", + activityGroupOther: "Другое", + languageName: "Русский", shopOrderStatusPaid: "Оплата получена", shopOrderStatusReceived: "Получено", shopOrderMarkPaid: "Отметить оплату полученной", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "У вас пока нет покупок.", shopOrderPmSeller: "PM Seller", shopOrderSeller: "Продавец", + shopOrderPaymentConcept: "Оплата", shopOrderInvoice: "Счёт", shopOrderInvoiceConcept: "Счёт", shopOrderStatusPending: "Ожидает", shopOrderStatusAccepted: "Принят", shopOrderStatusRejected: "Отклонён", shopOrderStatusShipped: "Отправлен", + shopOrderStatusDelivered: "Доставлен", shopOrderAccept: "Принять", shopOrderReject: "Отклонить", shopOrderMarkShipped: "Отметить отправленным", + shopOrderMarkDelivered: "Отметить доставленным", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "Смарт-контракт", shopOrderContractConcept: "Заказ магазина", shopOrdersPending: "Ожидающие заказы", all: "Все", @@ -50,13 +75,14 @@ module.exports = { viewJson: "Просмотр JSON", matchScore: "Совпадение", preferencesLabel: "Предпочтения", - inhabitantProfileTitle: "Житель", + inhabitantProfileTitle: "Профиль жителя", contentWarningLabel: "Предупреждение о контенте", invalidPost: "Недопустимое содержимое", invalidPostHint: "В этом сообщении пустой/недопустимый текст.", spreadBy: "От", spreadChron: "Распространение", spreaded: "Распространено", + spreadMore: "ещё", spreadContentUnavailable: "Контент пока недоступен (ожидается репликация)", filteredByTag: "Отфильтровано по тегу", filteredByTagTitle: "Отфильтровано по тегу", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "Без серьёзности", calendarDeleteDate: "Удалить дату", calendarIntervalUntil: "До", + bookmarkCategory: "Категория", bookmarkLastVisit: "Последний визит", profileClearnetBookmarksLabel: "Закладки", + forumFilterHot: "Популярные", invitesPort: "Порт", currentlyUnrecheable: "ОШИБКА!", peerHostValidation: "Корректный IPv4 (напр. 192.168.1.100) или имя хоста (напр. pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "Годовая макс. оценка", statsCarbonNetwork: "Всего по сети", statsCarbonOfEstMax: "от оценочной макс. ёмкости", - statsCarbonYearProgress: "года прошло", - statsNetworkTitle: "Сеть", - statsStorageTitle: "Хранилище", - statsEcoTaxTitle: "ECO-налог", - statsAccountTitle: "Аккаунт", - statsAveragesTitle: "Средние значения", statsCarbonOfNetwork: "от общего по сети", statsCarbonUser: "Ваш след", statsContentCountColumn: "Количество", @@ -198,13 +220,15 @@ module.exports = { graphosShowing: "Показано", graphosYou: "Вы", graphosTotalNodes: "Всего узлов", + manualMode: "Ручной режим", mentions: "Упоминания", mentionsDescription: [ - strong("Каждое сообщение, где вас @упоминают"), - ".", + strong("Записи, в которых @упоминают вас"), + ", в обратном хронологическом порядке.", ], + nextPage: "Далее", + previousPage: "Назад", noMentions: "Вы ещё не получали @упоминаний.", - mentionsSearchPlaceholder: "Поиск упоминаний...", private: "Входящие", privateInbox: "ВХОДЯЩИЕ", privateReminders: "НАПОМИНАНИЯ", @@ -212,11 +236,15 @@ module.exports = { inboxToggleToMutuals: "Переключить на взаимных", inboxToggleToWhole: "Переключить на всех", privateSent: "ОТПРАВЛЕННЫЕ", + privateFrom: "От", + privateTo: "Кому", privateDate: "Дата", privateDelete: "Удалить", pmCreateButton: "Написать ЛС", pmReply: "Ответить", pmReplies: "ответы", + pmNew: "новое", + pmMarkRead: "Отметить как прочитанное", inReplyTo: "В ОТВЕТ НА", pmPreview: "Предпросмотр", pmPreviewTitle: "Предпросмотр сообщения", @@ -225,8 +253,11 @@ module.exports = { privateDescription: ["Личные сообщения ",strong("зашифрованы вашим открытым ключом")," и могут иметь максимум 7 получателей."], search: "Поиск", searchDescription: "Описание", + imageSearch: "Поиск изображений", searchFromLabel: "С", searchToLabel: "До", + searchMinOpinionsLabel: "Мин. мнений", + searchInhabitantLabel: "Житель (Oasis ID)", searchQueryLabel: "Запрос", searchPerPageLabel: "Результатов на странице", settings: "Настройки", @@ -239,39 +270,68 @@ module.exports = { melodyDescription: 'Воспроизведите мелодию вашего блокчейна — каждый блок становится нотой.', melodyEmpty: 'Ещё нет нот — ваш блокчейн пока не создал ни одного блока.', melodyCompositionTitle: 'Композиция', + melodyCompositionHelp: 'Каждый блок вашего блокчейна становится музыкальной нотой по типу и размеру.', + melodyStatsTitle: 'Разбивка по типам', + melodyInhabitantLabel: 'Житель', melodyTotalBlocks: 'Ноты', + melodyType: 'Тип', + melodyCount: 'Блоки', melodyFilterMine: 'МОЁ', melodyFilterAll: 'ВСЕ', + melodyFilterShare: 'Загрузить мелодию', + melodyShareTitle: 'Поделитесь своей мелодией', + melodyShareHelp: 'Опубликуйте снимок вашей текущей мелодии, чтобы другие могли её услышать и переработать.', + melodyShareTitleLabel: 'Название (необязательно)', + melodyShareTitlePlaceholder: 'Призрак блокчейна', + melodyShareSubmit: 'Опубликовать мелодию', + melodyNoShared: 'Ещё нет мелодий блокчейна.', melodyRegenerate: 'Перегенерировать', melodyDownload: "Скачать BCS", + profileActivityTitle: 'Активность', + profileActivityMore: 'Вся активность', profileContentSectionTitle: 'Содержимое аватара', profileContentHelp: 'Выбери, какие модули будут отображаться в твоём аватаре.', profileSensorsHelp: 'Дополнительные метрики, отображаемые в аватаре.', profileClearnetHelp: 'Модули, доступные извне Oasis.', profileHubAll: 'Все', + profileHubTitle: 'Публичное содержимое', + footerPulseTitle: 'Пульс', + footerPulsePeers: 'Узлы', + footerPulseInbox: 'Входящие', + footerPulseSync: 'Посл. синх.', + footerPulseEcoValue: 'ECO/ч', + footerPulseLastActivity: 'Последняя активность', footerLicenseLabel: 'Лицензия', profileSwitchToOasis: 'Вернуться в Oasis', - profileVisibilityFediverse: "Мультивселенная", + profileSwitchToClearnet: 'Погрузиться в Clearnet', + profileVisibilityFediverse: "Федиверс", + profileSwitchToFediverse: "Погрузиться в федиверс", + coordLabel: 'Координата (напр., A3)', + coordPlaceholder: 'Введите координату', contributorsTitle: "Участники", + pixeliaBy: "автор", colorLabel: 'Выберите цвет', paintButton: 'Рисовать!', + invalidCoordinate: 'Неверная координата', + goToMuralButton: "Смотреть фреску", totalPixels: 'Всего пикселей', modules: "Модули", modulesViewTitle: "Модули", modulesViewDescription: "Настройте свою среду, включая или отключая модули.", inbox: "Входящие", multiverse: "Мультивселенная", - fediverse: "Мультивселенная", - fediverseDescription: "Управляйте своими другими аккаунтами мультивселенной, включая отправку и получение контента.", + fediverse: "Федиверс", + fediverseDescription: "Управляйте своими другими аккаунтами федиверса, включая отправку и получение контента.", fediverseStatus: "Статус", - fediverseDisconnected: "Мультивселенная отключён. Проверьте %LINK% или состояние подключения.", - fediverseSettingsTitle: "Мультивселенная", + fediverseDisconnected: "Федиверс отключён. Проверьте %LINK% или состояние подключения.", + fediverseSettingsTitle: "Федиверс", fediverseInstanceLabel: "Адрес", fediverseTokenLabel: "Токен доступа", fediverseTokenHelp: "Укажите ваш токен доступа. Он будет использоваться для управления вашим аккаунтом Mastodon из Oasis.", fediverseConnect: "Подключить", fediverseConnectedAs: "Подключено как", fediverseDisconnect: "Отключить", + fediverseEmpty: "Когда вы подключите другие аккаунты федиверса из %LINK%, вы сможете управлять ими отсюда.", fediverseEmptyLink: "настроек", fediverseComposePlaceholder: "О чём вы думаете?", fediverseReplyPlaceholder: "Напишите ответ…", @@ -280,15 +340,21 @@ module.exports = { fediverseReply: "Ответить", fediverseBoosted: "продвинул(а)", fediverseNoPosts: "Ваша лента пуста.", + fediverseThread: "Обсуждение", fediverseTimeline: "Ленты", + fediverseManageTimeline: "Управлять лентой", fediverseFollowers: "Подписчики", fediverseFollowing: "Подписки", fediversePosts: "Посты", fediverseJoined: "Регистрация", + fediverseOverview: "Обзор", fediverseRefresh: "Обновить", fediverseWrite: "Написать", fediversePreview: "Предпросмотр", + fediverseEdit: "Изменить", + fediverseOpenFeed: "Открыть ленту", fediverseManage: "Управление", + fediverseStatusNotConnected: "Не подключено", fediverseError: "Не удалось связаться с Mastodon.", fediverseErrInstance: "Неверный URL сервера.", fediverseErrAuth: "Неверный или просроченный токен.", @@ -299,6 +365,17 @@ module.exports = { fediverseErrPost: "Не удалось опубликовать.", fediverseErrMedia: "Не удалось загрузить файл.", fediverseErrEmpty: "Напишите что-нибудь или прикрепите файл.", + popularLabel: "Избранное", + topicsLabel: "Темы", + latestLabel: "Последние", + summariesLabel: "Сводки", + threadsLabel: "Обсуждения", + multiverseLabel: "Мультивселенная", + inboxLabel: "Входящие", + invitesLabel: "Приглашения", + walletLabel: "Кошелёк", + legacyLabel: "Ключи", + cipherLabel: "Шифратор", bookmarksLabel: "Закладки", videosLabel: "Видео", torrentsLabel: "Торренты", @@ -309,14 +386,18 @@ module.exports = { inhabitantsLabel: "Жители", trendingLabel: "В тренде", eventsLabel: "События", + tasksLabel: "Задачи", apply: "Применить", menuPersonal: "Личное", + menuContent: "Контент", + menuBlogs: "Блоги", menuGovernance: "Управление", menuOffice: "Офис", - menuNetwork: "Сообщество", + menuMultiverse: "Мультивселенная", + menuNetwork: "Сеть", menuCreative: "Творчество", menuEconomy: "Экономика", - menuMedia: "Библиотека", + menuMedia: "Медиа", menuTools: "Инструменты", comment: "Комментарий", subtopic: "Подтема", @@ -326,6 +407,13 @@ module.exports = { follow: "Поддержать", block: "Заблокировать", unblock: "Разблокировать", + newerPosts: "Более новые записи", + olderPosts: "Более старые записи", + feedRangeEmpty: "Данный диапазон пуст для этой ленты. Попробуйте просмотреть ", + seeFullFeed: "полную ленту", + feedEmpty: "Сеть Oasis никогда не видела записей от этого аккаунта.", + beginningOfFeed: "Это начало ленты", + noNewerPosts: "Более новых записей пока не получено.", relationshipNotFollowing: "Вас не поддерживают", relationshipTheyFollow: "Поддерживает вас", relationshipMutuals: "Взаимная поддержка", @@ -335,20 +423,25 @@ module.exports = { relationshipBlockedBy: "Вы заблокированы", relationshipMutualBlock: "Взаимная блокировка", relationshipNone: "Вы не поддерживаете", + relationshipConflict: "Конфликт", relationshipBlockingPost: "Заблокированная запись", viewLikes: "Посмотреть распространения", spreadedDescription: "Список записей, распространённых жителем.", + totalspreads: "Всего распространений", + attachFiles: "Прикрепить файлы", preview: "Предпросмотр", publish: "Написать", contentWarningPlaceholder: "Добавьте тему к записи (необязательно)", - publishWarningPlaceholder: "Текст ограничен 7000 символами.", - publishTooLong: "Ваш пост слишком длинный. Пожалуйста, сократите его.", + privateWarningPlaceholder: "Добавьте жителей для отправки приватной записи (необязательно)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "ПОМНИТЕ: Благодаря технологии блокчейн, после публикации запись невозможно отредактировать или удалить.", ], commentWarning: [ "ПОМНИТЕ: Благодаря технологии блокчейн, после публикации запись невозможно отредактировать или удалить.", ], + commentPublic: "публичный", + commentPrivate: "приватный", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -368,6 +461,7 @@ module.exports = { ".", ], publishCustom: "Написать расширенную запись", + subtopicLabel: "Создать подтему для этой записи", messagePreview: "Предпросмотр записи", mentionsMatching: "Совпадающие упоминания", mentionsName: "Имя", @@ -390,93 +484,21 @@ module.exports = { ssbLogStream: "Блокчейн", ssbLogStreamDescription: "Настройте лимит сообщений для потоков блокчейна.", saveSettings: "Сохранить настройки", + exportTitle: "Экспорт данных", exportDescription: "Установите пароль (минимум 32 символа) для шифрования вашего ключа", exportDataTitle: "Резервная копия", exportDataDescription: "Скачайте ваши данные (без секретного ключа!)", exportDataButton: "Скачать базу данных", + pubWallet: "PUB Кошелёк", + pubWalletDescription: "Установите URL кошелька PUB. Он будет использоваться для транзакций PUB (включая UBI).", + pubWalletConfiguration: "Сохранить конфигурацию", + importTitle: "Импорт данных", importDescription: "Импортируйте ваш зашифрованный секрет (приватный ключ) для активации аватара", + importAttach: "Прикрепить зашифрованный файл (.enc)", + passwordLengthInfo: "Пароль должен содержать минимум 32 символа.", passwordImport: "Введите пароль для расшифровки данных, которые будут сохранены в домашней директории системы (имя файла: secret)", exportPasswordPlaceholder: "Используйте строчные, заглавные буквы, цифры и символы", - fileInfo: "Ваш зашифрованный секретный ключ будет скачан на ваше устройство (имя файла: oasis.enc)", - developer: "Разработка", - devTitle: "Разработка", - welcomeBannerText: "Добро пожаловать в OASIS. Выполните эти быстрые шаги, чтобы настроить свой аватар.", - aiSuggestionBanner: "Совет:", - aiSuggestionsEnable: "Советы ИИ", - welcomeBannerAction: "Начать!", - welcomeTitle: "Добро пожаловать", - welcomeStepGreetingAction: "Отправить", - welcomeJoinPubButton: "Войти в паб", - welcomeStepLarpAction: "Вступить в «Академию»", - welcomeStepLarpText: "Присоединяйтесь к нашей ролевой игре живого действия.", - welcomeStepLarpTitle: "Присоединиться к L.A.R.P.", - welcomeStepGreetingTitle: "Отправьте «Привет, мир!»", - welcomeStepGreetingText: "Отправьте сообщение, чтобы другие жители могли вас найти.", - welcomeJoinPub: "Присоединиться:", - welcomeDescription: "Несколько вещей, которые стоит сделать перед началом.", - welcomeProgress: "Выполнено", - welcomeStepDone: "готово", - welcomeStepPending: "не сделано", - welcomeSkip: "Не сейчас", - welcomeStepLanguageTitle: "Выберите язык", - welcomeStepLanguageText: "Oasis говорит на нескольких языках. Изменить можно в любой момент.", - welcomeStepProfileTitle: "Заполните профиль", - welcomeStepProfileText: "Выберите имя, добавьте описание и поставьте изображение, чтобы другие жители вас узнавали.", - welcomeStepProfileAction: "Сохранить профиль", - welcomeStepFederationTitle: "Присоединитесь к основной сети", - welcomeStepFederationText: "Подключитесь к нашему пабу, чтобы встретить других жителей и начать репликацию.", - welcomeStepFederationAction: "К приглашениям", - welcomeStepBackupTitle: "Сохраните свой ID", - welcomeStepBackupText: "Ваша личность — это файл ключа на этом устройстве.", - welcomeStepBackupWarning: "ЕСЛИ ВЫ ЕГО ПОТЕРЯЕТЕ, НИКТО НЕ СМОЖЕТ ЕГО ВОССТАНОВИТЬ.", - welcomeStepBackupAction: "Резервная копия!", - welcomeCompleteTitle: "Всё готово.", - welcomeCompleteText: "Ваш узел готов. Дальше сеть растёт вместе с теми, на кого вы подписаны.", - welcomeFindPeople: "Найти жителей", - devFilterFiles: "ФАЙЛЫ", - devFilterSearch: "ПОИСК", - devFilterModules: "МОДУЛИ", - devFilterTranslations: "ПЕРЕВОДЫ", - devFilterStyles: "СТИЛИ", - devFilterTests: "ТЕСТЫ", - devVersion: "Версия", - devNodeVersion: "Node", - devStatsTests: "Наборы тестов", - devParentFolder: "Родительская папка", - devOpenFolder: "открыть", - devDescription: "Изучайте исходный код, работающий на этом узле.", - devRoot: "корень", - devSearchPlaceholder: "Текст для поиска в коде", - devAnyExtension: "Любой файл", - devStatsFiles: "Файлы", - devStatsLines: "Строки", - devStatsModules: "Модули", - devStatsLanguages: "Языки", - devNotViewable: "нельзя открыть", - devEmptyFolder: "Эта папка пуста.", - devSize: "Размер", - devShowing: "Показано", - devLine: "строка", - devReportBug: "Сообщить об Ошибке", - devCreateTask: "Создать Задачу", - devRaw: "RAW", - devReportContext: "Найдено при чтении исходного кода Oasis", - devTaskPrefix: "Проверить", - devPrevPage: "Предыдущие строки", - devNextPage: "Следующие строки", - devMatches: "совпадений", - devFilesScanned: "файлов проверено", - devNoResults: "Совпадений пока нет.", - devColModule: "Модуль", - devColState: "Состояние", - devColModel: "Модель", - devColView: "Представление", - devColRoutes: "Маршруты", - devColTests: "Тесты", - devOn: "вкл", - devOff: "выкл", - modulesDevLabel: "Разработка", - modulesDevDescription: "Модуль для управления исходным кодом Oasis.", + fileInfo: "Ваш зашифрованный секретный ключ будет сохранён в домашней директории системы (имя файла: oasis.enc)", themeIntro: "Выберите тему.", setTheme: "Установить тему", @@ -484,13 +506,16 @@ module.exports = { languageDescription: "Если вы хотите использовать другой язык, выберите его здесь.", setLanguage: "Установить язык", + peerConnections: "Узлы", peerConnectionsIntro: "Управляйте всеми подключениями к другим узлам.", peerConnectionsTitle: "Подключения", online: "В сети", offline: "Не в сети", discovered: 'Обнаружен', unknown: 'Неизвестно', + lanBroadcastLabel: "LAN-вещание", lanBroadcastActive: "Активно (UDP-multicast в LAN)", + lanBroadcastDisabled: "Отключено (режим pub)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -503,6 +528,8 @@ module.exports = { noConnections: "Нет подключённых узлов.", noDiscovered: "Нет обнаруженных узлов.", noUnknownPeers: "Нет неизвестных пиров в графе друзей.", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -512,6 +539,9 @@ module.exports = { peerImport: "Импорт", peerImportTitle: "Импорт списка пиров", peerImportPlaceholder: "Вставьте по одному multiserver-адресу на строку или загрузите .txt файл…", + noSupportedConnections: "Нет поддерживаемых узлов.", + noBlockedConnections: "Нет заблокированных узлов.", + noRecommendedConnections: "Нет рекомендованных узлов.", connectionActionIntro: "", startNetworking: "Запустить сеть", @@ -525,19 +555,9 @@ module.exports = { homePageDescription: "Выберите модуль, который будет вашей главной страницей.", saveHomePage: "Установить главную", invites: "Приглашения", + invitesTitle: "Приглашения", + invitesInvites: "Приглашения", invitesDescription: "Управляйте и применяйте коды приглашений в вашей сети.", - invitesPubsHint: "Вставьте пригласительный код паба, чтобы федерироваться с ним и начать реплицировать его жителей.", - invitesFederationsHint: "Пабы, с которыми вы федерированы: обновите их, удалите недоступные или экспортируйте список.", - invitesHousesHint: "Введите код дома, чтобы вступить в дом LARP и участвовать в его экономике.", - invitesTribesHint: "Введите код племени, чтобы стать участником и получить доступ к его закрытому содержимому.", - invitesChatsHint: "Введите код чата, чтобы присоединиться к беседе и её темам.", - invitesPadsHint: "Введите код пада, чтобы вместе писать в общем документе.", - invitesCalendarsHint: "Введите код календаря, чтобы делиться датами и напоминаниями.", - invitesEventsHint: "Введите код события, чтобы присоединиться к закрытому событию.", - invitesForumsHint: "Введите код форума, чтобы читать и отвечать в закрытом форуме.", - invitesMapsHint: "Введите код карты, чтобы видеть и добавлять места на общей карте.", - invitesShopsHint: "Введите код магазина, чтобы присоединиться к магазину и его каталогу.", - invitesInhabitantsHint: "Введите код жителя, чтобы подписаться друг на друга и обмениваться содержимым.", invitesTribesTitle: "Племена", invitesTribeInviteCodePlaceholder: "Введите код приглашения племени", invitesTribeJoinButton: "Вступить в племя", @@ -568,6 +588,7 @@ module.exports = { invitesAlreadyFederated: "Вы уже федерированы с этим пабом.", invitesFederationsTitle: "Федерации", invitesAcceptedInvites: "Федеративные", + invitesNoInvites: "Приглашения ещё не приняты.", invitesUnfollow: "Отписаться", invitesFollow: "Подписаться", invitesUnfollowedInvites: "Нефедеративные", @@ -587,24 +608,39 @@ module.exports = { panicMode: "Режим паники!", encryptData: "Установите пароль (минимум 32 символа) для шифрования вашего блокчейна", decryptData: "Введите пароль для расшифровки вашего блокчейна", + panicModeDescription: "Зашифровать/Расшифровать или УДАЛИТЬ ваш блокчейн", removeDataDescription: "ВНИМАНИЕ: Этот процесс нельзя отменить.", + encryptPanicButton: "Зашифровать блокчейн", + decryptPanicButton: "Расшифровать блокчейн", removePanicButton: "УДАЛИТЬ ВСЕ ДАННЫЕ!", searchTitle: "Поиск", searchDescriptionLabel: "Поиск контента в вашей сети.", + searchLanguagesLabel: "Языки", + searchSkillsLabel: "Навыки", searchPlaceholder:"Искать контент...", + searchDateLabel:"Дата", searchLocationLabel:"Местоположение", searchPriceLabel:"Цена", + searchUrlLabel:"URL", searchCategoryLabel:"Категория", + searchStartLabel:"Время начала", + searchEndLabel:"Время окончания", searchPriorityLabel:"Приоритет", searchStatusLabel:"Статус", statusLabel:"Статус", + totalVotesLabel:"Всего голосов", noResultsFound:"Результатов не найдено.", votesOption:"Варианты голосования", voteYesLabel:"За", voteNoLabel:"Против", allTypesLabel: "БЕЗ ОГРАНИЧЕНИЙ", + ABSTENTIONLabel:"ВОЗДЕРЖАНИЕ", YESLabel:"ЗА", NOLabel:"ПРОТИВ", + FOLLOW_MAJORITYLabel: "СЛЕДОВАТЬ БОЛЬШИНСТВУ", + CONFUSEDLabel: "ЗАМЕШАТЕЛЬСТВО", + NOT_INTERESTEDLabel: "НЕ ИНТЕРЕСНО", + StatusLabel:"Статус", votesOptionYesLabel:"За", votesOptionNoLabel:"Против", votesOptionAbstentionLabel:"Воздержание", @@ -612,6 +648,7 @@ module.exports = { votesCreatedByLabel:"Создано", voteStatusOpen:"ОТКРЫТО", voteStatusClosed:"ЗАКРЫТО", + imageSearchLabel: "Введите слова для поиска изображений с соответствующими метками.", commentDescription: ({ parentUrl }) => [ " прокомментировал ", a({ href: parentUrl }, " обсуждение"), @@ -622,20 +659,53 @@ module.exports = { a({ href: parentUrl }, " записи"), ], subtopicTitle: ({ authorName }) => [`Подтема записи @${authorName}`], + mysteryDescription: "опубликовал загадочную запись", oasisDescription: "Сеть проекта OASIS", searchSubmit: "Искать...", + postLabel: "ЗАПИСИ", + aboutLabel: "ЖИТЕЛИ", + feedLabel: "ЛЕНТЫ", votesLabel: "ГОЛОСОВАНИЯ", + reportLabel: "ОТЧЁТЫ", + imageLabel: "ИЗОБРАЖЕНИЯ", + videoLabel: "ВИДЕО", + audioLabel: "АУДИО", + documentLabel: "ДОКУМЕНТЫ", + torrentLabel: "ТОРРЕНТЫ", pdfFallbackLabel: "PDF документ", + eventLabel: "СОБЫТИЯ", + taskLabel: "ЗАДАЧИ", + transferLabel: "ПЕРЕВОДЫ", + curriculumLabel: "РЕЗЮМЕ", + bookmarkLabel: "ЗАКЛАДКИ", + tribeLabel: "ПЛЕМЕНА", + marketLabel: "РЫНОК", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "КОШЕЛЬКИ", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "Отправить", + subjectLabel: "Тема", editProfile: "Редактировать аватар", + profileQrAlt: "QR-код профиля", + profileQrHint: "Отсканируйте, чтобы скопировать этот Oasis ID.", oasisIdLabel: "OasisID", - spreadContent: "Реплицировать", + spreadLabel: "Распространить", + spreadHint: "Распространить вашим сторонникам (через ваш feed).", welcomePmSubject: "Hello World!", - welcomePmBody: "Добро пожаловать в Oasis — свободная, одноранговая, федеративная и зашифрованная социальная сеть с распределённой архитектурой только по приглашениям.\n\nНесколько интересных мест, с которых можно начать:\n\n- /profile — Отредактируй аватар, имя, описание и какие модули видны на твоей публичной стороне.\n- /invites — Добавь pub, чтобы федерироваться с остальной сетью.\n- /peers — Посмотри, кто на связи, пересканируй LAN, экспортируй или импортируй списки пиров.\n- /inhabitants — Открывай и заходи к аватарам других людей.\n- /tribes — Создавай или вступай в приватные группы со сквозным шифрованием.\n- /inbox — Читай и отправляй приватные сообщения.\n- /activity — Смотри свежую активность — свою и твоей сети.\n- /publish — Напиши пост или поделись изображением, аудио, видео или документом.\n- /search — Ищите содержимое сети по типу.\n\nНесколько интересных мест для подключения:\n\n- /мультивселенной — Управляйте своими другими аккаунтами мультивселенной, включая отправку и получение контента.\n\nЭто сообщение было отправлено приватно от тебя самому себе, поэтому для его получения не требовалось соединение.", + welcomePmBody: "Добро пожаловать в Oasis — свободная, одноранговая, федеративная и зашифрованная социальная сеть с распределённой архитектурой только по приглашениям.\n\nНесколько интересных мест, с которых можно начать:\n\n- /profile — Отредактируй аватар, имя, описание и какие модули видны на твоей публичной стороне.\n- /invites — Добавь pub, чтобы федерироваться с остальной сетью.\n- /peers — Посмотри, кто на связи, пересканируй LAN, экспортируй или импортируй списки пиров.\n- /inhabitants — Открывай и заходи к аватарам других людей.\n- /tribes — Создавай или вступай в приватные группы со сквозным шифрованием.\n- /inbox — Читай и отправляй приватные сообщения.\n- /activity — Смотри свежую активность — свою и твоей сети.\n- /publish — Напиши пост или поделись изображением, аудио, видео или документом.\n\nНесколько интересных мест для подключения:\n\n- /fediverse — Управляйте своими другими аккаунтами федиверса, включая отправку и получение контента.\n\nЭто сообщение было отправлено приватно от тебя самому себе, поэтому для его получения не требовалось соединение.", + profileVisibilityTitle: "Видимость публичного профиля", + profileVisibilityHint: "Выберите поля, видимые другим жителям. По умолчанию показана только Карма.", profileSensorsSectionTitle: "Сенсоры", profileVisibilityActivity: "Уровень активности", + profileVisibilityDevice: "Устройство", profileDeviceLockedHint: "Этот датчик всегда включён: он показывает другим, с какого устройства вы подключаетесь, и не может быть отключён.", profileVisibilityKarma: "Карма", profileVisibilityUbi: "UBI", @@ -643,6 +713,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "Ключ GPG", + profileVisibilityClearnet: "Опубликовать мой профиль", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "Магазины", profileClearnetJobsLabel: "Вакансии", @@ -696,6 +767,7 @@ module.exports = { " или состояние подключения.", ], walletSentToLine: ({ destination, amount }) => `Отправлено ECO ${amount} на ${destination}`, + walletSettingsTitle: "Кошелёк", walletSettingsDescription: "Интегрируйте Oasis с вашим кошельком ECOin.", walletSettingsDocLink: "Руководство по установке ECOin", walletStatusMessages: { @@ -717,19 +789,37 @@ module.exports = { cipherTitle: "Шифратор", cipherDescription: "Симметричное шифрование и расшифровка текста (с использованием общего пароля).", randomPassword: "Случайный пароль", + cipherEncryptTitle: "Зашифровать текст", + cipherEncryptDescription: "Введите текст для шифрования", + cipherTextLabel: "Текст для шифрования", cipherTextPlaceholder: "Введите текст для шифрования...", cipherPasswordLabel: "Установите пароль (минимум 32 символа) для шифрования текста", cipherPasswordDecryptLabel: "Установите пароль (минимум 32 символа) для расшифровки текста", cipherPasswordPlaceholder: "Введите пароль...", cipherEncryptButton: "Зашифровать", + cipherDecryptTitle: "Расшифровать текст", + cipherDecryptDescription: "Введите текст для расшифровки", cipherEncryptedMessageLabel: "Зашифрованный текст", cipherDecryptedMessageLabel: "Расшифрованный текст", + cipherPasswordUsedLabel: "Пароль, использованный для шифрования (сохраните его!)", cipherEncryptedTextPlaceholder: "Введите зашифрованный текст...", + cipherIvLabel: "IV", + cipherIvPlaceholder: "Введите вектор инициализации...", cipherDecryptButton: "Расшифровать", password: "Пароль", text: "Текст", encryptedText: "Зашифрованный текст", iv: "Вектор инициализации (IV)", + encryptTitle: "Зашифровать текст", + encryptDescription: "Введите текст для шифрования и укажите пароль.", + encryptButton: "Зашифровать", + decryptTitle: "Расшифровать текст", + decryptDescription: "Введите зашифрованный текст и укажите тот же пароль, что использовался при шифровании.", + decryptButton: "Расшифровать", + passwordLengthError: "Пароль должен содержать минимум 32 символа.", + missingFieldsError: "Текст, пароль или IV не указаны.", + encryptionError: "Ошибка шифрования текста.", + decryptionError: "Ошибка расшифровки текста.", bookmarkTitle: "Закладки", bookmarkDescription: "Находите и управляйте закладками в вашей сети.", bookmarkAllSectionTitle: "Закладки", @@ -755,6 +845,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "Необязательно", bookmarkTagsLabel: "Теги", bookmarkTagsPlaceholder: "Введите теги через запятую", + bookmarkCategoryLabel: "Категория", + bookmarkCategoryPlaceholder: "Необязательно", bookmarkLastVisitLabel: "Последний визит", bookmarkSearchPlaceholder: "Искать URL, теги, категорию, автора...", bookmarkSortRecent: "Новейшие", @@ -769,6 +861,8 @@ module.exports = { noLastVisit: "Нет записи о посещении", videoTitle: "Видео", videoDescription: "Просматривайте и управляйте видеоконтентом в вашей сети.", + videoPluginTitle: "Название", + videoPluginDescription: "Описание", videoMineSectionTitle: "Ваши видео", videoCreateSectionTitle: "Загрузить видео", videoUpdateSectionTitle: "Обновить видео", @@ -800,6 +894,7 @@ module.exports = { videoSortOldest: "Старейшие", videoSortTop: "Больше голосов", videoSearchButton: "Искать", + videoMessageAuthorButton: "ЛС", videoUpdatedAt: "Обновлено", videoNoMatch: "Видео не найдены.", documentTitle: "Документы", @@ -840,6 +935,8 @@ module.exports = { documentUpdatedAt: "Обновлено", audioTitle: "Аудио", audioDescription: "Просматривайте и управляйте аудиоконтентом в вашей сети.", + audioPluginTitle: "Название", + audioPluginDescription: "Описание", audioMineSectionTitle: "Ваши аудио", audioCreateSectionTitle: "Загрузить аудио", audioUpdateSectionTitle: "Обновить аудио", @@ -850,6 +947,7 @@ module.exports = { audioFilterMine: "МОИ", audioFilterRecent: "НЕДАВНИЕ", audioFilterTop: "ЛУЧШИЕ", + audioFilterBlockchain: "БЛОКЧЕЙН", audioCreateButton: "Загрузить аудио", audioUpdateButton: "Обновить", audioDeleteButton: "Удалить", @@ -876,7 +974,6 @@ module.exports = { audioFilterFavorites: "ИЗБРАННОЕ", favoritesTitle: "Избранное", favoritesDescription: "Весь ваш избранный контент в одном месте.", - favoritesSearchPlaceholder: "Поиск в избранном...", favoritesFilterAll: "ВСЕ", favoritesFilterRecent: "НЕДАВНИЕ", favoritesFilterAudios: "АУДИО", @@ -892,13 +989,18 @@ module.exports = { favoritesNoItems: "Пока нет избранного.", yourContacts: "Ваши контакты", allInhabitants: "Жители", - allCVs: "Резюме", + allCVs: "Все резюме", discoverPeople: "Находите жителей в вашей сети.", + allInhabitantsButton: "ВСЕ", + contactsButton: "ПОДДЕРЖКА", + CVsButton: "Резюме", + matchSkills: "Совпадение навыков", + matchSkillsButton: "СОВПАДЕНИЕ НАВЫКОВ", + suggestedButton: "РЕКОМЕНДОВАННЫЕ", searchInhabitantsPlaceholder: "ФИЛЬТР жителей ПО ИМЕНИ...", filterLocation: "ФИЛЬТР жителей ПО МЕСТОПОЛОЖЕНИЮ...", filterLanguage: "ФИЛЬТР жителей ПО ЯЗЫКУ...", filterSkills: "ФИЛЬТР жителей ПО НАВЫКАМ...", - cvSkillsCount: "Навыки", applyFilters: "Применить фильтры", locationLabel: "Местоположение", languagesLabel: "Языки", @@ -907,18 +1009,17 @@ module.exports = { mutualFollowers: "Общие подписчики", suggestedFollowsYou: "Подписан на тебя", latestInteractions: "Последние взаимодействия", + viewAvatar: "Смотреть аватар", + viewCV: "Смотреть резюме", suggestedSectionTitle: "Рекомендованные", topkarmaSectionTitle: "Лучшая Карма", topecoSectionTitle: "Лучшие Эко", topactivitySectionTitle: "Наибольшая активность", - "TOP INACTIVITYButton": "НЕАКТИВНЫЕ", - topinactivitySectionTitle: "Наименее активные жители", blockedSectionTitle: "Заблокированные", gallerySectionTitle: "ГАЛЕРЕЯ", - topSectionTitle: "ТОП", + blockedButton: "ЗАБЛОКИРОВАННЫЕ", blockedLabel: "Заблокированный пользователь", inhabitantviewDetails: "Подробнее", - cvVisitButton: "Открыть резюме", keepReading: "Продолжить чтение...", oasisId: "ID", noInhabitantsFound: "Жители пока не найдены.", @@ -928,9 +1029,8 @@ module.exports = { parliamentDescription: "Изучайте формы правления и коллективные законы управления.", parliamentFilterGovernment: "ПРАВИТЕЛЬСТВО", parliamentFilterCandidatures: "КАНДИДАТУРЫ", - parliamentFilterProposals: "ПРЕДЛОЖЕНИЯ", + parliamentFilterProposals: "ЗАКОНОПРОЕКТЫ", parliamentFilterLaws: "ЗАКОНЫ", - parliamentFilterRevocations: "ОТЗЫВЫ", parliamentFilterHistorical: "ИСТОРИЯ", parliamentFilterLeaders: "ЛИДЕРЫ", parliamentFilterRules: "ПРАВИЛА", @@ -956,8 +1056,10 @@ module.exports = { parliamentPoliciesDeclined: "ОТКЛОНЁННЫЕ ЗАКОНЫ", parliamentPoliciesDiscarded: "ОТМЕНЁННЫЕ ЗАКОНЫ", parliamentEfficiency: "% ЭФФЕКТИВНОСТЬ", + parliamentFilterRevocations: 'ОТЗЫВЫ', parliamentRevocationFormTitle: 'Отозвать закон', parliamentRevocationLaw: 'Закон', + parliamentRevocationTitle: 'Название', parliamentRevocationReasons: 'Причины', parliamentRevocationPublish: 'Опубликовать отзыв', parliamentCurrentRevocationsTitle: 'Текущие отзывы', @@ -970,15 +1072,27 @@ module.exports = { parliamentNoGovernments: "Правительств пока нет.", parliamentCandidatureFormTitle: "Предложить кандидатуру", parliamentCandidatureIdPh: "Oasis ID (@...) или название племени", + parliamentCandidatureSlogan: "Слоган (макс. 140 символов)", + parliamentCandidatureSloganPh: "Краткий девиз", parliamentCandidatureMethod: "Метод", parliamentCandidatureProposeBtn: "Опубликовать кандидатуру", + parliamentThType: "Тип", parliamentThId: "ID", + parliamentThDate: "Дата предложения", + parliamentThSlogan: "Слоган", parliamentThMethod: "Метод", parliamentThKarma: "Карма", + parliamentThSince: "Профиль с", + parliamentThVotes: "Получено голосов", + parliamentThVoteAction: "Голосовать", + parliamentTypeUser: "Житель", + parliamentTypeTribe: "Племя", parliamentVoteBtn: "Голосовать", parliamentProposalFormTitle: "Предложить закон", parliamentProposalDescription: "Описание (до 1000 символов)", parliamentProposalPublish: "Опубликовать законопроект", + parliamentFinalize: "Завершить", + parliamentDeadline: "Крайний срок", parliamentNoProposals: "Законопроектов пока нет.", parliamentNoLaws: "Принятых законов пока нет.", parliamentMethodDEMOCRACY: "Демократия", @@ -993,21 +1107,28 @@ module.exports = { parliamentVotesSlashTotal: "Голоса/Всего", parliamentVotesNeeded: "Необходимо голосов", parliamentFutureLawsTitle: "Будущие законы", + parliamentNoFutureLaws: "Будущих законов пока нет.", parliamentVoteAction: "Голосовать", + parliamentLeadersTitle: "Лидеры", parliamentThLeader: "АВАТАР", parliamentPopulation: "Население", + parliamentThInPower: "У власти", parliamentThPresented: "КАНДИДАТУРЫ", parliamentNoLeaders: "Лидеров пока нет.", parliamentCandidaturesListTitle: "Список кандидатур", parliamentTimeRemaining: "Оставшееся время", + parliamentNoLeader: "Лидирующей кандидатуры пока нет.", parliamentElectionsStatusTitle: "Следующее правительство", parliamentProposalDeadlineLabel: "Крайний срок", parliamentProposalTimeLeft: "Осталось времени", + parliamentProposalOnTrack: "На пути к принятию", + parliamentProposalOffTrack: "Недостаточно поддержки", parliamentRulesTitle: "Как работает Парламент", parliamentRulesIntro: "Выборы проводятся каждые 2 месяца; кандидатуры подаются непрерывно и сбрасываются при выборе правительства.", parliamentRulesCandidates: "Любой житель может выдвинуть себя, другого жителя или любое племя. Каждый житель может предложить до 3 кандидатур за цикл; дубликаты в одном цикле отклоняются.", parliamentRulesElection: "Побеждает кандидатура, набравшая наибольшее количество голосов на момент подведения итогов.", parliamentRulesTies: "Порядок разрешения ничьих: наивысшая karma жителя; при равенстве — самый старый профиль; при дальнейшем равенстве — самое раннее предложение; затем лексикографически по ID.", + parliamentRulesFallback: "Если никто не голосует, побеждает последняя предложенная кандидатура. Если кандидатур нет, выбирается случайное племя.", parliamentRulesTerm: "На вкладке «Правительство» отображается текущее правительство и статистика.", parliamentRulesMethods: "Формы: Анархия (простое большинство), Демократия (50%+1), Большинство (80%), Меньшинство (20%), Карматократия (законопроекты с наивысшей karma), Диктатура (мгновенное утверждение).", parliamentRulesAnarchy: "Анархия — режим по умолчанию: если ни одна кандидатура не избрана на момент подведения итогов, провозглашается Анархия. При Анархии любой житель может предлагать законы.", @@ -1029,7 +1150,11 @@ module.exports = { courtsCaseFormTitle: "Открыть дело", courtsCaseRespondent: "Обвиняемый / Ответчик", courtsCaseRespondentPh: "Oasis ID (@...) или название племени", + courtsCaseMediatorsAccuser: "Посредники (обвинитель)", courtsCaseMethod: "Метод разрешения", + courtsCaseDescription: "Описание (макс. 1000 символов)", + courtsCaseEvidenceTitle: "Доказательства по делу", + courtsCaseEvidenceHelp: "Прикрепите изображения, аудио, документы (PDF) или видео, подтверждающие вашу позицию.", courtsCaseSubmit: "Подать дело", courtsNominateJudge: "Выдвинуть судью", courtsJudgeId: "Судья", @@ -1074,6 +1199,22 @@ module.exports = { courtsRulesMisconduct: "Преследование, манипуляции или фабрикация доказательств могут привести к немедленному негативному решению.", courtsRulesGlossary: "Дело: запись о конфликте. Доказательство: материалы, подтверждающие утверждения. Вердикт: решение с мерой пресечения. Апелляция: запрос на пересмотр вердикта.", courtsFilterActions: "ДЕЙСТВИЯ", + courtsNoActions: "Нет ожидающих действий для вашей роли.", + courtsCaseTitlePlaceholder: "Краткое описание конфликта", + courtsCaseSeverity: "Серьёзность", + courtsCaseSeverityNone: "Без метки серьёзности", + courtsCaseSeverityLOW: "Низкая", + courtsCaseSeverityMEDIUM: "Средняя", + courtsCaseSeverityHIGH: "Высокая", + courtsCaseSeverityCRITICAL: "Критическая", + courtsCaseSubject: "Тема", + courtsCaseSubjectNone: "Без метки темы", + courtsCaseSubjectBEHAVIOUR: "Поведение", + courtsCaseSubjectCONTENT: "Контент", + courtsCaseSubjectGOVERNANCE: "Управление / правила", + courtsCaseSubjectFINANCIAL: "Финансы / ресурсы", + courtsCaseSubjectOTHER: "Другое", + courtsHiddenRespondent: "Скрыто (видно только участникам дела).", courtsThRole: "Роль", courtsRoleAccuser: "Обвинитель", courtsRoleDefence: "Защита", @@ -1084,19 +1225,52 @@ module.exports = { courtsAssignJudgeBtn: "Выбрать судью", trendingTitle: "В тренде", exploreTrending: "Изучайте самый популярный контент в вашей сети.", + RECENTButton: "НЕДАВНИЕ", + bookmarkButton: "ЗАКЛАДКИ", + transferButton: "ПЕРЕВОДЫ", + eventButton: "СОБЫТИЯ", + taskButton: "ЗАДАЧИ", votesButton: "ГОЛОСОВАНИЯ", - shopProductButton: "МАГАЗИН", + reportButton: "ОТЧЁТЫ", + feedButton: "ЛЕНТА", + marketButton: "РЫНОК", + imageButton: "ИЗОБРАЖЕНИЯ", + audioButton: "АУДИО", + videoButton: "ВИДЕО", + documentButton: "ДОКУМЕНТЫ", + torrentButton: "ТОРРЕНТЫ", + noTrendingFound: "Трендовый контент не найден.", + noContentMessage: "Трендового контента пока нет.", + trendingDescription: "Описание", + trendingDate: "Дата", + trendingLocation: "Местоположение", + trendingPrice: "Цена", + trendingUrl: "URL", + trendingCategory: "Категория", + trendingStart: "Начало", + trendingEnd: "Конец", + trendingPriority: "Приоритет", + trendingStatus: "Статус", + trendingFrom: "От", + trendingTo: "Кому", + trendingConcept: "Концепция", + trendingAmount: "Сумма", + trendingDeadline: "Крайний срок", + trendingItemStatus: "Статус элемента", + trendingTotalVotes: "Всего голосов", trendingTotalOpinions: "Всего мнений", - trendingNoContentMessage: "Трендов пока нет.", + trendingNoContentMessage: "Трендового контента пока нет.", + trendingAuthor: "Автор", + trendingCreatedAtLabel: "Создано", trendingTotalCount: "Всего", tasksTitle: "Задачи", tasksDescription: "Находите и управляйте задачами в вашей сети.", - taskSearchPlaceholder: "Поиск задач...", taskTitleLabel: "Название", taskDescriptionLabel: "Описание", taskStartTimeLabel: "Время начала", taskEndTimeLabel: "Время окончания", taskPriorityLabel: "Приоритет", + taskPrioritySelect: "Выберите приоритет", taskPriorityUrgent: "Срочный", taskPriorityHigh: "Высокий", taskPriorityMedium: "Средний", @@ -1106,13 +1280,13 @@ module.exports = { taskVisibilityLabel: "Видимость", taskPublic: "публичная", taskPrivate: "приватная", + taskCreatedAt: "Создано", + taskBy: "Автор", taskStatus: "Статус", taskStatusOpen: "Открыта", taskStatusInProgress: "В работе", taskStatusClosed: "Закрыта", taskAssignedTo: "Назначено", - taskAssignedChip: "Назначено", - taskUnassignedChip: "Не назначено", taskAssignees: "Исполнители", taskAssignButton: "Назначить себе", taskUnassignButton: "Снять назначение", @@ -1125,6 +1299,7 @@ module.exports = { taskFilterInProgress: "В РАБОТЕ", taskFilterClosed: "ЗАКРЫТЫЕ", taskFilterAssigned: "НАЗНАЧЕННЫЕ", + taskFilterArchived: "АРХИВ", taskFilterUrgent: "СРОЧНЫЕ", taskFilterHigh: "ВЫСОКИЙ", taskFilterMedium: "СРЕДНИЙ", @@ -1137,21 +1312,23 @@ module.exports = { taskInProgressTitle: "Задачи в работе", taskClosedTitle: "Закрытые задачи", taskAssignedTitle: "Назначенные задачи", + taskArchivedTitle: "Архивные задачи", + taskPublicTitle: "Публичные задачи", + taskPrivateTitle: "Приватные задачи", notasks: "Нет доступных задач.", noLocation: "Местоположение не указано", taskSetStatus: "Установить статус", + eventTitle: "Событие", eventDateLabel: "Дата", - eventRecurrenceLabel: "Повторение", - eventRecurrenceUntil: "Повторять до", - eventRecurrenceHint: "Оставьте дату окончания пустой для разового события.", - eventUpcomingDates: "Ближайшие даты", eventsTitle: "События", eventsDescription: "Находите и управляйте событиями в вашей сети.", - eventSearchPlaceholder: "Поиск событий...", + eventDescription: "Описание", eventPrice: "Цена", eventStatus: "Статус", + eventOrganizer: "Организатор", eventAllSectionTitle: "События", eventMineSectionTitle: "Ваши события", + eventArchivedTitle: "Архивные события", eventCreateSectionTitle: "Создать событие", eventUpdateSectionTitle: "Обновить событие", eventDeleteButton: "Удалить", @@ -1159,26 +1336,11 @@ module.exports = { eventAddToCalendar: "Добавить в календарь", eventVisitCalendar: "Открыть календарь", viewTask: "Открыть задачу", - viewEvent: "Открыть событие", - viewPad: "Открыть пад", - viewMap: "Открыть карту", - viewCalendar: "Открыть календарь", viewReport: "Открыть отчёт", viewTransfer: "Открыть перевод", viewItem: "Открыть объект", viewShop: "Открыть магазин", viewProject: "Открыть проект", - viewJob: "Смотреть Вакансию", - viewPlace: "Смотреть Место", - generatePdf: "Создать PDF", - sharePm: "Отправить в ЛС", - galleryImages: "Изображения (макс. 50 МБ каждое)", - galleryPhotos: "Изображения", - galleryAddPhoto: "Добавить изображение", - galleryRemovePhoto: "Удалить", - galleryVideo: "Видео (макс. 50 МБ)", - galleryAddVideo: "Добавить видео", - galleryRemoveVideo: "Удалить видео", viewVotation: "Открыть голосование", voteCastTitle: "Проголосовать", voteResults: "Результаты", @@ -1186,15 +1348,31 @@ module.exports = { eventDescriptionLabel: "Описание", eventDescriptionPlaceholder: "Введите описание события...", eventUpdateButton: "Обновить", + eventAttendeesLabel: "Участники", + eventAttendeesPlaceholder: "Введите участников через запятую...", eventTagsLabel: "Теги", + eventTagsPlaceholder: "Введите теги через запятую...", + eventTags: "Теги", eventPriceLabel: "Цена", eventUrlLabel: "URL", eventAttendees: "Участники", + noAttendees: "Участников пока нет", + eventCreatedAt: "Создано", eventLocation: "Местоположение", eventLocationLabel: "Местоположение", + eventNoLocation: "Местоположение не указано", + eventNoURL: "URL не указан", + eventBy: "Автор", noevents: "Нет доступных событий.", eventDate: "Дата", + eventDateFormat: "ДД:ММ:ГГГГ ЧЧ:мм", eventAttendButton: "Принять участие", + eventUnattendButton: "Отменить участие", + eventCreatedBy: "Создано", + eventAttendeesCount: "Количество участников", + eventCreatedByYou: "Вы создали это событие", + eventAttendConfirmation: "Вы теперь участвуете в этом событии", + eventUnattendConfirmation: "Вы больше не участвуете в этом событии", eventFilterAll: "ВСЕ", eventFilterMine: "МОИ", eventFilterToday: "СЕГОДНЯ", @@ -1202,9 +1380,16 @@ module.exports = { eventFilterMonth: "ЭТОТ МЕСЯЦ", eventFilterYear: "ЭТОТ ГОД", eventFilterArchived: "АРХИВ", + eventTodayTitle: "Сегодняшние события", + eventThisWeekTitle: "События этой недели", + eventThisMonthTitle: "События этого месяца", + eventThisYearTitle: "События этого года", eventPrivacyLabel: "Видимость", eventPublic: "Публичное", eventPrivate: "Приватное", + eventPublicTitle: "Публичные события", + eventNoPrice: "Бесплатное событие", + eventNoImage: "Изображение не загружено", eventAttended: "Участвует", eventUnattended: "Не участвует", eventStatusOpen: "Открыто", @@ -1215,14 +1400,9 @@ module.exports = { tagsTopSectionTitle: "Популярные теги", tagsCloudSectionTitle: "Облако тегов", tagsFilterAll: "ВСЕ", - tagsFilterMine: "МОИ", - tagsFilterRecent: "НОВЫЕ", - tagsMineSectionTitle: "Мои теги", - tagsRecentSectionTitle: "Новые теги", tagsFilterTop: "ЛУЧШИЕ", tagsFilterCloud: "ОБЛАКО", tagsNoItems: "Нет доступных тегов.", - tagsSearchPlaceholder: "Поиск тегов...", tagsTableHeaderTag: "ТЕГ/ССЫЛКА:", tagsTableHeaderCount: "СЧЁТЧИК:", transfersTitle: "Переводы", @@ -1262,12 +1442,18 @@ module.exports = { transfersDeadline: "Крайний срок", transfersTags: "Теги", transfersStatus: "Статус", + transfersStatusUnconfirmed: "НЕПОДТВЕРЖДЁН", + transfersStatusClosed: "ЗАКРЫТ", + transfersStatusDiscarded: "ОТМЕНЁН", + transfersCreatedAt: "Создано", transfersConfirmations: "ПОДТВЕРЖДЕНИЯ", - transfersExportContract: "Создать Счёт", + transfersContainingBlock: "Содержащий блок", + transfersExportContract: "Создать контракт", transfersConfirmButton: "Подтвердить перевод", transfersNoItems: "Переводы не найдены.", transfersMineSectionTitle: "Ваши переводы", transfersUBISectionTitle: "UBI переводы", + transfersMarketSectionTitle: "Рыночные переводы", transfersTopSectionTitle: "Лучшие переводы", transfersPendingSectionTitle: "Ожидающие переводы", transfersUnconfirmedSectionTitle: "Неподтверждённые переводы", @@ -1275,13 +1461,21 @@ module.exports = { transfersDiscardedSectionTitle: "Отменённые переводы", transfersCreateSectionTitle: "Создать перевод", transfersAllSectionTitle: "Переводы", + transfersFilterFavs: "Избранное", + transfersFavsSectionTitle: "Избранные переводы", + transfersSearchLabel: "Поиск", transfersSearchPlaceholder: "Искать концепцию, теги, пользователей...", transfersMinAmountLabel: "Мин. сумма", transfersMaxAmountLabel: "Макс. сумма", + transfersSortLabel: "Сортировать по", transfersSortRecent: "Новейшие", transfersSortAmount: "Наибольшая сумма", transfersSortDeadline: "Ближайший срок", transfersSearchButton: "Искать", + transfersFavoriteButton: "В избранное", + transfersUnfavoriteButton: "Из избранного", + transfersMessageUserButton: "Написать", + transfersExpiringSoonBadge: "ИСТЕКАЕТ", transfersExpiredBadge: "ИСТЁК", transfersUpdatedAt: "Обновлено", transfersNoMatch: "Переводы не найдены по вашему запросу.", @@ -1294,9 +1488,6 @@ module.exports = { voteClosedTitle: "Закрытые голосования", voteAllSectionTitle: "Голосования", voteCreateButton: "Создать голосование", - voteErrorTitle: "Проверьте форму", - voteErrorDeadlineMin: "Срок должен быть не менее {days} дней от текущего момента.", - voteErrorGeneric: "Не удалось сохранить голосование.", voteUpdateButton: "Обновить", voteDeleteButton: "Удалить", voteOptionYes: "ЗА", @@ -1326,6 +1517,16 @@ module.exports = { voteNoQuestion: "Вопрос не указан", voteUnknownCreator: "Неизвестный автор", voteUnknownDate: "Неизвестная дата", + errorVoteNotFound: "Голосование не найдено", + errorAlreadyVoted: "Вы уже высказали мнение.", + errorVoteClosedCannotEdit: "Нельзя редактировать закрытое голосование", + errorVoteDeadlinePassed: "Срок этого голосования истёк", + errorRetrievingVote: "Ошибка получения голосования", + errorCreatingVote: "Ошибка создания голосования", + errorVoteAlreadyVoted: "Нельзя редактировать после голосования", + errorDeletingOldVote: "Ошибка удаления старого мнения", + errorCreatingUpdatedVote: "Ошибка создания обновлённого мнения", + errorCreatingTombstone: "Ошибка создания записи об удалении", voteDetailSectionTitle: 'Детали голосования', voteCommentsLabel: 'Комментарии', voteCommentsForumButton: 'Открыть обсуждение', @@ -1335,6 +1536,7 @@ module.exports = { voteNewCommentButton: 'Отправить комментарий', voteNewCommentLabel: 'Добавить комментарий', cvTitle: "Резюме", + cvLabel: "Curriculum Vitae (CV)", cvEditSectionTitle: "Редактировать резюме", cvCreateSectionTitle: "Создать резюме", cvDescription: "Управляйте и делитесь профессиональными навыками и информацией.", @@ -1353,121 +1555,29 @@ module.exports = { cvLocationLabel: "Местоположение", cvStatusLabel: "Статус", cvPreferencesLabel: "Предпочтения", + cvOasisContributorLabel: "Участник Oasis", cvPersonal: "Личное", cvOasis: "Участник Oasis (необязательно)", - cvOasisContributorView: "Вклад", + cvOasisContributorView: "Вклад в Oasis", cvEducational: "Образование (необязательно)", cvEducationalView: "Образование", cvProfessional: "Профессиональное (необязательно)", cvProfessionalView: "Профессиональное", cvAvailability: "Доступность (необязательно)", + cvAvailabilityView: "Доступность", cvUpdateButton: "Обновить", cvCreateButton: "Создать резюме", + cvContactLabel: "Контакт", cvCreatedAt: "Создано", cvUpdatedAt: "Обновлено", cvEditButton: "Обновить", cvDeleteButton: "Удалить", cvNoCV: "Резюме не найдено.", blogSubject: "Тема", - blogMessage: "Сообщение", + blogMessage: "Текст ограничен 8096 символами.", blogImage: "Загрузить медиа (макс: 50MB)", - blogMedia: "Вложения (до 8 изображений и одно видео)", blogPublish: "Предварительный просмотр", - pollsTitle: "Опросы", - dataTitle: "Совпадения", - dataDescription: "Исследуйте и визуализируйте совпадения в вашей сети.", - dataFilterAll: "ВСЁ", - dataFilterMine: "МОИ", - dataFilterRecent: "НЕДАВНИЕ", - dataFilterTop: "ТОП", - dataKindInhabitants: "ЖИТЕЛИ", - dataKindJobs: "ВАКАНСИИ", - dataKindProjects: "ПРОЕКТЫ", - dataKindEvents: "СОБЫТИЯ", - dataKindTribes: "ПЛЕМЕНА", - dataKindMarket: "РЫНОК", - dataKindHousing: "ЖИЛЬЁ", - dataKindIndustry: "ПРОИЗВОДСТВО", - dataKindTasks: "ЗАДАЧИ", - dataKindReports: "ОТЧЁТЫ", - dataKindVotes: "ГОЛОСОВАНИЯ", - dataSearchPlaceholder: "Поиск по перекрёстным данным...", - dataCohesionTitle: "Коэффициент сплочённости (CC)", - dataCcShort: "CC", - dataCohesionHint: "Средняя близость между всем, что опубликовано в сети.", - dataAffinity: "Сходство", - dataCommonTerms: "Ключевое слово", - dataStatPeople: "Резюме", - dataStatConnected: "Связаны", - dataStatSkills: "Навыки", - dataBestMatch: "Лучшее совпадение", - dataStatIsolated: "Изолированы", - dataStatEntities: "Сравнённые сущности", - dataStatTerms: "Различных терминов", - dataStatPairs: "Связанные пары", - dataMatchesTitle: "Совпадения", - dataMatchesHint: "Персонализированные предложения алгоритма.", - dataTermsTitle: "Ключевые слова", - dataTermsHint: "Термины, встречающиеся в наибольшем числе материалов.", - dataConnections: "Связанные совпадения", - dataTopicTitle: "Всё, что связано с", - dataTopicHint: "Что алгоритм связывает с этой темой", - dataNoMatches: "Нет перекрёстных данных.", - dataNoProfile: "Публикуйте контент или заполните CV, чтобы алгоритм узнал, что вам интересно.", - pollsDescription: "Модуль, чтобы спросить сеть и подсчитать ответы.", - pollFilterAll: "ВСЕ", - pollFilterMine: "МОИ", - pollFilterRecent: "НЕДАВНИЕ", - pollFilterTop: "ТОП", - pollFilterVoted: "ПРОГОЛОСОВАЛ", - pollFilterOpen: "ОТКРЫТЫЕ", - pollFilterClosed: "ЗАКРЫТЫЕ", - pollCreateButton: "Создать опрос", - pollCreateTitle: "Создать опрос", - pollEditTitle: "Изменить опрос", - pollQuestion: "Вопрос", - pollQuestionPlaceholder: "Что вы хотите спросить?", - pollOptions: "Варианты (по одному в строке)", - pollOutcome: "Результат", - pollNoVotesYet: "Голосов пока нет", - pollTie: "Ничья", - pollOptionsPlaceholder: "Площадь\nПарк\nБар", - pollAnonymous: "Анонимный", - pollAnonymousLabel: "Анонимный опрос", - pollMultiple: "Множественный выбор", - pollMultipleLabel: "Разрешить несколько ответов", - pollDeadline: "Срок", - pollTags: "Метки", - pollPublishButton: "Опубликовать опрос", - pollUpdateButton: "Обновить", - pollCloseButton: "Закрыть", - pollDeleteButton: "Удалить", - pollVoteButton: "Голосовать", - pollChangeVote: "Изменить голос", - pollVoters: "Проголосовали", - pollComments: "Комментарии", - pollStatusLabel: "Статус", - pollStatusOpen: "ОТКРЫТ", - pollStatusClosed: "ЗАКРЫТ", - pollSearchPlaceholder: "Поиск опросов...", - pollsNoItems: "Опросы не найдены.", - viewPoll: "Смотреть опрос", - pollChatCreate: "Создать опрос", - pollInChat: "Опрос", - blogTitle: "Блоги", - blogDescription: "Модуль для поиска и управления блогами.", - blogFilterAll: "ВСЕ", - blogFilterMine: "МОИ", - blogFilterRecent: "НЕДАВНИЕ", - blogFilterFavorites: "ИЗБРАННОЕ", - blogFilterTop: "ТОП", - blogCreateButton: "Создать блог", - blogSearchPlaceholder: "Поиск блогов...", - blogComments: "Комментарии", - blogAllowComments: "Разрешить комментарии", - blogCommentsClosed: "Автор закрыл комментарии к этому блогу.", - blogNoItems: "Блоги не найдены.", - viewBlog: "Смотреть блог", + noPopularMessages: "Популярных сообщений пока нет", forumTitle: "Форумы", forumCategoryLabel: "Категория", forumTitleLabel: "Название", @@ -1475,22 +1585,43 @@ module.exports = { forumCreateButton: "Создать форум", forumCreateSectionTitle: "Создать форум", forumDescription: "Общайтесь открыто с другими жителями вашей сети.", - forumSearchPlaceholder: "Поиск форумов...", forumFilterAll: "ВСЕ", forumFilterMine: "МОИ", forumFilterRecent: "НЕДАВНИЕ", forumFilterTop: "ТОП", + forumMineSectionTitle: "Ваши форумы", + forumRecentSectionTitle: "Последние форумы", + forumAllSectionTitle: "Форумы", forumDeleteButton: "Удалить", forumParticipants: "участники", forumMessages: "сообщения", + forumLastMessage: "Последнее сообщение", forumMessageLabel: "Сообщение", forumMessagePlaceholder: "Напишите сообщение...", forumSendButton: "Отправить", + forumVisitForum: "Посетить форум", noForums: "Форумы не найдены.", - forumVisitButton: "Открыть форум", - forumTopicLabel: "Тема", + forumVisitButton: "Посетить форум", + forumCatGENERAL: "Общее", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", + forumCatPOLITICS: "Политика", + forumCatTECH: "Технологии", + forumCatSCIENCE: "Наука", + forumCatMUSIC: "Музыка", + forumCatART: "Искусство", + forumCatGAMING: "Игры", + forumCatBOOKS: "Книги", + forumCatFILMS: "Фильмы", + forumCatPHILOSOPHY: "Философия", + forumCatSOCIETY: "Общество", + forumCatPRIVACY: "Конфиденциальность", + forumCatCYBERWARFARE: "Кибервойна", + forumCatSURVIVALISM: "Выживание", imageTitle: "Изображения", imageDescription: "Исследуйте и управляйте изображениями в вашей сети.", + imagePluginTitle: "Название", + imagePluginDescription: "Описание", imageMineSectionTitle: "Ваши изображения", imageCreateSectionTitle: "Загрузить изображение", imageUpdateSectionTitle: "Обновить изображение", @@ -1499,12 +1630,14 @@ module.exports = { imageTopSectionTitle: "Топ изображений", imageFavoritesSectionTitle: "Избранное", imageGallerySectionTitle: "Галерея", + imageMemeSectionTitle: "Мемы", imageFilterAll: "ВСЕ", imageFilterMine: "МОИ", imageFilterRecent: "НЕДАВНИЕ", imageFilterTop: "ТОП", imageFilterFavorites: "ИЗБРАННОЕ", imageFilterGallery: "ГАЛЕРЕЯ", + imageFilterMeme: "МЕМЫ", imageCreateButton: "Загрузить изображение", imageUpdateButton: "Обновить", imageDeleteButton: "Удалить", @@ -1517,6 +1650,7 @@ module.exports = { imageTitlePlaceholder: "Необязательно", imageDescriptionLabel: "Описание", imageDescriptionPlaceholder: "Необязательно", + imageMemeLabel: "Отметить как МЕМ", imageNoFile: "Файл изображения не предоставлен", noImages: "Изображений нет.", imageSearchPlaceholder: "Поиск по названию, тегам, описанию, автору...", @@ -1524,6 +1658,7 @@ module.exports = { imageSortOldest: "Самые старые", imageSortTop: "С наибольшим рейтингом", imageSearchButton: "Поиск", + imageMessageAuthorButton: "Сообщение", imageUpdatedAt: "Обновлено", imageNoMatch: "Изображения не найдены.", feedTitle: "Лента", @@ -1534,6 +1669,7 @@ module.exports = { MINEButton: "Ваши посты", TODAYButton: "СЕГОДНЯ", TOPButton: "Топ постов", + CREATEButton: "Создать пост", totalOpinions: "Всего мнений", moreVoted: "Больше голосов", alreadyVoted: "Вы уже высказали мнение.", @@ -1558,12 +1694,20 @@ module.exports = { concept: "Концепция", description: "Описание", meme: "Мем", + activityContact: "Контакт", + activityBy: "Имя", + activityPixelia: "Добавлен новый пиксель", + viewImage: "Просмотр изображения", + playAudio: "Воспроизвести аудио", + playVideo: "Воспроизвести видео", typeRecent: "НЕДАВНИЕ", - typeTop: "ТОП", + errorActivity: "Ошибка получения активности", typePost: "БЛОГИ", + recentButton: "НЕДАВНИЕ", typeTribe: "ПЛЕМЕНА", typeLarp: "L.A.R.P.", typeInhabitants: "ЖИТЕЛИ", + activityProfileUpdated: "обновил профиль", typeAbout: "ЖИТЕЛИ", typeCurriculum: "РЕЗЮМЕ", typeImage: "ИЗОБРАЖЕНИЯ", @@ -1607,6 +1751,8 @@ module.exports = { typeCourtsSettlementAccepted: "Суды · Мировое соглашение принято", typeCourtsNomination: "Суды · Назначение", typeCourtsNominationVote: "Суды · Голосование за назначение", + activitySupport: "Заключён новый альянс", + activityJoin: "Присоединился к новому PUB", question: "Вопрос", deadline: "Срок", status: "Статус", @@ -1620,8 +1766,12 @@ module.exports = { date: "Дата", category: "Категория", attendees: "Участники", + activitySpread: "->", + visitLink: "Перейти по ссылке", + viewDocument: "Просмотр документа", location: "Местоположение", contentWarning: "Тема", + personName: "Имя жителя", bankWalletConnected: "Кошелёк ECOin", bankUbiReceived: "Получен UBI", bankTx: "Транзакция", @@ -1631,6 +1781,7 @@ module.exports = { link: "Ссылка", activityProjectFollow: "%OASIS% теперь %ACTION% этот проект %PROJECT%", activityProjectUnfollow: "%OASIS% теперь %ACTION% этот проект %PROJECT%", + activityProjectPledged: "%OASIS% внёс %ACTION% %AMOUNT% в проект %PROJECT%", following: "СЛЕЖУ", unfollowing: "ОТПИСАН", pledged: "ПОДДЕРЖАЛ", @@ -1677,28 +1828,34 @@ module.exports = { courtsVotesSlashTotal: "ЗА/ВСЕГО", courtsOpenVote: "Открытое голосование", courtsAnswerTitle: "Ответ", + courtsStanceADMIT: "Признаю", + courtsStanceDENY: "Отрицаю", + courtsStancePARTIAL: "Частично", + courtsStanceCOUNTERCLAIM: "Встречный иск", + courtsStanceNEUTRAL: "Нейтрально", courtsVerdictResult: "Результат", courtsVerdictOrders: "Предписания", courtsSettlementText: "Соглашение", courtsSettlementAccepted: "Принято", + courtsSettlementPending: "В ожидании", courtsJudge: "Судья", courtsThSupports: "Поддержки", courtsFilterOpenCase: "Открытое дело", + courtsEvidenceFileLabel: "Файл доказательства (изображение, аудио, видео или PDF)", + courtsCaseMediators: "Медиаторы", courtsCaseMediatorsPh: "Oasis ID медиаторов через запятую", courtsMediatorsLabel: "Медиаторы", courtsThCase: "Дело", courtsThCreatedAt: "Дата начала", + courtsThActions: "Действия", courtsPublicPrefLabel: "Видимость после решения", courtsPublicPrefYes: "Согласен, что это дело может быть полностью публичным", courtsPublicPrefNo: "Предпочитаю сохранить детали в тайне", courtsPublicPrefSubmit: "Сохранить настройку видимости", courtsNoCases: "Дел нет.", reportsTitle: "Отчёты", - reportsDescription: "Управляйте отчётами вашей сети и следите за ними.", - reportsSearchPlaceholder: "Поиск отчётов...", + reportsDescription: "Управляйте и отслеживайте отчёты, связанные с проблемами, ошибками, злоупотреблениями и предупреждениями в вашей сети.", reportsFilterAll: "ВСЕ", - reportsFilterRecent: "НОВЫЕ", - reportsFilterTop: "ТОП", reportsFilterMine: "МОИ", reportsFilterFeatures: "ФУНКЦИИ", reportsFilterBugs: "ОШИБКИ", @@ -1711,13 +1868,19 @@ module.exports = { reportsFilterInvalid: "НЕДЕЙСТВИТЕЛЬНЫЕ", reportsCreateButton: "Создать отчёт", reportsTitleLabel: "Название", + reportsDescriptionLabel: "Описание", + reportsDescriptionPlaceholder: "Пожалуйста, предоставьте подробное описание.", reportsCategory: "Категория", + reportsCategoryLabel: "Категория", reportsCategoryFeatures: "Функции", reportsCategoryBugs: "Ошибки", reportsCategoryAbuse: "Злоупотребления", - reportsCategoryContent: "Контент", + reportsCategoryContent: "Проблемы с контентом", reportsUpdateButton: "Обновить", reportsDeleteButton: "Удалить", + reportsDateLabel: "Дата", + reportsUploadFile: "Загрузить медиа (макс: 50MB)", + reportsCreatedBy: "Автор", reportsMineSectionTitle: "Ваши отчёты", reportsFeaturesSectionTitle: "Запросы функций", reportsBugsSectionTitle: "Ошибки", @@ -1725,6 +1888,10 @@ module.exports = { reportsContentSectionTitle: "Проблемы с контентом", reportsAllSectionTitle: "Отчёты", reportsNoItems: "Отчётов нет.", + reportsValidationTitle: "Введите допустимое название.", + reportsValidationDescription: "Описание не может быть пустым.", + reportsValidationCategory: "Выберите категорию.", + reportsCreatedAt: "Создано", reportsSeverity: "Серьёзность", reportsSeverityLow: "Низкая", reportsSeverityMedium: "Средняя", @@ -1735,10 +1902,13 @@ module.exports = { reportsStatusUnderReview: "На рассмотрении", reportsStatusResolved: "Решён", reportsStatusInvalid: "Недействительный", + reportsUpdateStatusButton: "Обновить статус", + reportsAnonymityOption: "Отправить анонимно", + reportsAnonymousAuthor: "Анонимный", reportsConfirmButton: "ПОДТВЕРДИТЬ ОТЧЁТ!", reportsConfirmations: "Подтверждения", reportsConfirmedSectionTitle: "Подтверждённые отчёты", - reportsCreateTaskButton: "Создать задачу", + reportsCreateTaskButton: "СОЗДАТЬ ЗАДАЧУ", reportsOpenSectionTitle: "Открытые отчёты", reportsUnderReviewSectionTitle: "Отчёты на рассмотрении", reportsResolvedSectionTitle: "Решённые отчёты", @@ -1782,20 +1952,6 @@ module.exports = { reportsRequestedActionLabel: "Запрашиваемое действие", reportsRequestedActionPlaceholder: "Удалить, скрыть, пометить, предупредить и т.д.", tribesTitle: "Племена", - tribeFilterAll: "ВСЕ", - tribeFilterRecent: "НЕДАВНИЕ", - tribeFilterMine: "МОИ", - tribeFilterMembership: "ЧЛЕНСТВО", - tribeFilterSubtribes: "ПОДПЛЕМЕНА", - tribeFilterTop: "ТОП", - tribeFilterGallery: "ГАЛЕРЕЯ", - tribeFeedFilterTOP: "ТОП", - tribeFeedFilterMINE: "МОИ", - tribeFeedFilterALL: "ВСЕ", - tribeFeedFilterRECENT: "НЕДАВНИЕ", - courtsStanceDENY: "Отрицать", - courtsStanceADMIT: "Признать", - courtsStancePARTIAL: "Признать частично", tribeAllSectionTitle: "Племена", tribeMineSectionTitle: "Ваши племена", tribeCreateSectionTitle: "Создать племя", @@ -1807,6 +1963,13 @@ module.exports = { tribeviewSubTribeButton: "Посетить под-племя", tribeRootLabel: "КОРЕНЬ", tribeDescription: "Исследуйте или создавайте племена в вашей сети.", + tribeFilterAll: "ВСЕ", + tribeFilterMine: "МОИ", + tribeFilterMembership: "ЧЛЕНСТВО", + tribeFilterRecent: "НЕДАВНИЕ", + tribeFilterTop: "ТОП", + tribeFilterSubtribes: "ПОДПЛЕМЕНА", + tribeFilterGallery: "ГАЛЕРЕЯ", tribeMainTribeLabel: "ГЛАВНОЕ ПЛЕМЯ", tribeCreateButton: "Создать племя", tribeUpdateButton: "Обновить", @@ -1825,8 +1988,13 @@ module.exports = { tribeModeLabel: "РЕЖИМ", tribeIsAnonymousLabel: "СТАТУС", tribeMembersCount: "Участники", + tribeInviteCodePlaceholder: "Введите код приглашения", + tribeJoinByCodeButton: "Войти с кодом", + tribeJoinButton: "ВСТУПИТЬ", tribeEnterInvite: "ВВЕСТИ КОД", tribeLeaveButton: "ПОКИНУТЬ", + tribeYes: "ДА", + tribeNo: "НЕТ", tribePublic: "ПУБЛИЧНОЕ", tribePrivate: "ЧАСТНОЕ", tribeGenerateInvite: "РАЗОВОЕ ПРИГЛАШЕНИЕ", @@ -1835,8 +2003,13 @@ module.exports = { tribeRemoveInvitation: "УДАЛИТЬ ПРИГЛАШЕНИЕ", tribeCreatedAt: "Создано", tribeAuthor: "Автор", + tribeAuthorLabel: "АВТОР", tribeStrict: "Строгое", tribeOpen: "Открытое", + tribeFeedFilterRECENT: "НЕДАВНИЕ", + tribeFeedFilterMINE: "МОИ", + tribeFeedFilterALL: "ВСЕ", + tribeFeedFilterTOP: "ТОП", tribeFeedRefeeds: "Переопубликации", tribeFeedRefeed: "Переопубликовать", tribeFeedMessagePlaceholder: "Написать пост…", @@ -1846,12 +2019,17 @@ module.exports = { tribeNotFound: "Племя не найдено!", createTribeTitle: "Создать племя", updateTribeTitle: "Обновить племя", + tribeSectionOverview: "Обзор", tribeSectionInhabitants: "Жители", tribeSectionVotations: "ГОЛОСОВАНИЯ", tribeSectionEvents: "СОБЫТИЯ", + tribeSectionReports: "Отчёты", tribeSectionTasks: "ЗАДАЧИ", tribeSectionFeed: "ЛЕНТА", tribeSectionForum: "ФОРУМ", + tribeSectionMarket: "Рынок", + tribeSectionJobs: "Вакансии", + tribeSectionProjects: "Проекты", tribeSectionMedia: "Медиа", tribeSectionImages: "ИЗОБРАЖЕНИЯ", tribeSectionAudios: "АУДИО", @@ -1889,6 +2067,11 @@ module.exports = { tribeTaskStatusClosed: "ЗАКРЫТЬ", tribeTaskAssign: "НАЗНАЧИТЬ", tribeTaskUnassign: "СНЯТЬ НАЗНАЧЕНИЕ", + tribeReportCreate: "Создать отчёт", + tribeReportsEmpty: "Отчётов пока нет.", + tribeReportTitle: "Название", + tribeReportDescription: "Описание", + tribeReportCategory: "Категория", tribeVotationCreate: "Создать голосование", tribeVotationsEmpty: "Голосований пока нет.", tribeVotationTitle: "Название", @@ -1906,6 +2089,27 @@ module.exports = { tribeForumCategory: "Категория", tribeForumReply: "Ответить", tribeForumReplies: "Ответы", + tribeMarketCreate: "Создать объявление", + tribeMarketEmpty: "Объявлений пока нет.", + tribeMarketTitle: "Название", + tribeMarketDescription: "Описание", + tribeMarketPrice: "Цена", + tribeMarketImage: "Изображение", + tribeMarketCategory: "Категория", + tribeJobCreate: "Создать вакансию", + tribeJobsEmpty: "Вакансий пока нет.", + tribeJobTitle: "Название", + tribeJobDescription: "Описание", + tribeJobLocation: "Местоположение", + tribeJobSalary: "Зарплата", + tribeJobDeadline: "Срок", + tribeProjectCreate: "Создать проект", + tribeProjectsEmpty: "Проектов пока нет.", + tribeProjectTitle: "Название", + tribeProjectDescription: "Описание", + tribeProjectGoal: "Цель", + tribeProjectFunded: "Финансировано", + tribeProjectDeadline: "Срок", tribeMediaUpload: "Загрузить медиа", readDocument: "Читать документ", tribeCreateImage: "Создать изображение", @@ -1916,6 +2120,7 @@ module.exports = { tribeMediaEmpty: "Медиа пока нет.", tribeMediaTitle: "Название", tribeMediaDescription: "Описание", + tribeMediaType: "Тип", tribeMediaTypeImage: "Изображение", tribeMediaTypeVideo: "Видео", tribeMediaTypeAudio: "Аудио", @@ -1925,6 +2130,11 @@ module.exports = { tribeInviteCodeText: "Код приглашения: ", oasisVersionLabel: "Версия Oasis", tribeInviteCodeHint: "Поделитесь этим кодом с тем, кого хотите пригласить. Он может присоединиться через /invites → Tribes.", + tribeGroupTribe: "Племя", + tribeGroupOffice: "Офис", + tribeGroupNetwork: "Сеть", + tribeGroupEconomy: "Экономика", + tribeGroupMedia: "Медиа", tribeStatusOpen: "ОТКРЫТОЕ", tribeStatusClosed: "ЗАКРЫТОЕ", tribeStatusInProgress: "В ПРОЦЕССЕ", @@ -1934,17 +2144,33 @@ module.exports = { tribePriorityCritical: "КРИТИЧЕСКИЙ", tribeTaskFilterAll: "ВСЕ", tribeMediaFilterAll: "ВСЕ", + tribeReportCatBug: "ОШИБКА", + tribeReportCatAbuse: "ЗЛОУПОТРЕБЛЕНИЕ", + tribeReportCatContent: "КОНТЕНТ", + tribeReportCatOther: "ДРУГОЕ", tribeForumCatGeneral: "ОБЩЕЕ", tribeForumCatProposal: "ПРЕДЛОЖЕНИЕ", tribeForumCatQuestion: "ВОПРОС", tribeForumCatAnnouncement: "ОБЪЯВЛЕНИЕ", + tribeMarketCatGoods: "ТОВАРЫ", + tribeMarketCatServices: "УСЛУГИ", + tribeMarketCatFood: "ЕДА", + tribeMarketCatOther: "ДРУГОЕ", tribeStatusLabel: "Статус", tribeSubTribes: "ПОДПЛЕМЕНА", tribeSubTribesCreate: "Создать подплемя", tribeSubTribesStrictDenied: "Строгий режим племени не позволяет создавать новые подплемена. Пожалуйста, свяжитесь с администратором.", + tribeSubTribesEmpty: "Подплемён пока не создано.", + tribeActivityJoined: "ВСТУПИЛ", + tribeActivityLeft: "ПОКИНУЛ", + tribeActivityFeed: "ЛЕНТА", tribeActivityRefeed: "ПЕРЕОПУБЛИКОВАЛ", + tribeGroupAnalytics: "Аналитика", + tribeGroupCreative: "Творчество", tribeSectionActivity: "АКТИВНОСТЬ", tribeSectionTrending: "ПОПУЛЯРНОЕ", + tribeSectionOpinions: "МНЕНИЯ", + tribeSectionPixelia: "PIXELIA", tribeSectionTags: "ТЕГИ", tribeSectionSearch: "ПОИСК", tribeActivityEmpty: "Активности пока нет.", @@ -1956,15 +2182,25 @@ module.exports = { tribeTrendingPeriodWeek: "Эта неделя", tribeTrendingPeriodAll: "За всё время", tribeTrendingEngagement: "вовлечённость", + tribeOpinionsEmpty: "Мнений пока нет.", + tribeOpinionsCast: "Голосовать", + tribeOpinionsRankings: "Рейтинги", + tribeOpinionsAlreadyVoted: "Уже проголосовали", tribeTopCategory: "Больше голосов", + tribePixeliaTitle: "Pixelia", + tribePixeliaDescription: "Совместный пиксель-арт.", + tribePixeliaPaint: "Рисовать", + tribePixeliaContributors: "участников", + tribePixeliaTotalPixels: "пикселей нарисовано", tribeTagsEmpty: "Тегов не найдено.", + tribeTagsCloud: "Облако тегов", + tribeTagsContentWith: "Контент с тегом", tribeSearchPlaceholder: "Поиск контента племени...", tribeSearchEmpty: "Результаты не найдены.", tribeSearchResults: "Результаты", tribeSearchMinChars: "Введите не менее 2 символов для поиска.", agendaTitle: "Повестка дня", agendaDescription: "Здесь вы можете найти все назначенные вам элементы.", - agendaSearchPlaceholder: "Поиск в повестке...", agendaFilterAll: "ВСЕ", agendaFilterToday: "СЕГОДНЯ", agendaFilterUpcoming: "БЛИЖАЙШИЕ", @@ -1978,14 +2214,23 @@ module.exports = { agendaFilterReports: "ОТЧЁТЫ", agendaFilterTransfers: "ПЕРЕВОДЫ", agendaFilterJobs: "ВАКАНСИИ", - agendaFilterIndustry: "ПРОМЫШЛЕННОСТЬ", - agendaFilterHousing: "ЖИЛЬЁ", agendaFilterProjects: "ПРОЕКТЫ", agendaFilterCalendars: "КАЛЕНДАРИ", agendaNoItems: "Назначений не найдено.", + agendaAuthor: "Автор", agendaDiscardButton: "Отклонить", agendaRestoreButton: "Восстановить", + agendaCreatedAt: "Создано", + agendaTitleLabel: "Название", agendaMembersCount: "Участники", + agendaDescriptionLabel: "Описание", + agendaStatus: "Статус", + agendaVisibility: "Видимость", + agendaEventDate: "Дата события", + agendaEventLocation: "Местоположение", + agendaEventPrice: "Цена", + agendaEventUrl: "URL", + agendaTaskStart: "Время начала", agendaLocationLabel: "Местоположение", agendaYes: "ДА", agendaNo: "НЕТ", @@ -1995,6 +2240,11 @@ module.exports = { agendareportCategory: "Категория", agendareportSeverity: "Серьёзность", agendareportStatus: "Статус", + agendareportDescription: "Описание", + agendaTaskEnd: "Время окончания", + agendaTaskPriority: "Приоритет", + agendaTransferFrom: "От", + agendaTransferTo: "Кому", agendaTransferConcept: "Концепция", agendaTransferAmount: "Сумма", agendaTransferDeadline: "Срок", @@ -2002,6 +2252,45 @@ module.exports = { shareYourOpinions: "Открывайте и голосуйте за мнения в вашей сети.", voteNow: "Голосовать", noOpinionsFound: "Мнений не найдено.", + interestingButton: "ИНТЕРЕСНОЕ", + necessaryButton: "НЕОБХОДИМОЕ", + funnyButton: "СМЕШНОЕ", + disgustingButton: "ОТВРАТИТЕЛЬНОЕ", + sensibleButton: "РАЗУМНОЕ", + propagandaButton: "ПРОПАГАНДА", + adultOnlyButton: "ТОЛЬКО ДЛЯ ВЗРОСЛЫХ", + boringButton: "СКУЧНОЕ", + confusingButton: "ЗАПУТАННОЕ", + inspiringButton: "ВДОХНОВЛЯЮЩЕЕ", + spamButton: "СПАМ", + usefulButton: "ПОЛЕЗНОЕ", + informativeButton: "ИНФОРМАТИВНОЕ", + wellResearchedButton: "ХОРОШО ИССЛЕДОВАННОЕ", + accurateButton: "ТОЧНОЕ", + needsSourcesButton: "НУЖНЫ ИСТОЧНИКИ", + wrongButton: "НЕПРАВИЛЬНОЕ", + lowQualityButton: "НИЗКОЕ КАЧЕСТВО", + creativeButton: "ТВОРЧЕСКОЕ", + insightfulButton: "ПРОНИЦАТЕЛЬНОЕ", + actionableButton: "ПРИМЕНИМОЕ", + loveButton: "ЛЮБЛЮ", + clearButton: "ЯСНОЕ", + upliftingButton: "ПОДНИМАЮЩЕЕ НАСТРОЙ", + unnecessaryButton: "НЕНУЖНОЕ", + rejectedButton: "ОТКЛОНЁННОЕ", + misleadingButton: "ВВОДЯЩЕЕ В ЗАБЛУЖДЕНИЕ", + offTopicButton: "НЕ ПО ТЕМЕ", + duplicateButton: "ДУБЛИРОВАНИЕ", + clickbaitButton: "КЛИКБЕЙТ", + trollButton: "ТРОЛЛЬ", + nsfwButton: "NSFW", + violentButton: "НАСИЛИЕ", + toxicButton: "ТОКСИЧНОЕ", + harassmentButton: "ПРЕСЛЕДОВАНИЕ", + hateButton: "НЕНАВИСТЬ", + scamButton: "МОШЕННИЧЕСТВО", + triggeringButton: "ПРОВОКАЦИОННОЕ", + opinionsCreatedAt: "Создано", opinionsTotalCount: "Всего мнений", voteInteresting: "Интересное", voteNecessary: "Необходимое", @@ -2038,60 +2327,40 @@ module.exports = { voteCreative: "Творческое", voteSpam: "Спам", voteAdultOnly: "Только для взрослых", + publishBlog: "Опубликовать блог", privateMessage: "ЛС", pmSendTitle: "Личные сообщения", pmSend: "Отправить!", - pmCancel: "Отмена", - pmReset: "Сбросить", - pmSharedKeyHint: "Передайте этот ключ получателю по другому каналу. Он нужен для расшифровки.", - pmDescription: "Используйте эту форму, чтобы отправить зашифрованное сообщение/файл другим жителям.", - pmComposeTitle: "Отправить сообщение", + pmDescription: "Используйте эту форму для отправки зашифрованного сообщения другим жителям.", pmRecipients: "Получатели", pmRecipientsHint: "Введите Oasis ID через запятую", - pmLimitsHint: "До 7 получателей на сообщение.", - pmTextPlaceholder: "Текст ограничен 7000 символами.", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "Тема", pmSubjectHint: "Введите тему сообщения", pmText: "Сообщение", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "Расшифровать", - fileShareTitle: "Поделиться файлом", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "Файл", - fileShareDownload: "Скачать", - fileShareMutualError: "Вы можете делиться файлами только с жителями при взаимной поддержке.", - fileShareNoFile: "Файл не выбран.", - fileShareTooLarge: "Файл превышает допустимый размер.", - fileShareFailed: "Не удалось подготовить файл.", - fileShareSendError: "Не удалось отправить файл.", - fileShareUnavailable: "Этот файл сейчас недоступен. Попробуйте позже.", pmCrypterBadKey: "Общий ключ неверный!", pmCrypterTooLong: "Сообщение слишком длинное для шифрования с помощью Crypter. Сократите его и попробуйте снова.", pmCrypterCipherLabel: "Повторно зашифрованное сообщение", pmCrypterCharsLabel: "символов", pmInvalidRecipients: "Недействительный Oasis ID (должен быть вида @…=.ed25519).", pmEncryptionLabel: "Шифрование", + pmFile: "Вложение", + pmInvalidMessage: "Недействительное сообщение", pmNoSubject: "(без темы)", pmSubjectLabel: "Тема:", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "Правящий дом L.A.R.P сменился.", - politicalBotParliamentTitle: "В Парламенте начался новый цикл правления.", - politicalBotTribeTitle: "В одном из ваших племён начался новый цикл правления.", - politicalBotLarpIntro: "Теперь в этом цикле правит {house}: {cycle}", - politicalBotParliamentIntro: "Текущее правление — {gov}", - politicalBotParliamentIntroLeader: "Текущее правление — {gov}, осуществляет {leader}", - politicalBotTribeIntro: "Текущее правление в племени {tribe} — {gov}", - politicalBotTribeIntroLeader: "Текущее правление в племени {tribe} — {gov}, осуществляет {leader}", - politicalBotLeaderLabel: "Лидер", + pmBodyLabel: "Тело", + pmBotJobs: "42-JobsBOT", + pmBotProjects: "42-ProjectsBOT", + pmBotMarket: "42-MarketBOT", inboxJobSubscribedTitle: "Новая подписка на вашу вакансию", pmInhabitantWithId: "Житель с OASIS ID:", pmHasSubscribedToYourJobOffer: "подписался на вашу вакансию", + inboxProjectCreatedTitle: "Создан новый проект", + pmHasCreatedAProject: "создал проект", inboxMarketItemSoldTitle: "Товар продан", - inboxShopSoldTitle: "Товар продан", pmYourItem: "Ваш товар", pmHasBeenSoldTo: "был продан", pmFor: "за", @@ -2099,6 +2368,7 @@ module.exports = { pmHasPledged: "внёс", pmToYourProject: "в ваш проект", blockchain: "BlockExplorer", + blockchainTitle: "BlockExplorer", blockchainDescription: "Исследуйте и визуализируйте блоки в блокчейне.", blockchainNoBlocks: "Блоков в блокчейне не найдено.", blockchainStatsTitle: "Статистика", @@ -2110,36 +2380,42 @@ module.exports = { blockchainBlockCarbon: "Углеродный след", blockchainBlockType: "Тип", blockchainBlockTimestamp: "Временная метка", + blockchainBlockContent: "Блок", + blockchainBlockURL: "URL:", + blockchainContent: "Блок", + blockchainContentPreview: "Предварительный просмотр содержимого блока", blockchainLatestDatagram: "Последняя датаграмма", blockchainDatagram: "Датаграмма", - blockchainDetails: "Посмотреть детали блока", - blockchainViewBlockexplorer: "Открыть в Blockexplorer", + blockchainDetails: "Просмотр деталей блока", + blockchainViewBlockexplorer: "Открыть в blockexplorer", + blockchainBlockInfo: "Информация о блоке", + blockchainBlockDetails: "Детали выбранного блока", blockchainBack: "Назад в BlockExplorer", blockchainContentDeleted: "Этот контент был удалён", - visitContent: "Открыть контент", - favoriteAdd: "Закрепить в избранном", - pmContentTooltip: "Отправить личное сообщение", - favoriteRemove: "Убрать из избранного", - reportContent: "Пожаловаться на контент", + visitContent: "Посетить контент", banking: "Банкинг", bankingTitle: "Банкинг", - bankingDescription: "Экономика ECOin: баланс, базовый доход, налоги и ценность промышленности.", + bankingDescription: "Исследуйте текущую стоимость ECOin и соответствующее распределение UBI, распределяемое за эпоху на основе участия и доверия.", bankOverview: "Обзор", bankEpochs: "Эпохи", bankRules: "Правила", pending: "В ожидании", closed: "Закрыто", + bankBack: "Назад в Банкинг", bankViewTx: "Просмотр транзакции", bankClaimNow: "Получить сейчас", bankClaimUBI: "Получить UBI!", bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: "Нет ожидающих распределений UBI для этой эпохи.", bankEpoch: "Эпоха", bankPool: "Пул (эта эпоха)", bankWeightsSum: "Сумма весов", + bankAllocations: "Распределения", bankNoAllocations: "Распределений не найдено.", bankNoEpochs: "Эпох не найдено.", bankEpochAllocations: "Распределения эпохи", @@ -2152,11 +2428,10 @@ module.exports = { bankEpochId: "ID эпохи", bankRuleHash: "Хэш снимка правил", bankViewEpoch: "Просмотр эпохи", - bankYourIndustryBalance: "Ваша доля в промышленности", - bankYourUbiMonth: "Ваш БОД (в этом месяце)", - bankYourFundsMonth: "Ваши средства (в этом месяце)", - bankIndustryBalance: "Промышленное производство (оценка)", bankUserBalance: "Ваш баланс", + ecoWalletNotConfigured: "Кошелёк ECOin не настроен", + editWallet: "Редактировать кошелёк", + addWallet: "Добавить кошелёк", bankAddresses: "Адреса", bankNoAddresses: "Адресов не найдено.", bankUser: "Oasis ID", @@ -2180,10 +2455,18 @@ module.exports = { bankAddressDeleteConfirm: "Удалить этот адрес?", bankLocal: "Локальный", bankFromOasis: "Oasis", + bankMyAddress: "Ваш адрес", + bankRemoveMyAddress: "Удалить мой адрес", + bankNotRemovableOasis: "Адреса нельзя удалить локально", + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "НЕТ СРЕДСТВ!", bankUbiAvailableOk: "ДОСТУПНО!", - bankUbiAvailability: "БОД (сеть)", + bankUbiAvailability: "Доступность UBI", bankAlreadyClaimedThisMonth: "Уже получено в этом месяце", bankUbiThisMonth: "БОД (этот месяц)", bankUbiLastClaimed: "БОД (последнее получение)", @@ -2197,6 +2480,13 @@ module.exports = { shopsTitle: "Магазины", shopDescription: "Откройте и управляйте магазинами в сети.", shopTitle: "Магазин", + shopFilterAll: "ВСЕ", + shopFilterMine: "МОИ", + shopFilterRecent: "НЕДАВНИЕ", + shopFilterTop: "ТОП", + shopFilterProducts: "ТОВАРЫ", + shopFilterPrices: "ЦЕНЫ", + shopFilterFavorites: "ИЗБРАННЫЕ", shopUpload: "Создать магазин", shopAllSectionTitle: "Все магазины", shopMineSectionTitle: "Мои магазины", @@ -2213,10 +2503,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "Опубликовать в Clearnet", + shopClearnetUnpublish: "Убрать из Clearnet", + shopClearnetUrlLabel: "URL в Clearnet", + shopClearnetWarning: "Публикация в Clearnet делает этот магазин доступным для индексации поисковыми системами и внешними сканерами.", shopAddFavorite: "Добавить в избранное", shopRemoveFavorite: "Убрать из избранного", shopOpen: "ОТКРЫТ", shopClosed: "ЗАКРЫТ", + shopOpenShop: "Открыть магазин", + shopCloseShop: "Закрыть магазин", shopMakePrivate: "СДЕЛАТЬ ПРИВАТНОЙ (E2E)", shopMakePublic: "СДЕЛАТЬ ПУБЛИЧНОЙ", shopProducts: "Товары", @@ -2244,6 +2540,8 @@ module.exports = { shopOrderPrice: "Цена", shopOrderBuyer: "Покупатель", shopBackToShop: "Назад в магазин", + shopShareUrl: "Ссылка для обмена", + shopVisitShop: "ПОСЕТИТЬ МАГАЗИН", marketVisitItem: "ОТКРЫТЬ ТОВАР", shopStatus: "СТАТУС", shopCreatedAt: "СОЗДАН", @@ -2252,10 +2550,12 @@ module.exports = { shopLocation: "Местоположение", shopTags: "Теги", shopVisibility: "Видимость", + shopImage: "Изображение", shopShortDescription: "Краткое описание", shopShortDescriptionPlaceholder: "Краткое описание для карточек (макс. 160 символов)", shopTitlePlaceholder: "Название вашего магазина", shopDescriptionPlaceholder: "Подробное описание вашего магазина", + shopUrlPlaceholder: "https://url-vashego-magazina.ru", shopLocationPlaceholder: "Город, Страна", shopTagsPlaceholder: "тег1, тег2, тег3", mapTitlePlaceholder: "Название карты", @@ -2273,6 +2573,7 @@ module.exports = { bankUnitMinutes: "минуты", bankUnitDays: "дни", bankExchangeNoData: "Данные недоступны", + bankExchangeIndex: "Стоимость ECOin (1ч)", bankInflation: "Инфляция ECOin (anual)", bankInflationMonthly: "Инфляция ECOin (mensual)", bankExchangeChartTitle: "Динамика стоимости ECOin со временем", @@ -2286,15 +2587,36 @@ module.exports = { bankingSyncStatusOutdated: "Устарел", statsTitle: "Статистика", statistics: "Статистика", + statsInhabitant: "Статистика жителя", statsDescription: "Откройте статистику вашей сети.", TOMBSTONEButton: "НАДГРОБИЯ", + statsYou: "Вы", + statsUserId: "Oasis ID", statsCreatedAt: "Создано", statsYourContent: "Контент", statsYourOpinions: "Мнения", + statsYourTombstone: "Надгробия", + statsNetwork: "Сеть", + statsTotalInhabitants: "Жители", + statsDiscoveredTribes: "Племена (публичные)", + statsPrivateDiscoveredTribes: "Племена (частные)", statsNetworkContent: "Контент", + statsYourMarket: "Рынок", + statsYourJob: "Вакансии", + statsYourProject: "Проекты", + statsYourTransfer: "Переводы", + statsYourForum: "Форумы", statsNetworkOpinions: "Мнения", + statsDiscoveredMarket: "Рынок", + statsDiscoveredJob: "Вакансии", + statsDiscoveredProject: "Проекты", + statsBankingTitle: "Банкинг", statsEcoWalletLabel: "Кошелёк ECOIN", statsEcoWalletNotConfigured: "Не настроен!", + statsTotalEcoAddresses: "Всего адресов", + statsDiscoveredTransfer: "Переводы", + statsDiscoveredForum: "Форумы", + statsNetworkTombstone: "Надгробия", statsBookmark: "Закладки", statsEvent: "События", statsTask: "Задачи", @@ -2317,12 +2639,16 @@ module.exports = { statsAiExchange: "ИИ", statsPUBs: "PUBs", statsPost: "Посты", + statsOasisID: "Oasis ID", statsSize: "Итого (размер)", statsBlockchainSize: "Блокчейн (размер)", statsBlobsSize: "Блобы (размер)", statsActivity7d: "Активность (последние 7 дней)", statsActivity7dTotal: "Итого за 7 дней", statsActivity30dTotal: "Итого за 30 дней", + statsKarmaScore: "Очки КАРМЫ", + statsPublic: "Публичное", + statsPrivate: "Частное", messages: "Сообщения", statsProjectsTitle: "Проекты", statsProjectsTotal: "Всего проектов", @@ -2332,16 +2658,33 @@ module.exports = { statsProjectsCancelled: "Отменённые", statsProjectsGoalTotal: "Общая цель", statsProjectsPledgedTotal: "Всего поддержано", + statsProjectsSuccessRate: "Процент успеха", + statsProjectsAvgProgress: "Средний прогресс", + statsProjectsMedianProgress: "Медианный прогресс", + statsProjectsActiveFundingAvg: "Среднее активное финансирование", + statsJobsTitle: "Вакансии", + statsJobsTotal: "Всего вакансий", + statsJobsOpen: "Открытые", + statsJobsClosed: "Закрытые", + statsJobsOpenVacants: "Открытые места", + statsJobsSubscribersTotal: "Всего подписчиков", + statsJobsAvgSalary: "Средняя зарплата", + statsJobsMedianSalary: "Медианная зарплата", statsMarketTitle: "Рынок", statsMarketTotal: "Всего товаров", statsMarketForSale: "На продаже", statsMarketReserved: "Зарезервировано", statsMarketClosed: "Закрыто", statsMarketSold: "Продано", + statsMarketRevenue: "Выручка", + statsMarketAvgSoldPrice: "Средняя цена продажи", statsUsersTitle: "Жители", user: "Житель", + statsTombstoneTitle: "Надгробия", + statsNetworkTombstones: "Надгробия сети", statsTombstoneRatio: "Доля надгробий (%)", statsAITraining: "Обучение ИИ", + statsAIExchanges: "Обмены", statsParliamentCandidature: "Кандидатуры парламента", statsParliamentTerm: "Сроки парламента", statsParliamentProposal: "Предложения парламента", @@ -2367,19 +2710,27 @@ module.exports = { aiConfiguration: "Установить запрос", aiPromptUsed: "Запрос", aiClearHistory: "Очистить историю чата", + aiSharePrompt: "Добавить этот ответ в коллективное обучение?", + aiShareYes: "Да", + aiShareNo: "Нет", + aiSharedLabel: "Добавлено в обучение", + aiRejectedLabel: "Не добавлено в обучение", aiServerError: "ИИ не смог ответить. Пожалуйста, попробуйте снова.", typeAiExchange: "ИИ", aiApproveTrain: "Добавить в коллективное обучение", aiRejectTrain: "Не обучать", + aiTrainPending: "Ожидает одобрения", aiTrainApproved: "Одобрено для обучения", aiTrainRejected: "Отклонено для обучения", aiSnippetsUsed: "Использованные фрагменты", aiSnippetsLearned: "Изученные фрагменты", + aiApproveCustomTrain: "Обучить используя этот пользовательский ответ", aiCustomAnswerPlaceholder: "Напишите ваш пользовательский ответ…", marketMineSectionTitle: "Ваши товары", marketCreateSectionTitle: "Создать товар", marketUpdateSectionTitle: "Обновить", marketAllSectionTitle: "Рынок", + marketRecentSectionTitle: "Последние товары", marketTitle: "Рынок", marketDescription: "Торговая площадка для обмена товарами или услугами в вашей сети.", marketFilterAll: "ВСЕ", @@ -2409,11 +2760,14 @@ module.exports = { marketItemDeadline: "Срок", marketItemIncludesShipping: "Включена доставка?", marketItemHighestBid: "Наибольшая ставка", + marketItemHighestBidder: "Лидер аукциона", marketItemStock: "Запас", marketOutOfStock: "Нет в наличии", + marketItemBidTime: "Время ставки", marketActionsUpdate: "Обновить", marketUpdateButton: "Обновить", marketActionsDelete: "Удалить", + marketActionsSold: "Отметить как проданное", marketActionsChangeStatus: "ИЗМЕНИТЬ СТАТУС", marketActionsBuy: "КУПИТЬ!", marketAuctionBids: "Текущие ставки", @@ -2422,82 +2776,19 @@ module.exports = { marketNoItems: "Товаров пока нет.", marketYourBid: "Ваша ставка", marketCreateFormImageLabel: "Загрузить медиа (макс: 50MB)", + marketSearchLabel: "Поиск", marketSearchPlaceholder: "Поиск по названию или тегам", marketMinPriceLabel: "Мин. цена", marketMaxPriceLabel: "Макс. цена", + marketSortLabel: "Сортировать по", marketSortRecent: "Самые новые", marketSortPrice: "Цена", marketSortDeadline: "Срок", marketSearchButton: "Поиск", marketAuctionEndsIn: "Завершается", marketAuctionEnded: "Завершён", + marketMyBidBadge: "Ваша ставка", marketNoItemsMatch: "Товаров не найдено.", - housingTitle: "Жильё", - housingDescriptionText: "Находите и управляйте местами в вашей сети.", - modulesHousingLabel: "Жильё", - modulesHousingDescription: "Модуль для поиска и управления местами.", - typeHousing: "ЖИЛЬЁ", - housingFilterAll: "ВСЕ", - housingFilterMine: "МОИ", - housingFilterRequested: "ЗАПРОШЕННЫЕ", - housingFilterSale: "ПРОДАЖА", - housingFilterRent: "АРЕНДА", - housingFilterCouchsurfing: "КАУЧСЁРФИНГ", - housingFilterOpen: "ДОСТУПНЫЕ", - housingFilterClosed: "НЕДОСТУПНЫЕ", - housingFilterRecent: "НОВЫЕ", - housingFilterTop: "ТОП", - housingType: "Предложение", - housingTypeSALE: "Продажа", - housingTypeRENT: "Аренда", - housingTypeCOUCHSURFING: "Каучсёрфинг", - housingProperty: "Объект", - housingPropertyAPARTMENT: "Квартира", - housingPropertyHOUSE: "Дом", - housingPropertyROOM: "Комната", - housingPropertyLAND: "Участок", - housingPropertyOTHER: "Другое", - housingTitleLabel: "Заголовок", - housingTitlePlaceholder: "Название места", - housingDescription: "Описание", - housingDescriptionPlaceholder: "Опишите место, район, что включено: санузлы, сад, спальные места…", - housingRules: "Правила", - housingRulesPlaceholder: "Животные, курение, тихие часы, минимальный срок…", - housingPlace: "Место", - housingPlacePlaceholder: "Страна, город, адрес", - housingPrice: "Цена", - housingPriceLabel: "Цена (ECO)", - housingSizeLabel: "Площадь (м²)", - housingFree: "БЕСПЛАТНО", - housingPerMonth: "/месяц", - housingRooms: "Комнаты", - housingSize: "Площадь", - housingCapacity: "Гости", - housingAvailableFrom: "Доступно с", - housingAvailableTo: "Доступно до", - housingTags: "Теги", - spreadEditWarning: "Этот контент потеряет спреды после редактирования", - housingRequests: "Запросы", - housingRequestButton: "Запросить", - housingCancelButton: "Отменить запрос", - housingRequestedBadge: "ЗАПРОШЕНО", - housingStatus: "Статус", - housingStatusOPEN: "ДОСТУПНО", - housingStatusCLOSED: "НЕДОСТУПНО", - housingSetOpen: "Сделать доступным", - housingSetClosed: "Сделать недоступным", - housingCreateButton: "Создать место", - housingUpdateButton: "Обновить", - housingDeleteButton: "Удалить", - housingSearchPlaceholder: "Поиск мест...", - housingSearchButton: "Искать", - housingMinPrice: "Мин. цена", - housingMaxPrice: "Макс. цена", - housingSortRecent: "Сначала новые", - housingSortPrice: "Сначала дешевле", - housingSortRequests: "Больше запросов", - housingSortRating: "Лучшие по оценке", - housingNoItems: "Места не найдены.", jobsTitle: "Вакансии", jobsCandidatesTitle: "Рекомендуемые кандидаты", jobsCandidatesDescription: "Жители, чьи навыки соответствуют этой вакансии. Отправьте им личное сообщение.", @@ -2546,6 +2837,7 @@ module.exports = { jobLocationPresencial: "На месте", jobLocationRemote: "Удалённо", jobVacantsPlaceholder: "Количество позиций", + jobSalaryPlaceholder: "Зарплата в ECO за 1 час работы", jobImage: "Загрузить медиа (макс: 50MB)", jobTasks: "Задачи", jobType: "Тип занятости", @@ -2555,6 +2847,7 @@ module.exports = { jobsFilterTop: "ТОП", jobsTopTitle: "Топ вакансий по зарплате", createJobButton: "Опубликовать вакансию", + viewDetailsButton: "Просмотр деталей", noJobsFound: "Вакансий не найдено.", jobAuthor: "Автор", jobTypeFreelance: "Фриланс", @@ -2566,6 +2859,10 @@ module.exports = { jobsHoursOffered: "Предлагаемые часы", jobsHoursRequested: "Часы, запрашиваемые взамен", jobsExchangeSkill: "Запрашиваемый навык в обмен", + jobsHoursOfferedPlaceholder: "напр. 4", + jobsHoursRequestedPlaceholder: "напр. 4", + jobsExchangeSkillPlaceholder: "напр. плотничество, дизайн", + jobExchangeHint: "Для работ по обмену часами заполните поля ниже вместо зарплаты.", jobTimePartial: "Неполная занятость", jobTimeComplete: "Полная занятость", jobsDeleteButton: "УДАЛИТЬ", @@ -2573,200 +2870,30 @@ module.exports = { jobsFilterApplied: "ПОДАННЫЕ", jobsAppliedTitle: "Мои заявки", jobsAppliedBadge: "Подано", + jobsFilterFavs: "Избранное", + jobsFavsTitle: "Избранное", + jobsFilterNeeds: "Нужна помощь", + jobsNeedsTitle: "Нужна помощь", + jobsSearchLabel: "Поиск", jobsSearchPlaceholder: "Поиск по названию, тегам, описанию...", jobsMinSalaryLabel: "Мин. зарплата", jobsMaxSalaryLabel: "Макс. зарплата", + jobsSortLabel: "Сортировать по", jobsSortRecent: "Самые новые", jobsSortSalary: "Самая высокая зарплата", jobsSortSubscribers: "Больше всего заявок", jobsSearchButton: "Поиск", + jobsFavoriteButton: "Добавить в избранное", + jobsUnfavoriteButton: "Удалить из избранного", + jobsMessageAuthorButton: "ЛС", jobsApplicants: "Заявители", jobsUpdatedAt: "Обновлено", + jobNewBadge: "НОВАЯ", jobsTagsLabel: "Теги", + jobsTagsPlaceholder: "тег1, тег2, тег3", noJobsMatch: "Вакансий не найдено.", - industrySearchPlaceholder: "Поиск фабрик…", - industryAllSectors: "Все секторы", - industryVisitButton: "Открыть промышленность", - industryAdvanceTo: "Перевести в", - industryAmount: "Сумма", - industryApproveBuild: "Одобрить", - industryBlueprint: "Чертёж", - industryBlueprints: "Чертежи", - industryBuild: "Сборка", - industryBuildNotes: "Заметки", - industryBuildStart: "Дата начала", - industryVoteUpdateButton: "Голос за обновление", - industryVoteDeleteButton: "Голос за удаление", - industryRevokeVote: "Отозвать", - industryTimeLeft: "Осталось времени", - industryBuildEnd: "Дата окончания", - industryBuilds: "Сборки", - industryBuildTitle: "Название", - industryContribute: "Внести вклад", - industryContributeButton: "Внести вклад", - industryContributions: "Вклады", - industryCreateBlueprintButton: "Создать чертёж", - industryDistribute: "Распределить", - industryDistributeButton: "Распределить по долям", - industryDistribution: "Распределение", - industryEcoAmount: "Сумма (ECO)", - industryEditBlueprint: "Редактировать чертёж", - industryFacility: "Фабрика", - industryKindLabel: "Тип", - industryKind_labor: "Труд", - industryKind_material: "Материал", - industryKind_eco: "Средства", - industryLaborHours: "Труд (часы)", - industryHours: "Часы", - industryMaterialItem: "Материал", - industryMaterials: "Материалы", - industryEstMaterials: "Итого материалы", - industryEstLabor: "Итого труд", - industryEstTotal: "Оценочная цена", - industryBuildingPrice: "Цена изготовления", - industryFinalPrice: "Итоговая цена", - industryBlueprintDescPlaceholder: "Характеристики, размеры, вес, документация…", - industryMaterialValue: "Стоимость (ECO)", - industryMember: "Участник", - industryNewBlueprint: "Новый чертёж", - industryNewBuild: "Предложить сборку", - industryEditBuild: "Редактировать производство", - industryNeedBlueprint: "Сначала создайте чертёж: каждое производство изготавливает его.", - industryNoBlueprints: "Пока нет чертежей.", - industryNoBuilds: "Пока нет сборок.", - industryNote: "Заметка", - industryOutput: "Выход", - industryOutputKind: "Тип продукта", - industryOutputValue: "Стоимость выхода (ECO, напр. от продажи)", - industryPostJob: "Создать вакансию", - industryPot: "Банк", - industryProposeBuildButton: "Предложить сборку", - industryProposer: "Инициатор", - industryAuthor: "Автор", - industrySellOnMarket: "Отправить на рынок", - industrySendToProjects: "Создать проект", - industrySkills: "Навыки", - industryTreasury: "Казна", - industryKind_physical: "Физический", - industryKind_digital: "Цифровой", - industryBuildStatus_PROPOSED: "ПРЕДЛОЖЕНО", - industryBuildStatus_REJECTED: "ОТКЛОНЁН", - industryBuildStatus_APPROVED: "ОДОБРЕНО", - industryBuildStatus_STOCKING: "СНАБЖЕНИЕ", - industryBuildStatus_IN_PRODUCTION: "В ПРОИЗВОДСТВЕ", - industryBuildStatus_COMPLETED: "ЗАВЕРШЕНО", - industryBuildStatus_FAILED: "ПРОВАЛЕНО", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "Вакансия совпадает с вашим резюме", - jobsBotMatchIntro: "Вакансия совпадает с вашим резюме.", - jobsBotMatchJob: "Вакансия", - jobsBotMatchHint: "Вы получаете это, потому что вашим резюме управляет ИИ. Отключить можно в резюме.", - cvAiManaged: "Управление ИИ", - switchOn: "ВКЛ", - switchOff: "ВЫКЛ", - cvAiManagedHint: "Если включено, JobsBot сообщит личным сообщением о подходящей вакансии.", - cvMatchThreshold: "Уведомлять с этого уровня совпадения", - housingBotRequestedTitle: "Кто-то запросил одно из ваших мест.", - housingBotCancelledTitle: "Запрос на одно из ваших мест отменён.", - housingBotYouRequestedTitle: "Вы запросили место.", - housingBotYouCancelledTitle: "Вы отменили запрос.", - housingBotUnavailableTitle: "Запрошенное вами место больше недоступно.", - housingBotUnavailableSentTitle: "Вы уведомили тех, кто запрашивал это место.", - industryBotAdmittedTitle: "Вас приняли на фабрику.", - industryBotApplicationTitle: "Новая заявка на вступление в вашу фабрику.", - industryBotInvitedTitle: "Вас пригласили на фабрику.", - industryBotDissolvedTitle: "Фабрика, к которой вы принадлежите, была распущена.", - industryBotBuildApprovedTitle: "Сборка одобрена.", - industryBotDistributedTitle: "Выход сборки распределён.", - industryFilterRules: "ПРАВИЛА", - industryRulesTitle: "Правила", - industryRulesIntro: "Industry позволяет жителям коллективно владеть средствами производства: Фабрика — это принадлежащая сети мастерская, которой никто не владеет частно.", - industryRulesSteward: "Основатель — управляющий, хранитель, а не владелец: он не может в одиночку продать, приватизировать или распустить Фабрику.", - industryRulesMembership: "Политика членства задаёт способ вступления: Открытая (сразу), Голосованием (принимают голосом участников) или Только по приглашению (сначала должен пригласить участник).", - industryRulesQuorum: "Кворум — минимальное число участников, которые должны проголосовать, чтобы решение засчиталось, чтобы маленькую Фабрику не решал один человек.", - industryRulesMajority: "Большинство — доля голосов для принятия (0,5 = половина, 1 = единогласие); решение проходит, когда голоса ЗА достигают хотя бы max(кворум, участники × большинство).", - industryRulesDecisions: "Участники голосуют за приём новых, одобрение сборок и роспуск Фабрики; управляющий не может обойти эти коллективные решения.", - industryRulesBlueprints: "Чертежи — свободные (COPYLEFT) рецепты: входы (материалы, часы труда, навыки) и выход (физический или цифровой продукт), и любой может их форкнуть.", - industryRulesBuilds: "Сборка — производственный заказ: ПРЕДЛОЖЕНО → ОДОБРЕНО (голосованием) → СНАБЖЕНИЕ → В ПРОИЗВОДСТВЕ → ЗАВЕРШЕНО, и принимает вклады только после одобрения.", - industryRulesContributions: "Участники вносят труд (часы), материалы (заявленная стоимость в ECO) или ECO в сборку; каждый вклад даёт карму.", - industryRulesLaborRate: "Ставка труда — стоимость в ECO одного рабочего часа на этой Фабрике; она переводит часы в карму, чтобы справедливо сравнивать труд и капитал: карма = часы × ставка + стоимость материалов + ECO.", - industryRulesShares: "Ваша доля в сборке — это ваша карма ÷ общая карма, и она определяет, сколько стоимости выхода вы получите.", - industryRulesDistribution: "После завершения производства стюард распределяет банк (средства производства плюс стоимость продажи) между участниками пропорционально их долям.", - industryRulesTreasury: "Вклады в ECO хранит управляющий как хранитель казны сборки до распределения; все движения записываются в сети, а споры могут идти в Courts.", - industryJobs: "Вакансии", - industryReportButton: "Пожаловаться", - industryCreateTask: "Создать задачу", - industryOpenDispute: "Открыть спор", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "Создать приглашение", - industryPauseVotes: "Голоса за статус", - industryTitle: "Промышленность", - industryDescription: "Модуль для коллективного управления средствами производства.", - typeIndustryBuild: "СБОРКА", - typeIndustryBlueprint: "ЧЕРТЁЖ", - typeIndustryAllocation: "РАСПРЕДЕЛЕНИЕ", - typeIndustry: "ПРОМЫШЛЕННОСТЬ", - modulesIndustryLabel: "Промышленность", - modulesIndustryDescription: "Модуль для коллективного управления средствами производства.", - industryFilterAll: "ВСЕ", - industryFilterMember: "УЧАСТНИК", - industryFilterBlueprints: "ЧЕРТЕЖИ", - industryFilterBuilds: "ПРОИЗВОДСТВА", - industryFilterMine: "МОИ", - industryFilterActive: "РАБОТАЕТ", - industryFilterPaused: "НА ПАУЗЕ", - industryFilterDissolved: "РАСПУЩЕННЫЕ", - industryAllTitle: "Промышленность", - industryMemberTitle: "Фабрики, где я состою", - industryMineTitle: "Мои фабрики", - industryActiveTitle: "Работает", - industryPausedTitle: "Фабрики на паузе", - industryDissolvedTitle: "Распущенные фабрики", - industryNoFacilitiesFound: "Фабрики не найдены.", - industryCreateFacility: "Создать фабрику", - industryMembers: "Участники", - industryMemberBadge: "УЧАСТНИК", - industryName: "Название", - industryNamePlaceholder: "Название фабрики", - industryDescriptionLabel: "Описание", - industryDescriptionPlaceholder: "Что производит эта фабрика?", - industrySector: "Сектор", - industryMembershipPolicy: "Политика членства", - industryQuorum: "Кворум", - industryMajority: "Большинство", - industryLaborRate: "Ставка труда", - industryCreateButton: "Создать фабрику", - industryUpdateButton: "Обновить", - industrySteward: "Управляющий", - industryStatusActive: "РАБОТАЕТ", - industryStatusPaused: "НА ПАУЗЕ", - industryStatusDissolved: "РАСПУЩЕНА", - industryStatusLabel: "Статус", - industryJoinButton: "Присоединиться", - industryApplyButton: "Подать заявку", - industryLeaveButton: "Покинуть", - industryInviteButton: "Пригласить", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "Для вступления нужно приглашение.", - industryPauseButton: "Голосовать за паузу", - industryResumeButton: "Голосовать за возобновление", - industryDeleteButton: "Удалить", - industryPendingApplicants: "Ожидающие заявки", - industryVotesYes: "да", - industryVotesNo: "нет", - industryAlreadyVoted: "проголосовал", - industryAdmit: "Принять", - industryReject: "Отклонить", - industryDissolveTitle: "Распустить фабрику", - industryDissolveVote: "Голосовать за роспуск", - industryPolicy_open: "Открытая", - industryPolicy_vote: "Голосованием", - industryPolicy_invite: "Только по приглашению", projectsTitle: "Проекты", projectsDescription: "Создавайте, финансируйте и следите за проектами сообщества в вашей сети.", - projectSearchPlaceholder: "Поиск проектов...", projectCreateProject: "Создать проект", projectCreateButton: "Создать проект", projectUpdateButton: "ОБНОВИТЬ", @@ -2810,8 +2937,14 @@ module.exports = { projectPledgeTitle: "Поддержать проект", projectPledgePlaceholder: "Сумма в ECO", projectBounties: "Награды", + projectBountiesInputLabel: "Награды (по одной в строке: Название|Сумма [ECO]|Описание)", + projectBountiesPlaceholder: "Исправить баг UI|100|Ссылка на задачу\nНаписать документацию|250|Примеры использования", projectNoBounties: "Наград не найдено.", projectTitle: "Название", + projectAddBountyTitle: "Новая награда", + projectBountyTitle: "Название награды", + projectBountyAmount: "Сумма (ECO)", + projectBountyDescription: "Описание", projectMilestoneSelect: "Выбрать веху", projectBountyCreateButton: "Создать награду", projectBountyStatus: "Статус награды", @@ -2826,15 +2959,19 @@ module.exports = { projectMilestoneTitle: "Название вехи", projectMilestoneTargetPercent: "Процент (%)", projectMilestoneDueDate: "Дата", + projectMilestoneCreateButton: "Создать веху", projectMilestoneStatus: "Статус вехи", projectMilestoneOpen: "открытая", projectMilestoneDone: "завершена", projectMilestoneMarkDone: "Отметить завершённой", projectMilestoneDue: "дата", + projectNoMilestones: "Вех не найдено.", projectMilestoneTitlePlaceholder: "Введите название вехи", projectMilestoneDescriptionPlaceholder: "Введите описание этой вехи", projectMilestoneDescription: "Описание вехи", + projectBudgetGoal: "Бюджет (Цель)", projectBudgetAssigned: "Назначено на награды", + projectBudgetRemaining: "Остаток", projectBudgetOver: "⚠ Превышение бюджета: назначено больше цели", projectFollowers: "Подписчики", projectFollowersTitle: "Подписчики", @@ -2847,6 +2984,7 @@ module.exports = { projectBackersTotalPledged: "Всего поддержано", projectBackersYourPledge: "Ваш взнос", projectBackersNone: "Взносов пока нет.", + projectNoRemainingBudget: "Оставшегося бюджета нет.", projectFilterBackers: "СПОНСОРЫ", projectFilterApplied: "ПОДАННЫЕ", projectAppliedTitle: "ПОДАННЫЕ", @@ -2855,15 +2993,30 @@ module.exports = { projectBackerAmount: "Всего вложено", projectBackerPledges: "Взносы", projectBackerProjects: "Проекты", + projectPledgeAmount: "Сумма", projectSelectMilestoneOrBounty: "Выбрать веху или награду", projectPledgeButton: "Поддержать", footerLicense: "GPLv3", + footerPackage: "Пакет", + footerVersion: "Версия", modulesModuleName: "Имя", modulesModuleDescription: "Описание", modulesModuleStatus: "Статус", modulesTotalModulesLabel: "Загруженные модули", modulesEnabledModulesLabel: "Включённые", modulesDisabledModulesLabel: "Отключённые", + modulesPopularLabel: "Популярное", + modulesPopularDescription: "Модуль для получения популярных, наиболее просматриваемых или комментируемых постов.", + modulesTopicsLabel: "Темы", + modulesTopicsDescription: "Модуль для получения категорий обсуждений на основе общих интересов.", + modulesSummariesLabel: "Резюме", + modulesSummariesDescription: "Модуль для получения резюме длинных обсуждений или постов.", + modulesLatestLabel: "Последние", + modulesLatestDescription: "Модуль для получения последних постов и обсуждений.", + modulesThreadsLabel: "Треды", + modulesThreadsDescription: "Модуль для получения разговоров, сгруппированных по теме или вопросу.", + modulesMultiverseLabel: "Мультивселенная", + modulesMultiverseDescription: "Модуль для получения контента от других федеративных узлов.", modulesInvitesLabel: "Приглашения", modulesInvitesDescription: "Модуль для управления и применения кодов приглашений.", modulesWalletLabel: "Кошелёк", @@ -2904,12 +3057,8 @@ module.exports = { modulesOpinionsDescription: "Модуль для открытия и голосования за мнения.", modulesTransfersLabel: "Переводы", modulesTransfersDescription: "Модуль для открытия и управления смарт-контрактами (переводами).", - modulesFediverseLabel: "Мультивселенная", - modulesBlogsLabel: "Блоги", - modulesBlogsDescription: "Модуль для поиска и управления блогами.", - modulesPollsLabel: "Опросы", - modulesPollsDescription: "Модуль, чтобы спросить сеть и подсчитать ответы.", - modulesFediverseDescription: "Управляйте своими другими аккаунтами мультивселенной, включая отправку и получение контента.", + modulesFediverseLabel: "Федиверс", + modulesFediverseDescription: "Управляйте своими другими аккаунтами федиверса, включая отправку и получение контента.", modulesFeedLabel: "Лента", modulesFeedDescription: "Модуль для открытия и публикации коротких текстов (лента).", modulesParliamentLabel: "Парламент", @@ -2945,37 +3094,45 @@ module.exports = { visibilityMakeHidden: "Скрыть", invitesInhabitantsTitle: "Жители", invitesInhabitantsFollow: "Подписаться", - aiNavPlaceholder: "Опишите, что вам нужно...", + aiNavPlaceholder: "Куда вы хотите перейти?", aiNavDisabled: "Навигация с ИИ отключена.", - aiNavResultsTitle: "Подсказки AINav", + aiNavResultsTitle: "Предложения навигации с ИИ", aiNavQueryLabel: "Запрос", + aiNavResultsDescription: "Выберите маршрут, наиболее подходящий вашему запросу.", + aiNavResultPath: "Маршрут", + aiNavResultDescription: "Что можно сделать", aiNavResultMatch: "Совпадение", aiNavResultsEmpty: "Подходящих маршрутов нет. Попробуйте /search.", + aiNavSearchInstead: "Поиск вместо этого", modulesAINavLabel: "AINav", modulesAINavDescription: "Модуль для запросов на естественном языке к содержимому сети.", + directConnect: "Прямое подключение", directConnectDescription: "Подключитесь напрямую к узлу, введя его IP-адрес, порт и публичный ключ. Узел будет добавлен как отслеживаемое соединение.", peerHost: "IP", peerPort: "Порт (по умолчанию: 8008)", peerPublicKey: "Публичный ключ (@...ed25519)", connectAndFollow: "Подключиться", + deviceSourceLabel: "Источник устройства", modulesPresetTitle: "Общие конфигурации", - workflowsTitle: "Рабочие режимы", - workflowsDescription: "Режим задаёт тему и набор активных модулей.", - workflowsSet: "Применить режим", - workflow_default: "По умолчанию", - workflow_jobs: "Работа", - workflow_ruling: "Управление", - workflow_politics: "Политика", - workflow_social: "Общение", - workflow_business: "Бизнес", - workflow_night: "Ночь", - workflow_mobile: "Мобильный", + modulesPreset_minimal: "Минимальный", + modulesPreset_basic: "Базовая", + modulesPreset_social: "Социальный", + modulesPreset_economy: "Экономическая", + modulesPreset_full: "Полная", statsCarbonFootprintTitle: "Углеродный след", + statsCarbonFootprintNetwork: "Углеродный след сети", + statsCarbonFootprintYours: "Ваш углеродный след", statsCarbonTombstone: "Углеродный след надгробий", + feedSuccessMsg: "Пост успешно опубликован!", + dominantOpinionLabel: "Доминирующее мнение", uploadMedia: "Загрузить медиа (макс: 50MB)", + feedViewDetails: "Просмотр деталей", feedOpenDiscussion: "Открыть обсуждение", feedDetailTitle: "Лента", feedPostComment: "Опубликовать комментарий", + eventFileLabel: "Прикрепить изображение", + taskFileLabel: "Прикрепить изображение", + courtsRespondentRequired: "ID ответчика обязателен", courtsRespondentInvalid: "Должен быть действительным SSB ID (@...ed25519)", noComments: "Пока нет комментариев", bankAllocId: "ID распределения", @@ -3009,13 +3166,14 @@ module.exports = { mapDescriptionLabel: "Описание", mapDescriptionPlaceholder: "Опишите карту или местоположение...", mapTypeLabel: "Тип карты", - mapInviteMode: "Приглашение", - mapTypeSingle: "ОДИНОЧНЫЙ", - mapTypeOpen: "ОТКРЫТЫЙ", - mapTypeClosed: "ЗАКРЫТЫЙ", + mapTypeSingle: "ОДИНОЧНЫЙ (только начальное местоположение)", + mapTypeOpen: "ОТКРЫТЫЙ (любой может добавлять маркеры)", + mapTypeClosed: "ЗАКРЫТЫЙ (только создатель добавляет маркеры)", mapTagsLabel: "Теги", mapTagsPlaceholder: "Введите теги через запятую", mapUrlLabel: "URL карты", + mapPickCoordLabel: "Или выберите местоположение на сетке:", + mapMarkersLabel: "маркеры", mapMarkersTitle: "Маркеры", mapMarkerDefault: "Маркер", mapMarkerLatLabel: "Широта маркера", @@ -3042,9 +3200,14 @@ module.exports = { padTitle: "Пад", modulesPadsLabel: "Пады", modulesPadsDescription: "Модуль для управления совместными текстовыми редакторами.", + padFilterAll: "ВСЕ", + padFilterMine: "МОИ", + padFilterRecent: "НЕДАВНИЕ", + padFilterOpen: "ОТКРЫТЫЕ", + padFilterClosed: "ЗАКРЫТЫЕ", padCreate: "Создать Пад", - padUpdate: "Обновить", - padDelete: "Удалить", + padUpdate: "Обновить Пад", + padDelete: "Удалить Пад", padTitleLabel: "Заголовок", padTitlePlaceholder: "Введите заголовок пада...", padStatusLabel: "Статус", @@ -3055,7 +3218,14 @@ module.exports = { padTagsLabel: "Теги", padTagsPlaceholder: "тег1, тег2, ...", padMembersLabel: "Участники", + padVisitPad: "Посетить Пад", + padShareUrl: "Поделиться URL", padCreated: "Создано", + padAuthor: "Автор", + padGenerateCode: "Сгенерировать Код", + padInviteCodeLabel: "Код Приглашения", + padInviteCodePlaceholder: "Введите код приглашения...", + padValidateInvite: "Проверить", padStartEditing: "НАЧАТЬ РЕДАКТИРОВАНИЕ!", padEditorPlaceholder: "Начните писать...", padSubmitEntry: "Отправить", @@ -3068,11 +3238,12 @@ module.exports = { padClosedSectionTitle: "Закрытые Пады", padCreateSectionTitle: "Создать Новый Пад", padUpdateSectionTitle: "Обновить Пад", + padInviteGenerated: "Код Приглашения Сгенерирован", typePad: "ПАДЫ", padNew: "НОВЫЙ", padAddFavorite: "Добавить в Избранное", padRemoveFavorite: "Удалить из Избранного", - padClose: "Закрыть", + padClose: "Закрыть Pad", padBackToEditor: "Назад к редактору", padSearchPlaceholder: "Поиск падов...", @@ -3080,6 +3251,8 @@ module.exports = { modulesChatsDescription: "Модуль для обнаружения и управления зашифрованными чатами.", typeChat: "ЧАТЫ", typeChatMessage: "СООБЩЕНИЕ ЧАТА", + chatLabel: "ЧАТЫ", + chatMessageLabel: "СООБЩЕНИЯ ЧАТА", chatsTitle: "Чаты", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3089,50 +3262,52 @@ module.exports = { chatDescription: "Описание", chatCategory: "Категория", chatStatus: "СТАТУС", + chatFilterAll: "ВСЕ", + chatFilterMine: "МОИ", + chatFilterRecent: "НЕДАВНИЕ", + chatFilterFavorites: "ИЗБРАННОЕ", + chatFilterOpen: "ОТКРЫТЫЕ", + chatFilterClosed: "ЗАКРЫТЫЕ", chatCreate: "Создать Чат", - chatUpdate: "Обновить", - chatDelete: "Удалить", + chatUpdate: "Обновить Чат", + chatDelete: "Удалить Чат", chatClose: "Закрыть Чат", chatVisitChat: "ПЕРЕЙТИ В ЧАТ", chatUntitled: "Чат без названия", chatNoItems: "Чаты не найдены.", chatParticipants: "Участники", - chatMessagesLabel: "Сообщения", - chatThreadsLabel: "Темы", + chatStartChatting: "НАЧАТЬ ОБЩЕНИЕ!", + chatGenerateCode: "Создать Код", + chatShareUrl: "Поделиться URL", chatCreatedAt: "СОЗДАН", chatSearchPlaceholder: "Поиск чатов...", chatStatusOpen: "ОТКРЫТЫЙ", chatStatusInviteOnly: "ТОЛЬКО ПО ПРИГЛАШЕНИЮ", chatStatusClosed: "ЗАКРЫТЫЙ", chatSendMessage: "Отправить", - chatJumpLatest: "Последнее сообщение", - chatPickChat: "Выберите чат, чтобы открыть его", - chatNoneYet: "Пока нет доступных чатов.", - activitySearchPlaceholder: "Поиск по активности...", - trendingSearchPlaceholder: "Поиск в трендах...", - opinionsSearchPlaceholder: "Поиск в мнениях...", - votesSearchPlaceholder: "Поиск в голосованиях...", - welcomeStepUxTitle: "Выберите интерфейс", - welcomeStepUxText: "Выберите вид Oasis. Потом можно изменить в настройках.", - welcomeStepUxAction: "Использовать этот вид", - chatRateLimitTitle: "Слишком много сообщений", - chatRateLimitMessage: "Вы достигли лимита сообщений этой комнаты за час. Попробуйте позже.", chatMessagePlaceholder: "Введите сообщение...", chatNoMessages: "Сообщений пока нет.", + chatLeave: "Покинуть Чат", + chatInviteCodeLabel: "Введите код приглашения", + chatJoinByInvite: "Войти", chatTitlePlaceholder: "Название чата", chatDescriptionPlaceholder: "Описание чата", chatTagsPlaceholder: "тег1, тег2, тег3", chatImageLabel: "Выбрать файл изображения (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "Код Приглашения", chatAuthor: "Автор", + chatCreated: "Создан", chatAddFavorite: "Добавить в Избранное", chatRemoveFavorite: "Убрать из Избранного", chatPM: "ЛС", chatStatusLabel: "Статус", chatCategoryLabel: "Категория", + chatParticipantsLabel: "Участники", gamesTitle: "Игры", gamesDescription: "Откройте и играйте в игры.", gamesFilterAll: "ВСЕ", gamesPlayButton: "ИГРАТЬ!", + gamesBackToGames: "Назад к играм", modulesGamesLabel: "Игры", modulesGamesDescription: "Модуль для открытия и игры в игры.", modulesGraphosLabel: "Графос", @@ -3149,6 +3324,8 @@ module.exports = { gamesArkanoidDesc: "Разбейте все кирпичи своей ракеткой и мячом. Классическая аркада.", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "Классический пинг-понг против ИИ. Первый до 5 очков побеждает.", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "Гонка со временем! Уворачивайтесь от машин и доберитесь до финиша вовремя.", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "Пилотируйте корабль через астероидное поле. Уничтожайте их до столкновения.", gamesRockPaperScissorsTitle: "Камень Ножницы Бумага", @@ -3169,6 +3346,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3179,6 +3358,12 @@ module.exports = { modulesCalendarsLabel: "Календари", modulesCalendarsDescription: "Модуль для просмотра и управления календарями.", typeCalendar: "КАЛЕНДАРИ", + calendarFilterAll: "ВСЕ", + calendarFilterMine: "МОИ", + calendarFilterOpen: "ОТКРЫТЫЕ", + calendarFilterClosed: "ЗАКРЫТЫЕ", + calendarFilterRecent: "НЕДАВНИЕ", + calendarFilterFavorites: "ИЗБРАННОЕ", calendarCreate: "Создать календарь", calendarUpdate: "Обновить", calendarDelete: "Удалить", @@ -3191,17 +3376,24 @@ module.exports = { calendarTagsLabel: "Теги", calendarTagsPlaceholder: "тег1, тег2...", calendarParticipantsLabel: "Участники", + calendarParticipantsCount: "Участники", + calendarVisitCalendar: "Открыть календарь", calendarCreated: "Создан", + calendarAuthor: "Автор", calendarJoin: "Присоединиться", calendarGenerateInvite: "Создать приглашение", + calendarInviteCodePlaceholder: "Введите код приглашения...", + calendarValidateInvite: "Проверить код", + calendarJoined: "Участник", + calendarAddDate: "Добавить дату", + calendarAddNote: "Добавить заметку", calendarDateLabel: "Дата", calendarDatePlaceholder: "Опишите эту дату...", - calendarNoteLabel: "Заметки", + calendarNoteLabel: "Заметка", calendarNotePlaceholder: "Добавить заметку...", calendarFirstDateLabel: "Дата", calendarFirstNoteLabel: "Заметки", calendarIntervalLabel: "Interval", - calendarIntervalNone: "Без повторения", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3212,6 +3404,8 @@ module.exports = { calendarsDescription: "Находите и управляйте календарями в вашей сети.", calendarMonthPrev: "← Назад", calendarMonthNext: "Вперёд →", + calendarMonthLabel: "Даты", + calendarsShareUrl: "Поделиться URL", calendarAllSectionTitle: "Календари", calendarRecentSectionTitle: "Недавние календари", calendarFavoritesSectionTitle: "Избранное", @@ -3225,6 +3419,7 @@ module.exports = { calendarRemoveFavorite: "Убрать из избранного", calendarSearchPlaceholder: "Поиск календарей...", calendarAddEntry: "Добавить запись", + calendarLeave: "Покинуть календарь", statsCalendar: "Календари", statsCalendarDate: "Даты календаря", statsCalendarNote: "Заметки календаря", @@ -3271,6 +3466,7 @@ module.exports = { torrentSortOldest: "Самые старые", torrentSortTop: "Самые популярные", torrentNoMatch: "Совпадений не найдено.", + torrentMessageAuthorButton: "Написать Автору", torrentUpdatedAt: "Обновлено", statsTorrent: "Торренты", typeTorrent: "ТОРРЕНТЫ", @@ -3278,18 +3474,10 @@ module.exports = { modulesTorrentsDescription: "Модуль для поиска и управления торрентами.", favoritesFilterTorrents: "ТОРРЕНТЫ", favoritesFilterMarket: "РЫНОК", - favoritesFilterEvents: "СОБЫТИЯ", - favoritesFilterForum: "ФОРУМ", - favoritesFilterHousing: "ЖИЛЬЁ", - favoritesFilterJobs: "ВАКАНСИИ", - favoritesFilterProjects: "ПРОЕКТЫ", - favoritesFilterReports: "ОТЧЁТЫ", - favoritesFilterTasks: "ЗАДАЧИ", - favoritesFilterTransfers: "ПЕРЕВОДЫ", - favoritesFilterVotes: "ГОЛОСОВАНИЯ", favoritesFilterShopProducts: "ТОВАРЫ МАГАЗИНА", tribeSectionTorrents: "ТОРРЕНТЫ", tribeCreateTorrent: "Загрузить Торрент", + tribeMediaTypeTorrent: "Торрент", settingsWishTitle: "Желание", settingsWishDesc: "Настройте желание вашего Аватара.", settingsWishWhole: "Multiverse", @@ -3300,10 +3488,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "Исходящие — защита UX. Входящие — фильтр внимания.", + pmBlockedNonMutual: "Личные сообщения можно отправлять только жителям со взаимной поддержкой.", inhabitantsPendingFollowsTitle: "Ожидающие запросы на поддержку", inhabitantsPendingAccept: "Принять", inhabitantsPendingReject: "Отклонить", + bxEncrypted: "ЗАШИФРОВАНО", + bxEncryptedHexLabel: "Шифротекст (превью)", tribeSectionGovernance: "УПРАВЛЕНИЕ", + tribeSubStatusPublic: "ПУБЛИЧНАЯ", + tribeSubStatusPrivate: "ЧАСТНАЯ", + tribeSubInheritedPrivate: "ЧАСТНАЯ (унаследовано от главного племени)", tribePadInviteRequired: "Нет доступа к паду. Запросите приглашение.", tribeChatInviteRequired: "Нет доступа к чату. Запросите приглашение.", tribeGovernanceDesc: "Внутреннее управление этим племенем.", @@ -3343,31 +3537,44 @@ module.exports = { tribeGovNoHistorical: "Пока нет ни одной записи", logsTitle: "Журнал", logsDescription: "Записывайте свой опыт в сети.", + logsReadMore: "Читать далее", logsViewTitle: "Журнал", logsColumnType: "Тип", logsView: "Смотреть", + logsManualPrompt: "Напишите ваш журнал", logsUpdateButton: "Обновить", - logsEditTitle: "Обновить запись", + logsEditTitle: "Редактировать запись", + logsCreateDescription: "Записывайте свой опыт...", logsCreateTitle: "Создать запись", logsWriteButton: "Написать", logsTextPlaceholder: "Опишите ваши впечатления...", + logsTextField: "Текст", + logsLabelPlaceholder: "Метка...", + logsLabelField: "Напишите ваш журнал (метка)", + logsAiDisabledWarn: "Включите модуль ИИ в /modules для записей от ИИ.", + logsAiContextValue: "День блокчейна", + logsAiContext: "Контекст", + logsAiModStatus: "AImod", + logsModeApply: "Применить!", logsModeAIWritten: "AI-Assistant", logsModeManual: "Вручную", logsModeAI: "ИИ", + logsColumnDelete: "Удалить", + logsColumnEdit: "Редактировать", logsColumnLog: "Журнал", logsColumnDate: "Дата", logsDelete: "Удалить", - logsEdit: "Обновить", + logsEdit: "Редактировать", logsFilterAlways: "ВСЕГДА", logsFilterYear: "ГОД", logsFilterMonth: "МЕСЯЦ", logsFilterWeek: "НЕДЕЛЯ", logsFilterToday: "СЕГОДНЯ", - logsComments: "Комментарии", - logsAuthorEntries: "Записи", + logsFilterRecent: "НЕДАВНИЕ", + logsFilterAll: "ВСЕ", logsCreate: "Создать запись", logsExport: "Экспорт журнала", - logsExportOne: "Экспортировать запись", + logsExportOne: "Экспорт", logsViewDetails: "Подробнее", logsGenerateButton: "Сгенерировать текст", logsSearchText: "Искать в журналах...", @@ -3377,25 +3584,31 @@ module.exports = { modulesLogsLabel: "Журнал", modulesLogsDescription: "Модуль для записи (с помощью ИИ-ассистента) ваших впечатлений.", statsLogsTitle: "Журнал", + statsLogsEntries: "Записи", typeLog: "ЖУРНАЛ", + blockchainCycle: "Цикл", larpTitle: "L.A.R.P.", larpDescription: "Слой ролевой игры в реальном времени для совместных экспериментов.", - larpNoHousesMatch: "Нет домов, соответствующих запросу.", - larpSearchPlaceholder: "Поиск домов...", larpAllHouses: "Дома:", larpFilterRuling: "ПРАВИТ", larpFilterHouses: "ДОМА", larpFilterRules: "FAQ", larpRulesTitle: "L.A.R.P. FAQ", + larpRulesEntryTitle: "Стартовый дом", larpRulesEntryText: "Каждый житель по умолчанию начинает в ACADEMIA. Из ACADEMIA выберите дом в панели «Вступить в дом»: это откроет вступительный тест. Ответьте на 10 вопросов и отправьте. Если вы преодолеете порог (7/10), вы будете автоматически приняты в этот дом; иначе вас отклонят, и вы останетесь в ACADEMIA. В любом случае система записывает ваш OasisID и цикл попытки и не даёт повторить попытку до окончания периода ожидания в 30 циклов.", + larpRulesLeaveText: "Участники любого дома могут вернуться в ACADEMIA в любое время со страницы своего дома; это сбрасывает их членство, но не отменяет ожидание теста.", + larpRulesGovernanceTitle: "Стена управления", larpRulesGovernanceText: "В свой цикл правящий дом носит значок «Правит» и является единственным, чья Стена показывается публично под вкладкой ПРАВИТ.", - larpRulesWallText: "У каждого дома есть Стена, где пишут его участники. Стена дома закрыта, кроме стены правящего дома и стены ACADEMIA, которые может читать любой.", + larpRulesSignTitle: "Эмблема L.A.R.P.", larpRulesSignText: "Жители могут включить (в /profile/edit > Сенсоры) показ изображения своего текущего дома как знака в профиле, на карточке автора и жителя. Знак ведёт на страницу дома.", larpPostsTitle: "Стена", larpPostsEmpty: "Пока нет публикаций.", + larpPostLabel: "Новая публикация", larpPostPlaceholder: "Что должен сказать этот дом?", + larpPostPublic: "Публичная (видно всем, иначе только участникам)", larpPostSubmit: "Опубликовать", + larpPostMembersOnly: "Только участники", larpCycleLabel: "Цикл:", audioBackToBcs: "Back to BCS", audioFilterBcs: "BCS", @@ -3512,9 +3725,12 @@ module.exports = { bankTaxesLookupNote: "Введите ID блока, чтобы найти его в вашем локальном канале.", bankTaxesUserNoteChipsClick: "Нажмите на любой чип, чтобы проверить его блок в blockexplorer.", mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntry: "Код приглашения", larpRulesInviteEntryText: "Участники домов могут создавать коды приглашения, дающие прямой доступ к дому.", larpRulesOneHouseText: "Вы можете быть участником не более одного дома в любой момент. Если ни в одном — вы в ACADEMIA. Каждая страница дома (кроме ACADEMIA) показывает участникам кнопку «Покинуть Дом», которая сбрасывает членство к ACADEMIA. Выход НЕ отменяет ожидание теста.", - larpRulesTestEntryText: "В тесте WILL каждый вариант даёт вес одному или нескольким домам; при отправке система суммирует баллы и автоматически принимает вас в дом с наибольшим счётом.", + larpRulesOneHouseTitle: "По одному дому за раз", + larpRulesTestEntry: "WILL-тест", + larpRulesTestEntryText: "Каждый вариант взвешивает один или несколько домов; при отправке система суммирует баллы и автоматически принимает вас в дом с наибольшим количеством баллов.", larpWillTestHint: "После завершения вам будет назначен дом, который лучше всего соответствует вашим ответам. Ваши индивидуальные ответы обрабатываются локально и никогда не сохраняются — в ваш канал публикуется только результат назначения дома.", larpWillTestTitle: "WILL-тест", settingsLanDesc: "Периодически анонсировать этот узел другим экземплярам Oasis в той же локальной сети. Отключите, чтобы остановить UDP-вещание.", @@ -3533,27 +3749,31 @@ module.exports = { aiExchangeMarkHelpful: "+1 полезно", larpCyclesUntilRuling: "Следующий цикл управления", larpVisitTribe: "Посетить племя", - larpStartJourney: "Начать путь", larpGoverning: "Правит:", - larpStartHere: "Начните здесь:", larpMyHouse: "Мой дом:", larpMembersCount: "Участники", + larpMembersTitle: "Участники", + larpNoMembers: "Пока нет участников.", larpRolesLabel: "Роли", larpFunctionLabel: "Функция", larpMonthLabel: "Цикл управления", larpLeaveToAcademia: "Вернуться в ACADEMIA", - larpAcademiaJoinTitle: "Дома L.A.R.P.", + larpAcademiaJoinTitle: "Вступить в дом", + larpAcademiaJoinHint: "Пройдите психологический тест профиля, чтобы вас отнесли к дому, который больше всего вам подходит, или используйте код приглашения, которым поделился участник одного из домов.", + larpTakeTestButton: "Пройти тест профиля", larpInviteRedeem: "Вступить в Дом", larpInviteCreate: "Создать код приглашения", larpInvitePlaceholder: "Код приглашения", larpInviteBannerTitle: "Новый код приглашения", larpInviteBannerHint: "Поделитесь этим кодом с кем-то в ACADEMIA. Срок действия — 30 циклов или одно использование.", + larpTestAssignedHouse: "Назначенный дом", larpTestRankingTitle: "Очки по домам", invitesHousesTitle: "L.A.R.P.", invitesHouseInviteCodePlaceholder: "Код приглашения дома", invitesHouseJoinButton: "Вступить в Дом", larpLastAttemptTitle: "Ваша последняя попытка", larpLastAttemptHouse: "Дом", + larpLastAttemptResult: "Результат", larpLastAttemptWhen: "Когда", larpLastAttemptCooldown: "Следующая попытка через", larpTestTitle: "Вступительный тест", @@ -3562,14 +3782,37 @@ module.exports = { larpTestCooldownActive: "Следующий тест доступен через", larpTestCooldownDays: "циклов", larpTestResultTitle: "Результат теста", + larpTestPassed: "Тест пройден — добро пожаловать!", + larpTestFailed: "Тест не пройден", larpTestScore: "Очки", larpTestNextAttempt: "Вы сможете пройти ещё один тест через 30 циклов.", larpGoToHouse: "Перейти в свой дом", larpBackToAcademia: "Назад в ACADEMIA", + larpBackToHouses: "◀ Дома", larpBadgeYou: "Вы", larpBadgeRuling: "Правит", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Модуль слоя ролевой игры в реальном времени Oasis (9 Домов).", + melodyUploadTitlePlaceholder: "Composition title", + melodyUploadDescPlaceholder: "Optional description", + ecoinTaxLabel: "ECOin Tax", + bankTaxesNetworkTitle: "Network Taxes", + bankTaxesUserTitle: "Your taxes", + bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", + bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", + bankTaxesExample: "Example", + bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", + statsEcoTaxTitle: "ECO Tax", + audioTranscodeTitle: "BCS Transcode", + audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", + audioTranscodeStegoTitle: "Embedded in the waveform", + audioBackToAudio: "Back to audio", + audioAuthorLabel: "Author", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + mapClickToCreate: "Click on the map to select a location", + mapClickToAddMarker: "Click on the map to add a marker", + gameScoreLabel: "GAME SCORES", larpProfileQ1: "Столкнувшись со сложной проблемой, что вы делаете в первую очередь?", larpProfileQ1O1: "Говорить с другими, чтобы найти консенсус", larpProfileQ1O2: "Создать прототип для проверки", diff --git a/src/client/assets/translations/oasis_zh.js b/src/client/assets/translations/oasis_zh.js index 4080916..18367fd 100644 --- a/src/client/assets/translations/oasis_zh.js +++ b/src/client/assets/translations/oasis_zh.js @@ -1,6 +1,27 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { zh: { + bbAdd: "添加", + bbQuickActions: "快捷操作", + bbPickTitle: "固定一个模块", + bbPickDesc: "为底部栏的 + 位置选择一个模块。", + bbPickNone: "无", + bbManage: "编辑", + bbYourBar: "你的工具栏", + bbRemove: "移除", + bbFull: "已达上限", + bbDone: "完成", + activityChangeFilter: "更改筛选", + emptyConnectHint: "连接到一个 pub 以与网络同步。", + emptyConnectCta: "连接到 pub", + menuCommunity: "社区", + activityGroupCommunity: "社区与治理", + activityGroupOrg: "组织", + activityGroupComm: "沟通", + activityGroupEconomy: "经济", + activityGroupMedia: "内容与媒体", + activityGroupOther: "其他", + languageName: "中文", shopOrderStatusPaid: "已收款", shopOrderStatusReceived: "已收到", shopOrderMarkPaid: "标记已收款", @@ -11,16 +32,20 @@ module.exports = { shopMyOrdersEmpty: "你还没有购买记录。", shopOrderPmSeller: "PM Seller", shopOrderSeller: "卖家", + shopOrderPaymentConcept: "付款", shopOrderInvoice: "发票", shopOrderInvoiceConcept: "发票", shopOrderStatusPending: "待处理", shopOrderStatusAccepted: "已接受", shopOrderStatusRejected: "已拒绝", shopOrderStatusShipped: "已发货", + shopOrderStatusDelivered: "已送达", shopOrderAccept: "接受", shopOrderReject: "拒绝", shopOrderMarkShipped: "标记为已发货", + shopOrderMarkDelivered: "标记为已送达", shopOrderPmBuyer: "PM Buyer", + shopOrderGenerateContract: "智能合约", shopOrderContractConcept: "商店订单", shopOrdersPending: "待处理订单", all: "全部", @@ -50,13 +75,14 @@ module.exports = { viewJson: "查看 JSON", matchScore: "匹配度", preferencesLabel: "偏好", - inhabitantProfileTitle: "居民", + inhabitantProfileTitle: "居民资料", contentWarningLabel: "内容警告", invalidPost: "无效内容", invalidPostHint: "此消息文本无效或为空。", spreadBy: "由", spreadChron: "传播", spreaded: "已传播", + spreadMore: "更多", spreadContentUnavailable: "内容尚不可用(等待同步)", filteredByTag: "按标签筛选", filteredByTagTitle: "按标签筛选", @@ -74,8 +100,10 @@ module.exports = { noSeverity: "无严重程度", calendarDeleteDate: "删除日期", calendarIntervalUntil: "直到", + bookmarkCategory: "分类", bookmarkLastVisit: "最近访问", profileClearnetBookmarksLabel: "书签", + forumFilterHot: "热门", invitesPort: "端口", currentlyUnrecheable: "错误!", peerHostValidation: "有效的 IPv4(如 192.168.1.100)或主机名(如 pub.example.com)", @@ -137,12 +165,6 @@ module.exports = { statsCarbonMaxAnnual: "年度最大估计", statsCarbonNetwork: "网络总计", statsCarbonOfEstMax: "占估计最大容量", - statsCarbonYearProgress: "年度已过", - statsNetworkTitle: "网络", - statsStorageTitle: "存储", - statsEcoTaxTitle: "ECO 税", - statsAccountTitle: "账户", - statsAveragesTitle: "平均值", statsCarbonOfNetwork: "占网络总计", statsCarbonUser: "你的足迹", statsContentCountColumn: "数量", @@ -154,6 +176,10 @@ module.exports = { statsTopTagsTitle: "热门标签", statsTopTypesTitle: "热门内容类型", statsTotalMsgs: "消息总数", + feedViewDetails: "查看详情", + eventFileLabel: "附加图片", + taskFileLabel: "附加图片", + courtsRespondentRequired: "需要被告ID", extended: "多元宇宙", extendedDescription: [ "当你支持某人时,你可能会下载他们所支持的居民的帖子,这些帖子会显示在这里,按时间倒序排列。", @@ -197,27 +223,36 @@ module.exports = { graphosShowing: "显示", graphosYou: "你", graphosTotalNodes: "节点总数", + manualMode: "手动模式", mentions: "提及", mentionsDescription: [ - strong("每一条 @提到你的消息"), - "。", + strong("@提及你的帖子"), + ",按时间倒序排列。", ], + nextPage: "下一页", + previousPage: "上一页", noMentions: "你还没有收到@提及。", - mentionsSearchPlaceholder: "搜索提及...", privateReminders: "提醒", inboxFiltersLabel: "筛选:", inboxToggleToMutuals: "切换为相互关注", inboxToggleToWhole: "切换为所有人", + privateFrom: "发件人", + privateTo: "收件人", privateDate: "日期", pmReply: "回复", pmReplies: "回复", + pmNew: "新消息", + pmMarkRead: "标记为已读", inReplyTo: "回复", pmPreview: "预览", pmPreviewTitle: "消息预览", peers: "节点", searchDescription: "描述", + imageSearch: "图片搜索", searchFromLabel: "从", searchToLabel: "到", + searchMinOpinionsLabel: "最少意见", + searchInhabitantLabel: "居民 (Oasis ID)", searchQueryLabel: "查询", searchPerPageLabel: "每页结果数", settings: "设置", @@ -230,39 +265,68 @@ module.exports = { melodyDescription: '播放你区块链的旋律 — 每个区块都会变成一个音符。', melodyEmpty: '还没有音符 — 你的区块链尚未产生任何区块。', melodyCompositionTitle: '作曲', + melodyCompositionHelp: '区块链中的每个区块都会根据其类型和大小变成一个音符。', + melodyStatsTitle: '类型分布', + melodyInhabitantLabel: '居民', melodyTotalBlocks: '音符', + melodyType: '类型', + melodyCount: '区块', melodyFilterMine: '我的', melodyFilterAll: '全部', + melodyFilterShare: '上传旋律', + melodyShareTitle: '分享你的旋律', + melodyShareHelp: '发布你当前旋律的快照,让别人收听和重新混音。', + melodyShareTitleLabel: '标题(可选)', + melodyShareTitlePlaceholder: '区块链幽灵', + melodyShareSubmit: '发布旋律', + melodyNoShared: '还没有区块链旋律。', melodyRegenerate: '重新生成', melodyDownload: "下载 BCS", + profileActivityTitle: '活动', + profileActivityMore: '查看所有活动', profileContentSectionTitle: '头像内容', profileContentHelp: '选择在你的头像中显示哪些模块。', profileSensorsHelp: '头像中显示的可选指标。', profileClearnetHelp: '可以从 Oasis 外部访问的模块。', profileHubAll: '全部', + profileHubTitle: '公开内容', + footerPulseTitle: '脉搏', + footerPulsePeers: '节点', + footerPulseInbox: '收件箱', + footerPulseSync: '上次同步', + footerPulseEcoValue: 'ECO/小时', + footerPulseLastActivity: '最近活动', footerLicenseLabel: '许可证', profileSwitchToOasis: '返回 Oasis', - profileVisibilityFediverse: "多元宇宙", + profileSwitchToClearnet: '进入 Clearnet', + profileVisibilityFediverse: "联邦宇宙", + profileSwitchToFediverse: "潜入联邦宇宙", + coordLabel: '坐标(例如 A3)', + coordPlaceholder: '输入坐标', contributorsTitle: "贡献者", + pixeliaBy: "作者", colorLabel: '选择颜色', paintButton: '画!', + invalidCoordinate: '坐标无效', + goToMuralButton: "查看壁画", totalPixels: '总像素数', modules: "模块", modulesViewTitle: "模块", modulesViewDescription: "通过启用或禁用模块来设置你的环境。", inbox: "收件箱", multiverse: "多元宇宙", - fediverse: "多元宇宙", - fediverseDescription: "管理你的其他多元宇宙账户,包括发送和接收内容。", + fediverse: "联邦宇宙", + fediverseDescription: "管理你的其他联邦宇宙账户,包括发送和接收内容。", fediverseStatus: "状态", - fediverseDisconnected: "多元宇宙已断开。请检查%LINK%或连接状态。", - fediverseSettingsTitle: "多元宇宙", + fediverseDisconnected: "联邦宇宙已断开。请检查%LINK%或连接状态。", + fediverseSettingsTitle: "联邦宇宙", fediverseInstanceLabel: "地址", fediverseTokenLabel: "访问令牌", fediverseTokenHelp: "设置你的访问令牌。它将用于在 Oasis 中管理你的 Mastodon 账户。", fediverseConnect: "连接", fediverseConnectedAs: "已连接为", fediverseDisconnect: "断开连接", + fediverseEmpty: "当你从%LINK%连接其他联邦宇宙账户后,便可在此管理它们。", fediverseEmptyLink: "设置", fediverseComposePlaceholder: "在想什么?", fediverseReplyPlaceholder: "写下你的回复…", @@ -271,15 +335,21 @@ module.exports = { fediverseReply: "回复", fediverseBoosted: "转嗷了", fediverseNoPosts: "你的时间线是空的。", + fediverseThread: "讨论串", fediverseTimeline: "时间线", + fediverseManageTimeline: "管理时间线", fediverseFollowers: "关注者", fediverseFollowing: "正在关注", fediversePosts: "嘟文", fediverseJoined: "加入于", + fediverseOverview: "概览", fediverseRefresh: "刷新", fediverseWrite: "撰写", fediversePreview: "预览", + fediverseEdit: "编辑", + fediverseOpenFeed: "查看信息流", fediverseManage: "管理", + fediverseStatusNotConnected: "未连接", fediverseError: "无法连接 Mastodon。", fediverseErrInstance: "实例地址无效。", fediverseErrAuth: "令牌无效或已过期。", @@ -290,6 +360,17 @@ module.exports = { fediverseErrPost: "无法发布。", fediverseErrMedia: "无法上传文件。", fediverseErrEmpty: "写点什么或添加文件。", + popularLabel: "精选", + topicsLabel: "话题", + latestLabel: "最新", + summariesLabel: "摘要", + threadsLabel: "讨论串", + multiverseLabel: "多元宇宙", + inboxLabel: "收件箱", + invitesLabel: "邀请", + walletLabel: "钱包", + legacyLabel: "密钥", + cipherLabel: "加密器", bookmarksLabel: "书签", videosLabel: "视频", torrentsLabel: "种子", @@ -300,14 +381,18 @@ module.exports = { inhabitantsLabel: "居民", trendingLabel: "热门", eventsLabel: "活动", + tasksLabel: "任务", apply: "应用", menuPersonal: "个人", + menuContent: "内容", + menuBlogs: "博客", menuGovernance: "治理", menuOffice: "办公", - menuNetwork: "社区", + menuMultiverse: "多元宇宙", + menuNetwork: "网络", menuCreative: "创意", menuEconomy: "经济", - menuMedia: "资料库", + menuMedia: "媒体", menuTools: "工具", comment: "评论", subtopic: "子话题", @@ -317,6 +402,13 @@ module.exports = { follow: "支持", block: "屏蔽", unblock: "取消屏蔽", + newerPosts: "更新的帖子", + olderPosts: "更早的帖子", + feedRangeEmpty: "该范围内没有内容。请尝试查看", + seeFullFeed: "完整动态", + feedEmpty: "Oasis 网络从未见过此账户的帖子。", + beginningOfFeed: "这是动态的起始位置", + noNewerPosts: "尚未收到更新的帖子。", relationshipNotFollowing: "你未被支持", relationshipTheyFollow: "支持你", relationshipMutuals: "互相支持", @@ -326,20 +418,25 @@ module.exports = { relationshipBlockedBy: "你被屏蔽了", relationshipMutualBlock: "互相屏蔽", relationshipNone: "你未支持", + relationshipConflict: "冲突", relationshipBlockingPost: "已屏蔽的帖子", viewLikes: "查看传播", spreadedDescription: "该居民传播的帖子列表。", + totalspreads: "总传播数", + attachFiles: "附加文件", preview: "预览", publish: "写作", contentWarningPlaceholder: "为帖子添加主题(可选)", - publishWarningPlaceholder: "正文上限 7000 个字符。", - publishTooLong: "你的帖子太长了,请缩短。", + privateWarningPlaceholder: "添加居民以发送私密帖子(可选)", + publishWarningPlaceholder: "...", publishCustomDescription: [ "提醒:由于区块链技术,帖子一旦发布就无法编辑或删除。", ], commentWarning: [ "提醒:由于区块链技术,帖子一旦发布就无法编辑或删除。", ], + commentPublic: "公开", + commentPrivate: "私密", commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -359,6 +456,7 @@ module.exports = { "。", ], publishCustom: "撰写高级帖子", + subtopicLabel: "创建此帖子的子话题", messagePreview: "帖子预览", mentionsMatching: "匹配的提及", mentionsName: "名称", @@ -381,93 +479,21 @@ module.exports = { ssbLogStream: "区块链", ssbLogStreamDescription: "配置区块链流的消息限制。", saveSettings: "保存设置", + exportTitle: "导出数据", exportDescription: "设置密码(至少32个字符)以加密你的密钥", exportDataTitle: "备份", exportDataDescription: "下载你的数据(不包含私钥!)", exportDataButton: "下载数据库", + pubWallet: "PUB 钱包", + pubWalletDescription: "设置 PUB 钱包 URL。这将用于 PUB 交易(包括 UBI)。", + pubWalletConfiguration: "保存配置", + importTitle: "导入数据", importDescription: "导入你的加密私钥以启用你的头像", + importAttach: "附加加密文件 (.enc)", + passwordLengthInfo: "密码至少需要32个字符。", passwordImport: "输入密码以解密将保存在系统主目录的数据(文件名:secret)", exportPasswordPlaceholder: "使用小写字母、大写字母、数字和符号", - fileInfo: "你的加密私钥将下载到你的设备(文件名:oasis.enc)", - developer: "开发", - devTitle: "开发", - welcomeBannerText: "欢迎来到 OASIS。按这几个步骤设置你的化身。", - aiSuggestionBanner: "建议:", - aiSuggestionsEnable: "AI 建议", - welcomeBannerAction: "开始设置!", - welcomeTitle: "欢迎", - welcomeStepGreetingAction: "发送", - welcomeJoinPubButton: "加入 pub", - welcomeStepLarpAction: "加入「学院」", - welcomeStepLarpText: "加入我们的实境角色扮演游戏。", - welcomeStepLarpTitle: "加入 L.A.R.P.", - welcomeStepGreetingTitle: "发送一句「Hello world!」", - welcomeStepGreetingText: "发一条消息,让其他居民能发现你。", - welcomeJoinPub: "加入", - welcomeDescription: "开始之前值得做的几件事。", - welcomeProgress: "已完成", - welcomeStepDone: "完成", - welcomeStepPending: "待办", - welcomeSkip: "暂不设置", - welcomeStepLanguageTitle: "选择你的语言", - welcomeStepLanguageText: "Oasis 支持多种语言,随时都可以更改。", - welcomeStepProfileTitle: "编辑你的资料", - welcomeStepProfileText: "取个名字、写段介绍并设置头像,让其他居民认得你。", - welcomeStepProfileAction: "保存资料", - welcomeStepFederationTitle: "加入主网络", - welcomeStepFederationText: "连接到我们的 pub,认识其他居民并开始同步。", - welcomeStepFederationAction: "前往邀请", - welcomeStepBackupTitle: "备份你的 ID", - welcomeStepBackupText: "你的身份就是本机上的一个密钥文件。", - welcomeStepBackupWarning: "一旦丢失,没有任何人能帮你找回。", - welcomeStepBackupAction: "立即备份!", - welcomeCompleteTitle: "全部就绪。", - welcomeCompleteText: "你的节点已经就绪。从这里开始,网络随着你关注的人一起成长。", - welcomeFindPeople: "寻找居民", - devFilterFiles: "文件", - devFilterSearch: "搜索", - devFilterModules: "模块", - devFilterTranslations: "翻译", - devFilterStyles: "样式", - devFilterTests: "测试", - devVersion: "版本", - devNodeVersion: "Node", - devStatsTests: "测试套件", - devParentFolder: "上级文件夹", - devOpenFolder: "打开", - devDescription: "浏览本节点上运行的源代码。", - devRoot: "根目录", - devSearchPlaceholder: "要在代码中查找的文本", - devAnyExtension: "任意文件", - devStatsFiles: "文件", - devStatsLines: "行数", - devStatsModules: "模块", - devStatsLanguages: "语言", - devNotViewable: "无法查看", - devEmptyFolder: "此文件夹为空。", - devSize: "大小", - devShowing: "显示", - devLine: "行", - devReportBug: "报告缺陷", - devCreateTask: "创建任务", - devRaw: "RAW", - devReportContext: "在阅读 Oasis 源代码时发现", - devTaskPrefix: "复查", - devPrevPage: "上一段", - devNextPage: "下一段", - devMatches: "个匹配", - devFilesScanned: "个文件已扫描", - devNoResults: "暂无匹配。", - devColModule: "模块", - devColState: "状态", - devColModel: "模型", - devColView: "视图", - devColRoutes: "路由", - devColTests: "测试", - devOn: "开", - devOff: "关", - modulesDevLabel: "开发", - modulesDevDescription: "用于管理 Oasis 源代码的模块。", + fileInfo: "你的加密私钥将保存在系统主目录(文件名:oasis.enc)", themeIntro: "选择一个主题。", setTheme: "设置主题", @@ -475,13 +501,16 @@ module.exports = { languageDescription: "如果你想使用其他语言,请在此选择。", setLanguage: "设置语言", + peerConnections: "节点", peerConnectionsIntro: "管理与其他节点的所有连接。", peerConnectionsTitle: "连接", online: "在线", offline: "离线", discovered: '已发现', unknown: '未知', + lanBroadcastLabel: "LAN 广播", lanBroadcastActive: "已启用(LAN 上的 UDP 组播)", + lanBroadcastDisabled: "已停用(pub 模式)", peerSourceRpc: "RPC", peerSourceGossip: "Gossip", peerSourceEbt: "EBT", @@ -494,6 +523,8 @@ module.exports = { noConnections: "没有已连接的节点。", noDiscovered: "没有已发现的节点。", noUnknownPeers: "好友图中没有未知节点。", + peersTechnicalTitle: "Peers (technical)", + peersTechnicalIntro: "Technical info from gossip.json + conn.json. Toggle the connection from here.", peersTechnicalEmpty: "No peers registered yet.", peerConnect: "Connect", peerDisconnect: "Disconnect", @@ -503,6 +534,9 @@ module.exports = { peerImport: "导入", peerImportTitle: "导入对等节点列表", peerImportPlaceholder: "每行粘贴一个 multiserver 地址,或上传 .txt 文件……", + noSupportedConnections: "没有已支持的节点。", + noBlockedConnections: "没有已屏蔽的节点。", + noRecommendedConnections: "没有推荐的节点。", connectionActionIntro: "", startNetworking: "启动网络", @@ -516,19 +550,9 @@ module.exports = { homePageDescription: "选择你想要作为主页的模块。", saveHomePage: "设置主页", invites: "邀请", + invitesTitle: "邀请", + invitesInvites: "邀请", invitesDescription: "管理和应用你网络中的邀请码。", - invitesPubsHint: "粘贴 pub 邀请码,与它建立联邦并开始复制其居民。", - invitesFederationsHint: "已联邦的 pub:刷新、移除无法连接的,或导出列表。", - invitesHousesHint: "输入房屋代码以加入 LARP 房屋并参与其经济。", - invitesTribesHint: "输入部落代码以成为成员并访问其私有内容。", - invitesChatsHint: "输入聊天代码以加入对话及其话题。", - invitesPadsHint: "输入协作页代码,与他人共同编辑同一份文档。", - invitesCalendarsHint: "输入日历代码以共享日期与提醒。", - invitesEventsHint: "输入活动代码以加入私密活动。", - invitesForumsHint: "输入论坛代码以在私密论坛中阅读与回复。", - invitesMapsHint: "输入地图代码,在共享地图上查看和添加地点。", - invitesShopsHint: "输入店铺代码以加入店铺及其目录。", - invitesInhabitantsHint: "输入居民代码以互相关注并交换内容。", invitesTribesTitle: "部落", invitesTribeInviteCodePlaceholder: "输入部落邀请码", invitesTribeJoinButton: "加入部落", @@ -559,6 +583,7 @@ module.exports = { invitesAlreadyFederated: "您已与该 pub 建立联邦。", invitesFederationsTitle: "联邦", invitesAcceptedInvites: "已联邦", + invitesNoInvites: "尚未接受任何邀请。", invitesUnfollow: "取消关注", invitesFollow: "关注", invitesUnfollowedInvites: "未联邦", @@ -578,24 +603,39 @@ module.exports = { panicMode: "紧急模式!", encryptData: "设置密码(至少32个字符)以加密你的区块链", decryptData: "输入密码以解密你的区块链", + panicModeDescription: "加密/解密或删除你的区块链", removeDataDescription: "警告:此操作不可撤销。", + encryptPanicButton: "加密区块链", + decryptPanicButton: "解密区块链", removePanicButton: "删除所有数据!", searchTitle: "搜索", searchDescriptionLabel: "在你的网络中搜索内容。", + searchLanguagesLabel: "语言", + searchSkillsLabel: "技能", searchPlaceholder:"搜索内容...", + searchDateLabel:"日期", searchLocationLabel:"位置", searchPriceLabel:"价格", + searchUrlLabel:"URL", searchCategoryLabel:"类别", + searchStartLabel:"开始时间", + searchEndLabel:"结束时间", searchPriorityLabel:"优先级", searchStatusLabel:"状态", statusLabel:"状态", + totalVotesLabel:"总票数", noResultsFound:"未找到结果。", votesOption:"投票选项", voteYesLabel:"赞成", voteNoLabel:"反对", allTypesLabel: "无限制", + ABSTENTIONLabel:"弃权", YESLabel:"赞成", NOLabel:"反对", + FOLLOW_MAJORITYLabel: "跟随多数", + CONFUSEDLabel: "困惑", + NOT_INTERESTEDLabel: "不感兴趣", + StatusLabel:"状态", votesOptionYesLabel:"赞成", votesOptionNoLabel:"反对", votesOptionAbstentionLabel:"弃权", @@ -603,6 +643,7 @@ module.exports = { votesCreatedByLabel:"创建者", voteStatusOpen:"进行中", voteStatusClosed:"已关闭", + imageSearchLabel: "输入关键词以搜索带有相应标签的图片。", commentDescription: ({ parentUrl }) => [ " 评论了 ", a({ href: parentUrl }, " 讨论串"), @@ -614,20 +655,53 @@ module.exports = { " 创建了子话题", ], subtopicTitle: ({ authorName }) => [`@${authorName} 帖子的子话题`], + mysteryDescription: "发布了一篇神秘的帖子", oasisDescription: "OASIS 项目网络", searchSubmit: "搜索...", + postLabel: "帖子", + aboutLabel: "居民", + feedLabel: "动态", votesLabel: "投票", + reportLabel: "报告", + imageLabel: "图片", + videoLabel: "视频", + audioLabel: "音频", + documentLabel: "文档", + torrentLabel: "种子", pdfFallbackLabel: "PDF 文档", + eventLabel: "活动", + taskLabel: "任务", + transferLabel: "转账", + curriculumLabel: "简历", + bookmarkLabel: "书签", + tribeLabel: "部落", + marketLabel: "市场", shopLabel: "SHOPS", + shopProductLabel: "SHOP PRODUCTS", + mapLabel: "MAPS", + jobLabel: "JOBS", + forumLabel: "FORUMS", + projectLabel: "PROJECTS", + bankWalletLabel: "钱包", + bankClaimLabel: "UBI CLAIMS", voteLabel: "VOTES", + contactLabel: "CONTACTS", + pubLabel: "PUBS", submit: "提交", + subjectLabel: "主题", editProfile: "编辑头像", + profileQrAlt: "个人资料二维码", + profileQrHint: "扫码以复制此 Oasis ID。", oasisIdLabel: "OasisID", - spreadContent: "复制", + spreadLabel: "转发", + spreadHint: "转发给你的支持者(通过你的 feed 复制)。", welcomePmSubject: "Hello World!", - welcomePmBody: "欢迎来到 Oasis — 一个自由、点对点、联邦化且加密的社交网络,采用仅限邀请的分布式架构。\n\n一些值得开始探索的地方:\n\n- /profile —— 编辑你的头像、名字、简介以及在公开侧显示哪些模块。\n- /invites —— 添加一个 pub,与更广泛的网络联邦。\n- /peers —— 查看谁已连接,重新扫描局域网,导出或导入对等节点列表。\n- /inhabitants —— 发现并访问其他人的头像。\n- /tribes —— 创建或加入端到端加密的私人小组。\n- /inbox —— 阅读和发送私信。\n- /activity —— 查看最近的活动,你的和你网络中的。\n- /publish —— 写一篇帖子,或分享图片、音频、视频、文档。\n- /search — 按类型搜索网络内容。\n\n一些值得连接的地方:\n\n- /multiverse — 管理你的其他多元宇宙账户,包括发送和接收内容。\n\n这条消息是私下从你发给你自己的,因此无需任何连接即可接收。", + welcomePmBody: "欢迎来到 Oasis — 一个自由、点对点、联邦化且加密的社交网络,采用仅限邀请的分布式架构。\n\n一些值得开始探索的地方:\n\n- /profile —— 编辑你的头像、名字、简介以及在公开侧显示哪些模块。\n- /invites —— 添加一个 pub,与更广泛的网络联邦。\n- /peers —— 查看谁已连接,重新扫描局域网,导出或导入对等节点列表。\n- /inhabitants —— 发现并访问其他人的头像。\n- /tribes —— 创建或加入端到端加密的私人小组。\n- /inbox —— 阅读和发送私信。\n- /activity —— 查看最近的活动,你的和你网络中的。\n- /publish —— 写一篇帖子,或分享图片、音频、视频、文档。\n\n一些值得连接的地方:\n\n- /fediverse — 管理你的其他联邦宇宙账户,包括发送和接收内容。\n\n这条消息是私下从你发给你自己的,因此无需任何连接即可接收。", + profileVisibilityTitle: "公开资料可见性", + profileVisibilityHint: "选择其他居民在你的资料中可见的字段。默认仅显示 Karma。", profileSensorsSectionTitle: "传感器", profileVisibilityActivity: "活跃度", + profileVisibilityDevice: "设备", profileDeviceLockedHint: "此传感器始终开启:它让其他人看到你从哪个设备连接,并且无法关闭。", profileVisibilityKarma: "KARMA 评分", profileVisibilityUbi: "UBI", @@ -635,6 +709,7 @@ module.exports = { profileVisibilityEcoTax: "ECO Tax", profileVisibilityLarpSign: "L.A.R.P. Sign", profileVisibilityGpg: "GPG 密钥", + profileVisibilityClearnet: "发布我的资料", clearnetSectionTitle: "Clearnet", profileClearnetShopsLabel: "店铺", profileClearnetJobsLabel: "工作", @@ -688,6 +763,7 @@ module.exports = { " 或连接状态。", ], walletSentToLine: ({ destination, amount }) => `已发送 ECO ${amount} 至 ${destination}`, + walletSettingsTitle: "钱包", walletSettingsDescription: "将 Oasis 与你的 ECOin 钱包集成。", walletSettingsDocLink: "ECOin 安装指南", walletStatusMessages: { @@ -709,19 +785,37 @@ module.exports = { cipherTitle: "加密器", cipherDescription: "使用共享密码对文本进行对称加密和解密。", randomPassword: "随机密码", + cipherEncryptTitle: "加密文本", + cipherEncryptDescription: "输入要加密的文本", + cipherTextLabel: "要加密的文本", cipherTextPlaceholder: "输入要加密的文本...", cipherPasswordLabel: "设置密码(至少32个字符)以加密你的文本", cipherPasswordDecryptLabel: "设置密码(至少32个字符)以解密你的文本", cipherPasswordPlaceholder: "输入密码...", cipherEncryptButton: "加密", + cipherDecryptTitle: "解密文本", + cipherDecryptDescription: "输入要解密的文本", cipherEncryptedMessageLabel: "加密文本", cipherDecryptedMessageLabel: "解密文本", + cipherPasswordUsedLabel: "加密所用密码(请妥善保管!)", cipherEncryptedTextPlaceholder: "输入加密文本...", + cipherIvLabel: "IV", + cipherIvPlaceholder: "输入初始化向量...", cipherDecryptButton: "解密", password: "密码", text: "文本", encryptedText: "加密文本", iv: "初始化向量 (IV)", + encryptTitle: "加密你的文本", + encryptDescription: "输入要加密的文本并提供密码。", + encryptButton: "加密", + decryptTitle: "解密你的文本", + decryptDescription: "输入加密文本并提供相同的加密密码。", + decryptButton: "解密", + passwordLengthError: "密码至少需要32个字符。", + missingFieldsError: "未提供文本、密码或 IV。", + encryptionError: "加密文本时出错。", + decryptionError: "解密文本时出错。", bookmarkTitle: "书签", bookmarkDescription: "发现和管理你网络中的书签。", bookmarkAllSectionTitle: "书签", @@ -747,6 +841,8 @@ module.exports = { bookmarkDescriptionPlaceholder: "可选", bookmarkTagsLabel: "标签", bookmarkTagsPlaceholder: "输入标签,用逗号分隔", + bookmarkCategoryLabel: "类别", + bookmarkCategoryPlaceholder: "可选", bookmarkLastVisitLabel: "上次访问", bookmarkSearchPlaceholder: "搜索 URL、标签、类别、作者...", bookmarkSortRecent: "最近", @@ -761,6 +857,8 @@ module.exports = { noLastVisit: "无访问记录", videoTitle: "视频", videoDescription: "探索和管理你网络中的视频内容。", + videoPluginTitle: "标题", + videoPluginDescription: "描述", videoMineSectionTitle: "你的视频", videoCreateSectionTitle: "上传视频", videoUpdateSectionTitle: "更新视频", @@ -792,6 +890,7 @@ module.exports = { videoSortOldest: "最早", videoSortTop: "最多投票", videoSearchButton: "搜索", + videoMessageAuthorButton: "私信", videoUpdatedAt: "已更新", videoNoMatch: "没有匹配的视频。", documentTitle: "文档", @@ -832,6 +931,8 @@ module.exports = { documentUpdatedAt: "已更新", audioTitle: "音频", audioDescription: "探索和管理你网络中的音频内容。", + audioPluginTitle: "标题", + audioPluginDescription: "描述", audioMineSectionTitle: "你的音频", audioCreateSectionTitle: "上传音频", audioUpdateSectionTitle: "更新音频", @@ -842,6 +943,7 @@ module.exports = { audioFilterMine: "我的", audioFilterRecent: "最近", audioFilterTop: "热门", + audioFilterBlockchain: "区块链", audioCreateButton: "上传音频", audioUpdateButton: "更新", audioDeleteButton: "删除", @@ -868,7 +970,6 @@ module.exports = { audioFilterFavorites: "收藏", favoritesTitle: "收藏", favoritesDescription: "所有你收藏的内容集中在一处。", - favoritesSearchPlaceholder: "搜索收藏...", favoritesFilterAll: "全部", favoritesFilterRecent: "最近", favoritesFilterAudios: "音频", @@ -884,13 +985,18 @@ module.exports = { favoritesNoItems: "还没有收藏。", yourContacts: "你的联系人", allInhabitants: "居民", - allCVs: "简历", + allCVs: "所有简历", discoverPeople: "发现你网络中的居民。", + allInhabitantsButton: "全部", + contactsButton: "支持", + CVsButton: "简历", + matchSkills: "匹配技能", + matchSkillsButton: "匹配技能", + suggestedButton: "推荐", searchInhabitantsPlaceholder: "按名称筛选居民...", filterLocation: "按位置筛选居民...", filterLanguage: "按语言筛选居民...", filterSkills: "按技能筛选居民...", - cvSkillsCount: "技能", applyFilters: "应用筛选", locationLabel: "位置", languagesLabel: "语言", @@ -899,18 +1005,17 @@ module.exports = { mutualFollowers: "共同关注者", suggestedFollowsYou: "关注了你", latestInteractions: "最近互动", + viewAvatar: "查看头像", + viewCV: "查看简历", suggestedSectionTitle: "推荐", topkarmaSectionTitle: "最高因缘值", topecoSectionTitle: "最佳生态", topactivitySectionTitle: "最高活跃度", - "TOP INACTIVITYButton": "最不活跃", - topinactivitySectionTitle: "最不活跃的居民", blockedSectionTitle: "已屏蔽", gallerySectionTitle: "画廊", - topSectionTitle: "排行", + blockedButton: "已屏蔽", blockedLabel: "已屏蔽用户", inhabitantviewDetails: "查看详情", - cvVisitButton: "查看简历", keepReading: "继续阅读...", oasisId: "ID", noInhabitantsFound: "尚未找到居民。", @@ -923,9 +1028,8 @@ module.exports = { parliamentFilterCandidatures: "候选", parliamentFilterProposals: "提案", parliamentFilterLaws: "法律", - parliamentFilterRevocations: "罢免", parliamentFilterHistorical: "历史", - parliamentFilterLeaders: "领导", + parliamentFilterLeaders: "领袖", parliamentFilterRules: "规则", parliamentGovernmentCard: "现任政府", parliamentActorInPowerInhabitant: '执政居民', @@ -949,8 +1053,10 @@ module.exports = { parliamentPoliciesDeclined: "否决的法律", parliamentPoliciesDiscarded: "废弃的法律", parliamentEfficiency: "% 效率", + parliamentFilterRevocations: '撤销', parliamentRevocationFormTitle: '撤销法律', parliamentRevocationLaw: '法律', + parliamentRevocationTitle: '标题', parliamentRevocationReasons: '理由', parliamentRevocationPublish: '发布撤销', parliamentCurrentRevocationsTitle: '当前撤销', @@ -963,12 +1069,23 @@ module.exports = { parliamentNoGovernments: "还没有政府。", parliamentCandidatureFormTitle: "提议候选", parliamentCandidatureIdPh: "Oasis ID (@...) 或部落名称", + parliamentCandidatureSlogan: "口号(最多140字)", + parliamentCandidatureSloganPh: "一个简短的口号", parliamentCandidatureMethod: "方式", parliamentCandidatureProposeBtn: "发布候选", + parliamentThDate: "提议日期", + parliamentThSlogan: "口号", + parliamentThSince: "注册时间", + parliamentThVotes: "获得票数", + parliamentThVoteAction: "投票", + parliamentTypeUser: "居民", + parliamentTypeTribe: "部落", parliamentVoteBtn: "投票", parliamentProposalFormTitle: "提议法律", parliamentProposalDescription: "描述(不超过1000字)", parliamentProposalPublish: "发布提案", + parliamentFinalize: "结束", + parliamentDeadline: "截止日期", parliamentNoProposals: "还没有法律提案。", parliamentNoLaws: "还没有已通过的法律。", parliamentMethodDEMOCRACY: "民主", @@ -986,21 +1103,29 @@ module.exports = { parliamentVotesSlashTotal: "票数/总计", parliamentVotesNeeded: "所需票数", parliamentFutureLawsTitle: "未来法律", + parliamentNoFutureLaws: "还没有未来法律。", parliamentVoteAction: "投票", + parliamentLeadersTitle: "领袖", parliamentThLeader: "头像", parliamentPopulation: "人口", + parliamentThType: "类型", + parliamentThInPower: "执政中", parliamentThPresented: "候选", parliamentNoLeaders: "还没有领袖。", parliamentCandidaturesListTitle: "候选列表", parliamentTimeRemaining: "剩余时间", + parliamentNoLeader: "尚无领先候选。", parliamentElectionsStatusTitle: "下届政府", parliamentProposalDeadlineLabel: "截止日期", parliamentProposalTimeLeft: "剩余时间", + parliamentProposalOnTrack: "有望通过", + parliamentProposalOffTrack: "支持不足", parliamentRulesTitle: "议会运作方式", parliamentRulesIntro: "选举每2个月进行一次;候选是持续的,在政府选定后重置。", parliamentRulesCandidates: "任何居民都可以提名自己、其他居民或任何部落。每位居民每个周期最多可提出3项候选;同一周期内的重复候选将被拒绝。", parliamentRulesElection: "获胜者是在结算时获得最多选票的候选。", parliamentRulesTies: "平局规则:最高居民 Karma;如果仍然平局,最早注册的;如果仍然平局,最早提出的;然后按 ID 字典序。", + parliamentRulesFallback: "如果没有人投票,最后提出的候选获胜。如果没有候选,将随机选择一个部落。", parliamentRulesTerm: "政府标签显示当前政府和统计数据。", parliamentRulesMethods: "形式:无政府(简单多数)、民主(50%+1)、多数(80%)、少数(20%)、Karma 制(最高 Karma 提案)、独裁(即时通过)。", parliamentRulesAnarchy: "无政府是默认模式:如果在结算时没有候选被选出,宣布无政府状态。在无政府状态下,任何居民都可以提议法律。", @@ -1022,7 +1147,11 @@ module.exports = { courtsCaseFormTitle: "开立案件", courtsCaseRespondent: "被告/答辩人", courtsCaseRespondentPh: "Oasis ID (@...) 或部落名称", + courtsCaseMediatorsAccuser: "调解员(原告方)", courtsCaseMethod: "解决方式", + courtsCaseDescription: "描述(最多1000字)", + courtsCaseEvidenceTitle: "案件证据", + courtsCaseEvidenceHelp: "附加图片、音频、文档(PDF)或视频以支持你的案件。", courtsCaseSubmit: "提交案件", courtsNominateJudge: "提名法官", courtsJudgeId: "法官", @@ -1067,6 +1196,22 @@ module.exports = { courtsRulesMisconduct: "骚扰、操纵或伪造证据可能导致立即不利裁决。", courtsRulesGlossary: "案件:冲突记录。证据:支持诉求的材料。裁决:附有补救措施的决定。上诉:请求审查裁决。", courtsFilterActions: "操作", + courtsNoActions: "你的角色没有待处理的操作。", + courtsCaseTitlePlaceholder: "冲突的简要描述", + courtsCaseSeverity: "严重程度", + courtsCaseSeverityNone: "无严重程度标签", + courtsCaseSeverityLOW: "低", + courtsCaseSeverityMEDIUM: "中", + courtsCaseSeverityHIGH: "高", + courtsCaseSeverityCRITICAL: "严重", + courtsCaseSubject: "主题", + courtsCaseSubjectNone: "无主题标签", + courtsCaseSubjectBEHAVIOUR: "行为", + courtsCaseSubjectCONTENT: "内容", + courtsCaseSubjectGOVERNANCE: "治理/规则", + courtsCaseSubjectFINANCIAL: "财务/资源", + courtsCaseSubjectOTHER: "其他", + courtsHiddenRespondent: "已隐藏(仅相关角色可见)。", courtsThRole: "角色", courtsRoleAccuser: "原告", courtsRoleDefence: "辩护方", @@ -1077,19 +1222,51 @@ module.exports = { courtsAssignJudgeBtn: "选择法官", trendingTitle: "热门", exploreTrending: "探索你网络中最受欢迎的内容。", + bookmarkButton: "书签", + transferButton: "转账", + eventButton: "活动", + taskButton: "任务", votesButton: "投票", - shopProductButton: "商店", + reportButton: "报告", + feedButton: "动态", + marketButton: "市场", + imageButton: "图片", + audioButton: "音频", + videoButton: "视频", + documentButton: "文档", + torrentButton: "种子", + noTrendingFound: "未找到热门内容。", + noContentMessage: "暂无热门内容。", + trendingDescription: "描述", + trendingDate: "日期", + trendingLocation: "位置", + trendingPrice: "价格", + trendingUrl: "URL", + trendingCategory: "类别", + trendingStart: "开始", + trendingEnd: "结束", + trendingPriority: "优先级", + trendingStatus: "状态", + trendingFrom: "发件人", + trendingTo: "收件人", + trendingConcept: "概念", + trendingAmount: "金额", + trendingDeadline: "截止日期", + trendingItemStatus: "项目状态", + trendingTotalVotes: "总票数", trendingTotalOpinions: "总观点数", - trendingNoContentMessage: "暂时还没有热门内容。", + trendingNoContentMessage: "暂无热门内容。", + trendingAuthor: "作者", + trendingCreatedAtLabel: "创建于", trendingTotalCount: "总计", tasksTitle: "任务", tasksDescription: "发现和管理你网络中的任务。", - taskSearchPlaceholder: "搜索任务...", taskTitleLabel: "标题", taskDescriptionLabel: "描述", taskStartTimeLabel: "开始时间", taskEndTimeLabel: "结束时间", taskPriorityLabel: "优先级", + taskPrioritySelect: "选择优先级", taskPriorityUrgent: "紧急", taskPriorityHigh: "高", taskPriorityMedium: "中", @@ -1099,13 +1276,13 @@ module.exports = { taskVisibilityLabel: "可见性", taskPublic: "公开", taskPrivate: "私密", + taskCreatedAt: "创建于", + taskBy: "作者", taskStatus: "状态", taskStatusOpen: "开放", taskStatusInProgress: "进行中", taskStatusClosed: "已关闭", taskAssignedTo: "分配给", - taskAssignedChip: "已认领", - taskUnassignedChip: "未认领", taskAssignees: "被分配者", taskAssignButton: "分配给我", taskUnassignButton: "取消分配", @@ -1118,6 +1295,7 @@ module.exports = { taskFilterInProgress: "进行中", taskFilterClosed: "已关闭", taskFilterAssigned: "已分配", + taskFilterArchived: "已归档", taskFilterUrgent: "紧急", taskFilterHigh: "高", taskFilterMedium: "中", @@ -1130,21 +1308,23 @@ module.exports = { taskInProgressTitle: "进行中的任务", taskClosedTitle: "已关闭的任务", taskAssignedTitle: "已分配的任务", + taskArchivedTitle: "已归档的任务", + taskPublicTitle: "公开任务", + taskPrivateTitle: "私密任务", notasks: "没有可用的任务。", noLocation: "未指定位置", taskSetStatus: "设置状态", + eventTitle: "活动", eventDateLabel: "日期", - eventRecurrenceLabel: "重复", - eventRecurrenceUntil: "重复至", - eventRecurrenceHint: "一次性活动请留空结束日期。", - eventUpcomingDates: "接下来的日期", eventsTitle: "活动", eventsDescription: "发现和管理你网络中的活动。", - eventSearchPlaceholder: "搜索活动...", + eventDescription: "描述", eventPrice: "价格", eventStatus: "状态", + eventOrganizer: "组织者", eventAllSectionTitle: "活动", eventMineSectionTitle: "你的活动", + eventArchivedTitle: "已归档的活动", eventCreateSectionTitle: "创建活动", eventUpdateSectionTitle: "更新活动", eventDeleteButton: "删除", @@ -1152,26 +1332,11 @@ module.exports = { eventAddToCalendar: "添加到日历", eventVisitCalendar: "查看日历", viewTask: "查看任务", - viewEvent: "查看活动", - viewPad: "查看协作文档", - viewMap: "查看地图", - viewCalendar: "查看日历", viewReport: "查看报告", viewTransfer: "查看转账", viewItem: "查看物品", viewShop: "查看商店", viewProject: "查看项目", - viewJob: "查看工作", - viewPlace: "查看地点", - generatePdf: "生成 PDF", - sharePm: "通过私信分享", - galleryImages: "图片(每张最大 50MB)", - galleryPhotos: "图片", - galleryAddPhoto: "添加图片", - galleryRemovePhoto: "移除", - galleryVideo: "视频(最大 50MB)", - galleryAddVideo: "添加视频", - galleryRemoveVideo: "移除视频", viewVotation: "查看投票", voteCastTitle: "投票", voteResults: "结果", @@ -1179,15 +1344,29 @@ module.exports = { eventDescriptionLabel: "描述", eventDescriptionPlaceholder: "输入活动描述...", eventUpdateButton: "更新", + eventAttendeesLabel: "参与者", + eventAttendeesPlaceholder: "输入参与者,用逗号分隔...", eventTagsLabel: "标签", + eventTagsPlaceholder: "输入活动标签,用逗号分隔...", + eventTags: "标签", eventPriceLabel: "价格", eventUrlLabel: "URL", eventAttendees: "参与者", + noAttendees: "暂无参与者", + eventCreatedAt: "创建于", eventLocation: "位置", eventLocationLabel: "位置", + eventNoLocation: "未指定位置", + eventNoURL: "未指定 URL", + eventBy: "作者", noevents: "没有可用的活动。", eventDate: "日期", + eventDateFormat: "YYYY年MM月DD日 HH:mm", eventAttendButton: "参加活动", + eventUnattendButton: "取消参加", + eventCreatedBy: "创建者", + eventAttendeesCount: "参与人数", + eventCreatedByYou: "你创建了此活动", eventFilterAll: "全部", eventFilterMine: "我的", eventFilterToday: "今天", @@ -1195,9 +1374,18 @@ module.exports = { eventFilterMonth: "本月", eventFilterYear: "今年", eventFilterArchived: "已归档", + eventTodayTitle: "今天的活动", + eventThisWeekTitle: "本周的活动", + eventThisMonthTitle: "本月的活动", + eventThisYearTitle: "今年的活动", eventPrivacyLabel: "可见性", eventPublic: "公开", eventPrivate: "私密", + eventPublicTitle: "公开活动", + eventNoPrice: "免费活动", + eventNoImage: "未上传图片", + eventAttendConfirmation: "你现在正在参加此活动", + eventUnattendConfirmation: "你已取消参加此活动", eventAttended: "已参加", eventUnattended: "未参加", eventStatusOpen: "开放", @@ -1208,14 +1396,9 @@ module.exports = { tagsTopSectionTitle: "热门标签", tagsCloudSectionTitle: "标签云", tagsFilterAll: "全部", - tagsFilterMine: "我的", - tagsFilterRecent: "最新", - tagsMineSectionTitle: "我的标签", - tagsRecentSectionTitle: "最新标签", tagsFilterTop: "热门", tagsFilterCloud: "标签云", tagsNoItems: "没有可用的标签。", - tagsSearchPlaceholder: "搜索标签...", tagsTableHeaderTag: "标签/链接:", tagsTableHeaderCount: "计数:", transfersTitle: "转账", @@ -1255,12 +1438,18 @@ module.exports = { transfersDeadline: "截止日期", transfersTags: "标签", transfersStatus: "状态", + transfersStatusUnconfirmed: "未确认", + transfersStatusClosed: "已关闭", + transfersStatusDiscarded: "已废弃", + transfersCreatedAt: "创建于", transfersConfirmations: "确认", - transfersExportContract: "生成账单", + transfersContainingBlock: "所在区块", + transfersExportContract: "创建合约", transfersConfirmButton: "确认转账", transfersNoItems: "未找到转账。", transfersMineSectionTitle: "你的转账", transfersUBISectionTitle: "UBI 转账", + transfersMarketSectionTitle: "市场转账", transfersTopSectionTitle: "热门转账", transfersPendingSectionTitle: "待处理转账", transfersUnconfirmedSectionTitle: "未确认转账", @@ -1268,13 +1457,21 @@ module.exports = { transfersDiscardedSectionTitle: "已废弃转账", transfersCreateSectionTitle: "创建转账", transfersAllSectionTitle: "转账", + transfersFilterFavs: "收藏", + transfersFavsSectionTitle: "收藏的转账", + transfersSearchLabel: "搜索", transfersSearchPlaceholder: "搜索概念、标签、用户...", transfersMinAmountLabel: "最低金额", transfersMaxAmountLabel: "最高金额", + transfersSortLabel: "排序", transfersSortRecent: "最近", transfersSortAmount: "最高金额", transfersSortDeadline: "最近截止", transfersSearchButton: "搜索", + transfersFavoriteButton: "收藏", + transfersUnfavoriteButton: "取消收藏", + transfersMessageUserButton: "消息", + transfersExpiringSoonBadge: "即将到期", transfersExpiredBadge: "已过期", transfersUpdatedAt: "已更新", transfersNoMatch: "没有匹配的转账。", @@ -1287,9 +1484,6 @@ module.exports = { voteClosedTitle: "已关闭的投票", voteAllSectionTitle: "投票", voteCreateButton: "创建投票", - voteErrorTitle: "请检查表单", - voteErrorDeadlineMin: "截止时间至少要在 {days} 天之后。", - voteErrorGeneric: "无法保存投票。", voteUpdateButton: "更新", voteDeleteButton: "删除", voteOptionYes: "赞成", @@ -1319,6 +1513,16 @@ module.exports = { voteNoQuestion: "未提供问题", voteUnknownCreator: "未知创建者", voteUnknownDate: "未知日期", + errorVoteNotFound: "未找到投票", + errorAlreadyVoted: "你已经表达了意见。", + errorVoteClosedCannotEdit: "无法编辑已关闭的投票", + errorVoteDeadlinePassed: "此投票的截止日期已过", + errorRetrievingVote: "获取投票时出错", + errorCreatingVote: "创建投票时出错", + errorVoteAlreadyVoted: "意见提交后无法编辑", + errorDeletingOldVote: "删除旧意见时出错", + errorCreatingUpdatedVote: "创建更新的意见时出错", + errorCreatingTombstone: "创建墓碑时出错", voteDetailSectionTitle: '投票详情', voteCommentsLabel: '评论', voteCommentsForumButton: '公开讨论', @@ -1328,6 +1532,7 @@ module.exports = { voteNewCommentButton: '发表评论', voteNewCommentLabel: '添加评论', cvTitle: "简历", + cvLabel: "简历 (CV)", cvEditSectionTitle: "编辑简历", cvCreateSectionTitle: "创建简历", cvDescription: "管理和分享你的专业技能和信息。", @@ -1346,121 +1551,29 @@ module.exports = { cvLocationLabel: "位置", cvStatusLabel: "状态", cvPreferencesLabel: "偏好", + cvOasisContributorLabel: "Oasis 贡献者", cvPersonal: "个人", cvOasis: "Oasis 贡献者(可选)", - cvOasisContributorView: "贡献", + cvOasisContributorView: "Oasis 贡献", cvEducational: "教育(可选)", cvEducationalView: "教育", cvProfessional: "专业(可选)", cvProfessionalView: "专业", cvAvailability: "可用性(可选)", + cvAvailabilityView: "可用性", cvUpdateButton: "更新", cvCreateButton: "创建简历", + cvContactLabel: "联系方式", cvCreatedAt: "创建于", cvUpdatedAt: "更新于", cvEditButton: "更新", cvDeleteButton: "删除", cvNoCV: "未找到简历。", blogSubject: "主题", - blogMessage: "消息", + blogMessage: "正文限制为 8096 个字符。", blogImage: "上传媒体(最大:50MB)", - blogMedia: "附件(最多 8 张图片和 1 个视频)", blogPublish: "预览", - pollsTitle: "投票", - dataTitle: "匹配", - dataDescription: "探索并可视化你的网络中的匹配。", - dataFilterAll: "全部", - dataFilterMine: "我的", - dataFilterRecent: "最近", - dataFilterTop: "最高", - dataKindInhabitants: "居民", - dataKindJobs: "工作", - dataKindProjects: "项目", - dataKindEvents: "活动", - dataKindTribes: "部落", - dataKindMarket: "市场", - dataKindHousing: "住房", - dataKindIndustry: "产业", - dataKindTasks: "任务", - dataKindReports: "报告", - dataKindVotes: "投票", - dataSearchPlaceholder: "搜索交叉数据...", - dataCohesionTitle: "凝聚系数 (CC)", - dataCcShort: "CC", - dataCohesionHint: "网络已发布内容之间的平均相近度。", - dataAffinity: "匹配度", - dataCommonTerms: "关键词", - dataStatPeople: "简历", - dataStatConnected: "已连接", - dataStatSkills: "技能", - dataBestMatch: "最佳匹配", - dataStatIsolated: "孤立", - dataStatEntities: "已比较的实体", - dataStatTerms: "不同词条", - dataStatPairs: "已连接的配对", - dataMatchesTitle: "匹配", - dataMatchesHint: "算法的个性化推荐。", - dataTermsTitle: "关键词", - dataTermsHint: "出现在最多内容中的词条。", - dataConnections: "关联匹配", - dataTopicTitle: "与此相关的一切:", - dataTopicHint: "算法与该主题关联的内容", - dataNoMatches: "没有可显示的交叉数据。", - dataNoProfile: "发布内容或填写简历,让算法了解你的兴趣。", - pollsDescription: "用于向网络提问并统计答案的模块。", - pollFilterAll: "全部", - pollFilterMine: "我的", - pollFilterRecent: "最近", - pollFilterTop: "热门", - pollFilterVoted: "已投票", - pollFilterOpen: "进行中", - pollFilterClosed: "已结束", - pollCreateButton: "创建投票", - pollCreateTitle: "创建投票", - pollEditTitle: "编辑投票", - pollQuestion: "问题", - pollQuestionPlaceholder: "你想问什么?", - pollOptions: "选项(每行一个)", - pollOutcome: "结果", - pollNoVotesYet: "尚无投票", - pollTie: "平局", - pollOptionsPlaceholder: "广场\n公园\n酒吧", - pollAnonymous: "匿名", - pollAnonymousLabel: "匿名投票", - pollMultiple: "多选", - pollMultipleLabel: "允许多个答案", - pollDeadline: "截止时间", - pollTags: "标签", - pollPublishButton: "发布投票", - pollUpdateButton: "更新", - pollCloseButton: "关闭", - pollDeleteButton: "删除", - pollVoteButton: "投票", - pollChangeVote: "修改投票", - pollVoters: "投票人", - pollComments: "评论", - pollStatusLabel: "状态", - pollStatusOpen: "进行中", - pollStatusClosed: "已结束", - pollSearchPlaceholder: "搜索投票...", - pollsNoItems: "未找到投票。", - viewPoll: "查看投票", - pollChatCreate: "创建投票", - pollInChat: "投票", - blogTitle: "博客", - blogDescription: "用于发现和管理博客的模块。", - blogFilterAll: "全部", - blogFilterMine: "我的", - blogFilterRecent: "最近", - blogFilterFavorites: "收藏", - blogFilterTop: "热门", - blogCreateButton: "创建博客", - blogSearchPlaceholder: "搜索博客...", - blogComments: "评论", - blogAllowComments: "允许评论", - blogCommentsClosed: "作者已关闭此博客的评论。", - blogNoItems: "未找到博客。", - viewBlog: "查看博客", + noPopularMessages: "还没有发布热门消息", forumTitle: "论坛", forumCategoryLabel: "类别", forumTitleLabel: "标题", @@ -1468,22 +1581,43 @@ module.exports = { forumCreateButton: "创建论坛", forumCreateSectionTitle: "创建论坛", forumDescription: "与你网络中的其他居民公开交流。", - forumSearchPlaceholder: "搜索论坛...", forumFilterAll: "全部", forumFilterMine: "我的", forumFilterRecent: "最近", forumFilterTop: "热门", + forumMineSectionTitle: "你的论坛", + forumRecentSectionTitle: "最近的论坛", + forumAllSectionTitle: "论坛", forumDeleteButton: "删除", forumParticipants: "参与者", forumMessages: "消息", + forumLastMessage: "最后消息", forumMessageLabel: "消息", forumMessagePlaceholder: "写下你的消息...", forumSendButton: "发送", + forumVisitForum: "访问论坛", noForums: "未找到论坛。", - forumVisitButton: "查看论坛", - forumTopicLabel: "主题", + forumVisitButton: "访问论坛", + forumCatGENERAL: "综合", + forumCatOASIS: "Oasis", + forumCatLARP: "L.A.R.P.", + forumCatPOLITICS: "政治", + forumCatTECH: "科技", + forumCatSCIENCE: "科学", + forumCatMUSIC: "音乐", + forumCatART: "艺术", + forumCatGAMING: "游戏", + forumCatBOOKS: "书籍", + forumCatFILMS: "电影", + forumCatPHILOSOPHY: "哲学", + forumCatSOCIETY: "社会", + forumCatPRIVACY: "隐私", + forumCatCYBERWARFARE: "网络战争", + forumCatSURVIVALISM: "生存主义", imageTitle: "图片", imageDescription: "探索和管理你网络中的图片内容。", + imagePluginTitle: "标题", + imagePluginDescription: "描述", imageMineSectionTitle: "你的图片", imageCreateSectionTitle: "上传图片", imageUpdateSectionTitle: "更新图片", @@ -1492,12 +1626,14 @@ module.exports = { imageTopSectionTitle: "热门图片", imageFavoritesSectionTitle: "收藏", imageGallerySectionTitle: "画廊", + imageMemeSectionTitle: "梗图", imageFilterAll: "全部", imageFilterMine: "我的", imageFilterRecent: "最近", imageFilterTop: "热门", imageFilterFavorites: "收藏", imageFilterGallery: "画廊", + imageFilterMeme: "梗图", imageCreateButton: "上传图片", imageUpdateButton: "更新", imageDeleteButton: "删除", @@ -1510,6 +1646,7 @@ module.exports = { imageTitlePlaceholder: "可选", imageDescriptionLabel: "描述", imageDescriptionPlaceholder: "可选", + imageMemeLabel: "标记为表情包", imageNoFile: "未提供图片文件", noImages: "没有可用的图片。", imageSearchPlaceholder: "搜索标题、标签、描述、作者...", @@ -1517,6 +1654,7 @@ module.exports = { imageSortOldest: "最早", imageSortTop: "最多投票", imageSearchButton: "搜索", + imageMessageAuthorButton: "消息", imageUpdatedAt: "已更新", imageNoMatch: "没有匹配的图片。", feedTitle: "动态", @@ -1524,6 +1662,7 @@ module.exports = { createFeedButton: "发送动态!", feedPlaceholder: "有什么新鲜事?(最多280字)", TODAYButton: "今天", + CREATEButton: "创建动态", totalOpinions: "总观点数", moreVoted: "最多投票", noFeedsFound: "未找到动态。", @@ -1546,12 +1685,20 @@ module.exports = { concept: "概念", description: "描述", meme: "表情包", + activityContact: "联系人", + activityBy: "名称", + activityPixelia: "新增像素", + viewImage: "查看图片", + playAudio: "播放音频", + playVideo: "播放视频", typeRecent: "最近", - typeTop: "排行", + errorActivity: "获取活动时出错", typePost: "博客", + recentButton: "最近", typeTribe: "部落", typeLarp: "L.A.R.P.", typeInhabitants: "居民", + activityProfileUpdated: "更新了个人资料", typeAbout: "居民", typeCurriculum: "简历", typeImage: "图片", @@ -1595,6 +1742,8 @@ module.exports = { typeCourtsSettlementAccepted: "法庭 · 和解接受", typeCourtsNomination: "法庭 · 提名", typeCourtsNominationVote: "法庭 · 提名投票", + activitySupport: "新联盟建立", + activityJoin: "加入新 PUB", question: "问题", deadline: "截止日期", status: "状态", @@ -1608,8 +1757,12 @@ module.exports = { date: "日期", category: "类别", attendees: "参与者", + activitySpread: "->", + visitLink: "访问链接", + viewDocument: "查看文档", location: "位置", contentWarning: "主题", + personName: "居民名称", bankWalletConnected: "ECOin 钱包", bankUbiReceived: "已收到 UBI", bankTx: "交易", @@ -1619,6 +1772,7 @@ module.exports = { link: "链接", activityProjectFollow: "%OASIS% 正在 %ACTION% 此项目 %PROJECT%", activityProjectUnfollow: "%OASIS% 正在 %ACTION% 此项目 %PROJECT%", + activityProjectPledged: "%OASIS% 已 %ACTION% %AMOUNT% 至项目 %PROJECT%", following: "关注中", unfollowing: "取消关注中", pledged: "已质押", @@ -1664,17 +1818,26 @@ module.exports = { courtsVotesSlashTotal: "赞成/总计", courtsOpenVote: "公开投票", courtsAnswerTitle: "答辩", + courtsStanceADMIT: "承认", + courtsStanceDENY: "否认", + courtsStancePARTIAL: "部分承认", + courtsStanceCOUNTERCLAIM: "反诉", + courtsStanceNEUTRAL: "中立", courtsVerdictResult: "结果", courtsVerdictOrders: "命令", courtsSettlementText: "和解", courtsSettlementAccepted: "已接受", + courtsSettlementPending: "待处理", courtsJudge: "法官", courtsThSupports: "支持", courtsFilterOpenCase: '开立案件', + courtsEvidenceFileLabel: '证据文件(图片、音频、视频或 PDF)', + courtsCaseMediators: '调解员', courtsCaseMediatorsPh: '调解员 Oasis ID,用逗号分隔', courtsMediatorsLabel: '调解员', courtsThCase: '案件', courtsThCreatedAt: '开始日期', + courtsThActions: '操作', courtsPublicPrefLabel: '解决后的可见性', courtsPublicPrefYes: '我同意此案件可完全公开', courtsPublicPrefNo: '我倾向于保持细节私密', @@ -1682,11 +1845,8 @@ module.exports = { courtsMethodMEDIATION: '调解', courtsNoCases: '没有案件。', reportsTitle: "报告", - reportsDescription: "管理并跟踪你的网络中的报告。", - reportsSearchPlaceholder: "搜索举报...", + reportsDescription: "管理和追踪你网络中与问题、漏洞、滥用和内容警告相关的报告。", reportsFilterAll: "全部", - reportsFilterRecent: "最新", - reportsFilterTop: "热门", reportsFilterMine: "我的", reportsFilterFeatures: "功能", reportsFilterBugs: "漏洞", @@ -1699,13 +1859,18 @@ module.exports = { reportsFilterInvalid: "无效", reportsCreateButton: "创建报告", reportsTitleLabel: "标题", + reportsDescriptionLabel: "描述", + reportsDescriptionPlaceholder: "请提供详细描述。", reportsCategory: "类别", + reportsCategoryLabel: "类别", reportsCategoryFeatures: "功能", reportsCategoryBugs: "漏洞", reportsCategoryAbuse: "滥用", - reportsCategoryContent: "内容", + reportsCategoryContent: "内容问题", reportsUpdateButton: "更新", reportsDeleteButton: "删除", + reportsDateLabel: "日期", + reportsUploadFile: "上传媒体(最大:50MB)", reportsMineSectionTitle: "你的报告", reportsFeaturesSectionTitle: "功能请求", reportsBugsSectionTitle: "漏洞", @@ -1713,6 +1878,11 @@ module.exports = { reportsContentSectionTitle: "内容问题", reportsAllSectionTitle: "报告", reportsNoItems: "没有可用的报告。", + reportsValidationTitle: "请输入有效的标题。", + reportsValidationDescription: "描述不能为空。", + reportsValidationCategory: "请选择一个类别。", + reportsCreatedAt: "创建于", + reportsCreatedBy: "作者", reportsSeverity: "严重程度", reportsSeverityLow: "低", reportsSeverityMedium: "中", @@ -1723,6 +1893,9 @@ module.exports = { reportsStatusUnderReview: "审查中", reportsStatusResolved: "已解决", reportsStatusInvalid: "无效", + reportsUpdateStatusButton: "更新状态", + reportsAnonymityOption: "匿名提交", + reportsAnonymousAuthor: "匿名", reportsConfirmButton: "确认报告!", reportsConfirmations: "确认", reportsConfirmedSectionTitle: "已确认的报告", @@ -1770,20 +1943,6 @@ module.exports = { reportsRequestedActionLabel: '请求的操作', reportsRequestedActionPlaceholder: '删除、隐藏、标记、警告等。', tribesTitle: "部落", - tribeFilterAll: "全部", - tribeFilterRecent: "最近", - tribeFilterMine: "我的", - tribeFilterMembership: "成员", - tribeFilterSubtribes: "子部落", - tribeFilterTop: "热门", - tribeFilterGallery: "图库", - tribeFeedFilterTOP: "热门", - tribeFeedFilterMINE: "我的", - tribeFeedFilterALL: "全部", - tribeFeedFilterRECENT: "最近", - courtsStanceDENY: "否认", - courtsStanceADMIT: "承认", - courtsStancePARTIAL: "部分承认", tribeAllSectionTitle: "部落", tribeMineSectionTitle: "你的部落", tribeCreateSectionTitle: "创建部落", @@ -1795,6 +1954,13 @@ module.exports = { tribeviewSubTribeButton: "访问子部落", tribeRootLabel: "根", tribeDescription: "探索或创建你网络中的部落。", + tribeFilterAll: "全部", + tribeFilterMine: "我的", + tribeFilterMembership: "成员资格", + tribeFilterRecent: "最近", + tribeFilterTop: "热门", + tribeFilterSubtribes: "子部落", + tribeFilterGallery: "画廊", tribeMainTribeLabel: "主部落", tribeCreateButton: "创建部落", tribeUpdateButton: "更新", @@ -1813,8 +1979,13 @@ module.exports = { tribeModeLabel: "模式", tribeIsAnonymousLabel: "状态", tribeMembersCount: "成员", + tribeInviteCodePlaceholder: "输入邀请码", + tribeJoinByCodeButton: "使用邀请码加入", + tribeJoinButton: "加入", tribeEnterInvite: "输入邀请码", tribeLeaveButton: "离开", + tribeYes: "是", + tribeNo: "否", tribePublic: "公开", tribePrivate: "私密", tribeGenerateInvite: "单次邀请", @@ -1823,8 +1994,13 @@ module.exports = { tribeRemoveInvitation: "移除邀请", tribeCreatedAt: "创建于", tribeAuthor: "作者", + tribeAuthorLabel: "作者", tribeStrict: "严格", tribeOpen: "开放", + tribeFeedFilterRECENT: "最近", + tribeFeedFilterMINE: "我的", + tribeFeedFilterALL: "全部", + tribeFeedFilterTOP: "热门", tribeFeedRefeeds: "转发", tribeFeedRefeed: "转发", tribeFeedMessagePlaceholder: "写一条动态…", @@ -1834,12 +2010,17 @@ module.exports = { tribeNotFound: "未找到部落!", createTribeTitle: "创建部落", updateTribeTitle: "更新部落", + tribeSectionOverview: "概览", tribeSectionInhabitants: "居民", tribeSectionVotations: "投票", tribeSectionEvents: "活动", + tribeSectionReports: "报告", tribeSectionTasks: "任务", tribeSectionFeed: "动态", tribeSectionForum: "论坛", + tribeSectionMarket: "市场", + tribeSectionJobs: "工作", + tribeSectionProjects: "项目", tribeSectionMedia: "媒体", tribeSectionImages: "图片", tribeSectionAudios: "音频", @@ -1877,6 +2058,11 @@ module.exports = { tribeTaskStatusClosed: "关闭", tribeTaskAssign: "分配", tribeTaskUnassign: "取消分配", + tribeReportCreate: "创建报告", + tribeReportsEmpty: "还没有报告。", + tribeReportTitle: "标题", + tribeReportDescription: "描述", + tribeReportCategory: "类别", tribeVotationCreate: "创建投票", tribeVotationsEmpty: "还没有投票。", tribeVotationTitle: "标题", @@ -1894,6 +2080,27 @@ module.exports = { tribeForumCategory: "类别", tribeForumReply: "回复", tribeForumReplies: "回复", + tribeMarketCreate: "创建商品", + tribeMarketEmpty: "还没有商品。", + tribeMarketTitle: "标题", + tribeMarketDescription: "描述", + tribeMarketPrice: "价格", + tribeMarketImage: "图片", + tribeMarketCategory: "类别", + tribeJobCreate: "创建工作", + tribeJobsEmpty: "还没有工作。", + tribeJobTitle: "标题", + tribeJobDescription: "描述", + tribeJobLocation: "位置", + tribeJobSalary: "薪资", + tribeJobDeadline: "截止日期", + tribeProjectCreate: "创建项目", + tribeProjectsEmpty: "还没有项目。", + tribeProjectTitle: "标题", + tribeProjectDescription: "描述", + tribeProjectGoal: "目标", + tribeProjectFunded: "已筹资", + tribeProjectDeadline: "截止日期", tribeMediaUpload: "上传媒体", readDocument: "阅读文档", tribeCreateImage: "创建图片", @@ -1904,6 +2111,7 @@ module.exports = { tribeMediaEmpty: "还没有媒体。", tribeMediaTitle: "标题", tribeMediaDescription: "描述", + tribeMediaType: "类型", tribeMediaTypeImage: "图片", tribeMediaTypeVideo: "视频", tribeMediaTypeAudio: "音频", @@ -1913,6 +2121,11 @@ module.exports = { tribeInviteCodeText: "邀请码:", oasisVersionLabel: "Oasis 版本", tribeInviteCodeHint: "将此邀请码分享给你想邀请的人,他们可在 /invites → Tribes 加入。", + tribeGroupTribe: "部落", + tribeGroupOffice: "办公", + tribeGroupNetwork: "网络", + tribeGroupEconomy: "经济", + tribeGroupMedia: "媒体", tribeStatusOpen: "开放", tribeStatusClosed: "已关闭", tribeStatusInProgress: "进行中", @@ -1922,17 +2135,33 @@ module.exports = { tribePriorityCritical: "严重", tribeTaskFilterAll: "全部", tribeMediaFilterAll: "全部", + tribeReportCatBug: "漏洞", + tribeReportCatAbuse: "滥用", + tribeReportCatContent: "内容", + tribeReportCatOther: "其他", tribeForumCatGeneral: "综合", tribeForumCatProposal: "提案", tribeForumCatQuestion: "问题", tribeForumCatAnnouncement: "公告", + tribeMarketCatGoods: "商品", + tribeMarketCatServices: "服务", + tribeMarketCatFood: "食品", + tribeMarketCatOther: "其他", tribeStatusLabel: "状态", tribeSubTribes: "子部落", tribeSubTribesCreate: "创建子部落", tribeSubTribesStrictDenied: "部落的严格模式不允许您创建新的子部落。请联系管理员。", + tribeSubTribesEmpty: "还没有创建子部落。", + tribeActivityJoined: "已加入", + tribeActivityLeft: "已离开", + tribeActivityFeed: "动态", tribeActivityRefeed: "转发", + tribeGroupAnalytics: "分析", + tribeGroupCreative: "创意", tribeSectionActivity: "活动记录", tribeSectionTrending: "热门", + tribeSectionOpinions: "观点", + tribeSectionPixelia: "PIXELIA", tribeSectionTags: "标签", tribeSectionSearch: "搜索", tribeActivityEmpty: "还没有活动。", @@ -1944,15 +2173,25 @@ module.exports = { tribeTrendingPeriodWeek: "本周", tribeTrendingPeriodAll: "全部时间", tribeTrendingEngagement: "参与度", + tribeOpinionsEmpty: "还没有观点。", + tribeOpinionsCast: "投票", + tribeOpinionsRankings: "排名", + tribeOpinionsAlreadyVoted: "已投票", tribeTopCategory: "最多投票", + tribePixeliaTitle: "Pixelia", + tribePixeliaDescription: "协作像素艺术画布。", + tribePixeliaPaint: "画", + tribePixeliaContributors: "贡献者", + tribePixeliaTotalPixels: "已绘制像素", tribeTagsEmpty: "未找到标签。", + tribeTagsCloud: "标签云", + tribeTagsContentWith: "带有标签的内容", tribeSearchPlaceholder: "搜索部落内容...", tribeSearchEmpty: "未找到结果。", tribeSearchResults: "结果", tribeSearchMinChars: "请输入至少2个字符进行搜索。", agendaTitle: "日程", agendaDescription: "在这里你可以找到所有分配给你的项目。", - agendaSearchPlaceholder: "搜索议程...", agendaFilterAll: "全部", agendaFilterToday: "今天", agendaFilterUpcoming: "即将到来", @@ -1966,14 +2205,23 @@ module.exports = { agendaFilterReports: "报告", agendaFilterTransfers: "转账", agendaFilterJobs: "工作", - agendaFilterIndustry: "工业", - agendaFilterHousing: "住所", agendaFilterProjects: "项目", agendaFilterCalendars: "日历", agendaNoItems: "未找到分配项目。", + agendaAuthor: "作者", agendaDiscardButton: "丢弃", agendaRestoreButton: "恢复", + agendaCreatedAt: "创建于", + agendaTitleLabel: "标题", agendaMembersCount: "成员", + agendaDescriptionLabel: "描述", + agendaStatus: "状态", + agendaVisibility: "可见性", + agendaEventDate: "活动日期", + agendaEventLocation: "位置", + agendaEventPrice: "价格", + agendaEventUrl: "URL", + agendaTaskStart: "开始时间", agendaLocationLabel: "位置", agendaYes: "是", agendaNo: "否", @@ -1983,6 +2231,11 @@ module.exports = { agendareportCategory: "类别", agendareportSeverity: "严重程度", agendareportStatus: "状态", + agendareportDescription: "描述", + agendaTaskEnd: "结束时间", + agendaTaskPriority: "优先级", + agendaTransferFrom: "发起人", + agendaTransferTo: "接收人", agendaTransferConcept: "概念", agendaTransferAmount: "金额", agendaTransferDeadline: "截止日期", @@ -1992,7 +2245,47 @@ module.exports = { voteNow: "立即投票", alreadyVoted: "你已经表达了意见。", noOpinionsFound: "未找到观点。", + RECENTButton: "最近", TOPButton: "热门", + interestingButton: "有趣", + necessaryButton: "必要", + funnyButton: "搞笑", + disgustingButton: "恶心", + sensibleButton: "明智", + propagandaButton: "宣传", + adultOnlyButton: "仅限成人", + boringButton: "无聊", + confusingButton: "令人困惑", + usefulButton: "有用", + informativeButton: "信息丰富", + wellResearchedButton: "研究充分", + accurateButton: "准确", + needsSourcesButton: "需要来源", + wrongButton: "错误", + lowQualityButton: "低质量", + creativeButton: "有创意", + insightfulButton: "有洞察力", + actionableButton: "可操作", + inspiringButton: "鼓舞人心", + loveButton: "喜爱", + clearButton: "清晰", + upliftingButton: "振奋人心", + unnecessaryButton: "不必要", + rejectedButton: "被拒绝", + misleadingButton: "误导", + offTopicButton: "离题", + duplicateButton: "重复", + clickbaitButton: "标题党", + spamButton: "垃圾信息", + trollButton: "恶搞", + nsfwButton: "NSFW", + violentButton: "暴力", + toxicButton: "有毒", + harassmentButton: "骚扰", + hateButton: "仇恨", + scamButton: "诈骗", + triggeringButton: "触发性", + opinionsCreatedAt: "创建于", opinionsTotalCount: "总观点数", voteInteresting: "有趣", voteNecessary: "必要", @@ -2029,39 +2322,27 @@ module.exports = { voteCreative: "有创意", voteSpam: "垃圾信息", voteAdultOnly: "仅限成人", + publishBlog: "发布博客", privateMessage: "私信", pmSendTitle: "私信", pmSend: "发送!", - pmCancel: "取消", - pmReset: "重置", - pmSharedKeyHint: "请通过其他渠道将此密钥分享给收件人。解密时需要用到。", - pmDescription: "使用此表单向其他居民发送加密的消息/文件。", - pmComposeTitle: "发送消息", + pmDescription: "使用此表单向其他居民发送加密消息。", pmRecipients: "收件人", pmRecipientsHint: "输入 Oasis ID,用逗号分隔", - pmLimitsHint: "每条消息最多 7 个收件人。", - pmTextPlaceholder: "正文上限 7000 个字符。", + pmLevelOfExposition: "Level of exposition", + pmLimitsHint: "Up to 7 recipients per message. Body capped at 8096 characters.", pmSubject: "主题", pmSubjectHint: "输入消息主题", pmText: "消息", pmCrypterChip: "2xE2E", pmCrypterDecryptButton: "解密", - fileShareTitle: "分享文件", - fileShareRecipientPlaceholder: "@...=.ed25519", - fileShareFileLabel: "文件", - fileShareDownload: "下载", - fileShareMutualError: "你只能与相互支持的居民分享文件。", - fileShareNoFile: "未选择文件。", - fileShareTooLarge: "文件超过允许的大小。", - fileShareFailed: "无法准备文件。", - fileShareSendError: "无法发送文件。", - fileShareUnavailable: "该文件当前不可用。请稍后再试。", pmCrypterBadKey: "共享密钥不正确!", pmCrypterTooLong: "消息过长,无法用 Crypter 加密。请缩短后重试。", pmCrypterCipherLabel: "重新加密的消息", pmCrypterCharsLabel: "字符", pmInvalidRecipients: "无效的 Oasis ID(应形如 @…=.ed25519)。", pmEncryptionLabel: "加密", + pmFile: "附件", private: "私密", privateDescription: "你的加密消息。", privateInbox: "收件箱", @@ -2071,27 +2352,19 @@ module.exports = { noPrivateMessages: "没有私信。", pmFromLabel: "发件人:", pmToLabel: "收件人:", + pmInvalidMessage: "无效消息", pmNoSubject: "(无主题)", pmSubjectLabel: "主题:", - pmBotJobs: "JobsBot", - pmBotProjects: "ProjectsBot", - pmBotMarket: "MarketBot", - pmBotShops: "ShopsBot", - pmBotPolitical: "PoliticalBot", - politicalBotLarpTitle: "L.A.R.P 的执政家族已更替。", - politicalBotParliamentTitle: "议会已开始新的执政周期。", - politicalBotTribeTitle: "你所在的某个部落已开始新的执政周期。", - politicalBotLarpIntro: "现在由 {house} 在本周期执政:{cycle}", - politicalBotParliamentIntro: "当前政府为 {gov}", - politicalBotParliamentIntroLeader: "当前政府为 {gov},由 {leader} 行使", - politicalBotTribeIntro: "部落 {tribe} 当前政府为 {gov}", - politicalBotTribeIntroLeader: "部落 {tribe} 当前政府为 {gov},由 {leader} 行使", - politicalBotLeaderLabel: "领袖", + pmBodyLabel: "正文", + pmBotJobs: "42-JobsBOT", + pmBotProjects: "42-ProjectsBOT", + pmBotMarket: "42-MarketBOT", inboxJobSubscribedTitle: "你的工作机会有新订阅", pmInhabitantWithId: "Oasis ID 为以下的居民:", pmHasSubscribedToYourJobOffer: "已订阅你的工作机会", + inboxProjectCreatedTitle: "新项目已创建", + pmHasCreatedAProject: "已创建一个项目", inboxMarketItemSoldTitle: "商品已售出", - inboxShopSoldTitle: "商品已售出", pmYourItem: "你的商品", pmHasBeenSoldTo: "已售出给", pmFor: "售价", @@ -2099,6 +2372,7 @@ module.exports = { pmHasPledged: "已质押", pmToYourProject: "至你的项目", blockchain: '区块浏览器', + blockchainTitle: '区块浏览器', blockchainDescription: '探索和可视化区块链中的区块。', blockchainNoBlocks: '区块链中未找到区块。', blockchainStatsTitle: '统计', @@ -2110,36 +2384,42 @@ module.exports = { blockchainBlockCarbon: '碳足迹', blockchainBlockType: '类型', blockchainBlockTimestamp: '时间戳', + blockchainBlockContent: '区块', + blockchainBlockURL: 'URL:', + blockchainContent: '区块', + blockchainContentPreview: '区块内容预览', blockchainLatestDatagram: '最新数据报', - blockchainDatagram: "数据报", - blockchainDetails: "查看区块详情", - blockchainViewBlockexplorer: "在 Blockexplorer 中查看", + blockchainDatagram: '数据报', + blockchainDetails: '查看区块详情', + blockchainViewBlockexplorer: "在区块浏览器中查看", + blockchainBlockInfo: '区块信息', + blockchainBlockDetails: '所选区块的详情', blockchainBack: '返回区块浏览器', blockchainContentDeleted: "此内容已被标记为墓碑", - visitContent: "查看内容", - favoriteAdd: "固定到收藏", - pmContentTooltip: "发送私信", - favoriteRemove: "从收藏移除", - reportContent: "举报内容", + visitContent: "访问内容", banking: '银行', bankingTitle: '银行', - bankingDescription: "ECOin 经济:余额、全民基本收入、税收与工业价值。", + bankingDescription: '探索 ECOin 的当前价值和相应的 UBI 分配,根据参与度和信任度按纪元分配。', bankOverview: '概览', bankEpochs: '纪元', bankRules: '规则', pending: '待处理', closed: '已关闭', + bankBack: '返回银行', bankViewTx: '查看交易', bankClaimNow: '立即领取', bankClaimUBI: '领取 UBI!', bankClaimAndPay: 'Claim & Pay', bankClaimedPending: 'Claim pending...', + bankStatusUnclaimed: 'Unclaimed', bankStatusClaimed: 'Claimed', bankStatusExpired: 'Expired', bankPubOnly: 'PUB-only operation', + bankNoPendingUBI: '本期无待领取的 UBI 分配。', bankEpoch: '纪元', bankPool: '资金池(本纪元)', bankWeightsSum: '权重总和', + bankAllocations: '分配', bankNoAllocations: '未找到分配。', bankNoEpochs: '未找到纪元。', bankEpochAllocations: '纪元分配', @@ -2153,11 +2433,10 @@ module.exports = { bankEpochId: '纪元 ID', bankRuleHash: '规则快照哈希', bankViewEpoch: '查看纪元', - bankYourIndustryBalance: "你的工业份额", - bankYourUbiMonth: "你本月的全民基本收入", - bankYourFundsMonth: "你本月的资金", - bankIndustryBalance: "工业产值(估算)", bankUserBalance: '你的余额', + ecoWalletNotConfigured: '未配置 ECOin 钱包', + editWallet: '编辑钱包', + addWallet: '添加钱包', bankAddresses: '地址', bankNoAddresses: '未找到地址。', bankUser: 'Oasis ID', @@ -2182,10 +2461,18 @@ module.exports = { search: '搜索!', bankLocal: '本地', bankFromOasis: 'Oasis', + bankMyAddress: '你的地址', + bankRemoveMyAddress: '移除我的地址', + bankNotRemovableOasis: '无法在本地移除地址', + bankingFutureUBI: "UBI", + pubIdTitle: "PUB Wallet", + pubIdDescription: "Set the PUB OASIS ID. This will be used for PUB transactions (including the UBI).", pubIdLabel: "PUB ID", + pubIdSave: "Save configuration", + pubIdPlaceholder: "@PUB_ID.ed25519", bankUbiAvailableNo: "资金不足!", bankUbiAvailableOk: "可用!", - bankUbiAvailability: "全民基本收入(网络)", + bankUbiAvailability: "UBI 可用性", bankAlreadyClaimedThisMonth: "本月已领取", bankUbiThisMonth: "UBI(本月)", bankUbiLastClaimed: "UBI(上次领取)", @@ -2199,6 +2486,13 @@ module.exports = { shopsTitle: "商店", shopDescription: "发现和管理网络中的商店。", shopTitle: "商店", + shopFilterAll: "全部", + shopFilterMine: "我的", + shopFilterRecent: "最近", + shopFilterTop: "热门", + shopFilterProducts: "产品", + shopFilterPrices: "价格", + shopFilterFavorites: "收藏", shopUpload: "创建商店", shopAllSectionTitle: "所有商店", shopMineSectionTitle: "我的商店", @@ -2215,10 +2509,16 @@ module.exports = { shopReachClearnet: "Clearnet", shopReachOasis: "Oasis", encryptedChipLabel: "E2E", + shopClearnetPublish: "发布到 Clearnet", + shopClearnetUnpublish: "从 Clearnet 撤下", + shopClearnetUrlLabel: "Clearnet 网址", + shopClearnetWarning: "发布到 Clearnet 后,本店铺可被外部搜索引擎和爬虫索引。", shopAddFavorite: "添加收藏", shopRemoveFavorite: "取消收藏", shopOpen: "开放", shopClosed: "关闭", + shopOpenShop: "开放商店", + shopCloseShop: "关闭商店", shopMakePrivate: "设为私密 (E2E)", shopMakePublic: "设为公开", shopProducts: "产品", @@ -2246,6 +2546,8 @@ module.exports = { shopOrderPrice: "价格", shopOrderBuyer: "买家", shopBackToShop: "返回商店", + shopShareUrl: "分享链接", + shopVisitShop: "访问商店", marketVisitItem: "查看商品", shopStatus: "状态", shopCreatedAt: "创建时间", @@ -2254,10 +2556,12 @@ module.exports = { shopLocation: "位置", shopTags: "标签", shopVisibility: "可见性", + shopImage: "图片", shopShortDescription: "简短描述", shopShortDescriptionPlaceholder: "店铺卡片简短描述(最多160个字符)", shopTitlePlaceholder: "您的店铺名称", shopDescriptionPlaceholder: "您的店铺详细描述", + shopUrlPlaceholder: "https://您的店铺网址.com", shopLocationPlaceholder: "城市,国家", shopTagsPlaceholder: "标签1, 标签2, 标签3", mapTitlePlaceholder: "地图标题", @@ -2275,6 +2579,7 @@ module.exports = { bankUnitMinutes: "分钟", bankUnitDays: "天", bankExchangeNoData: '无可用数据', + bankExchangeIndex: 'ECOin 价值(1小时)', bankInflation: 'ECOin 通胀率 (anual)', bankInflationMonthly: 'ECOin 通胀率 (mensual)', bankExchangeChartTitle: 'ECOin 价值随时间的演进', @@ -2288,17 +2593,38 @@ module.exports = { bankingSyncStatusOutdated: '已过时', statsTitle: '统计', statistics: "统计", + statsInhabitant: "居民统计", statsDescription: "发现你网络的统计数据。", ALLButton: "全部", MINEButton: "我的", TOMBSTONEButton: "墓碑", + statsYou: "你", + statsUserId: "Oasis ID", statsCreatedAt: "创建于", statsYourContent: "内容", statsYourOpinions: "观点", + statsYourTombstone: "墓碑", + statsNetwork: "网络", + statsTotalInhabitants: "居民", + statsDiscoveredTribes: "部落(公开)", + statsPrivateDiscoveredTribes: "部落(私密)", statsNetworkContent: "内容", + statsYourMarket: "市场", + statsYourJob: "工作", + statsYourProject: "项目", + statsYourTransfer: "转账", + statsYourForum: "论坛", statsNetworkOpinions: "观点", + statsDiscoveredMarket: "市场", + statsDiscoveredJob: "工作", + statsDiscoveredProject: "项目", + statsBankingTitle: "银行", statsEcoWalletLabel: "ECOIN 钱包", statsEcoWalletNotConfigured: "未配置!", + statsTotalEcoAddresses: "地址总数", + statsDiscoveredTransfer: "转账", + statsDiscoveredForum: "论坛", + statsNetworkTombstone: "墓碑", statsBookmark: "书签", statsEvent: "活动", statsTask: "任务", @@ -2320,12 +2646,16 @@ module.exports = { statsAiExchange: "AI", statsPUBs: 'PUB', statsPost: "帖子", + statsOasisID: "Oasis ID", statsSize: "总计(大小)", statsBlockchainSize: "区块链(大小)", statsBlobsSize: "Blob(大小)", statsActivity7d: "活动(最近7天)", statsActivity7dTotal: "7天总计", statsActivity30dTotal: "30天总计", + statsKarmaScore: "KARMA 评分", + statsPublic: "公开", + statsPrivate: "私密", day: "日", messages: "消息", statsProject: "项目", @@ -2337,14 +2667,30 @@ module.exports = { statsProjectsCancelled: "已取消", statsProjectsGoalTotal: "目标总额", statsProjectsPledgedTotal: "质押总额", + statsProjectsSuccessRate: "成功率", + statsProjectsAvgProgress: "平均进度", + statsProjectsMedianProgress: "中位数进度", + statsProjectsActiveFundingAvg: "平均活跃筹资", + statsJobsTitle: "工作", + statsJobsTotal: "工作总数", + statsJobsOpen: "开放", + statsJobsClosed: "已关闭", + statsJobsOpenVacants: "开放职位", + statsJobsSubscribersTotal: "订阅者总数", + statsJobsAvgSalary: "平均薪资", + statsJobsMedianSalary: "中位数薪资", statsMarketTitle: "市场", statsMarketTotal: "商品总数", statsMarketForSale: "在售", statsMarketReserved: "已预订", statsMarketClosed: "已关闭", statsMarketSold: "已售出", + statsMarketRevenue: "收入", + statsMarketAvgSoldPrice: "平均售价", statsUsersTitle: "居民", user: "居民", + statsTombstoneTitle: "墓碑", + statsNetworkTombstones: "网络墓碑", statsTombstoneRatio: "墓碑比率 (%)", statsParliamentCandidature: "议会候选", statsParliamentTerm: "议会任期", @@ -2371,21 +2717,30 @@ module.exports = { aiConfiguration: "设置提示语", aiPromptUsed: "提示语", aiClearHistory: "清除聊天记录", + aiSharePrompt: "将此回答添加到集体训练?", + aiShareYes: "是", + aiShareNo: "否", + aiSharedLabel: "已添加到训练", + aiRejectedLabel: "未添加到训练", aiServerError: "AI 无法回答。请重试。", aiInputPlaceholder: "什么是 Oasis?", typeAiExchange: "AI", aiApproveTrain: "添加到集体训练", aiRejectTrain: "不训练", + aiTrainPending: "待批准", aiTrainApproved: "已批准训练", aiTrainRejected: "已拒绝训练", aiSnippetsUsed: "使用的片段", aiSnippetsLearned: "已学习的片段", statsAITraining: "AI 训练", + aiApproveCustomTrain: "使用此自定义回答训练", aiCustomAnswerPlaceholder: "写下你的自定义回答…", + statsAIExchanges: "模型交流", marketMineSectionTitle: "你的商品", marketCreateSectionTitle: "创建商品", marketUpdateSectionTitle: "更新", marketAllSectionTitle: "市场", + marketRecentSectionTitle: "最近的市场", marketTitle: "市场", marketDescription: "在你的网络中交换商品或服务的市场。", marketFilterAll: "全部", @@ -2415,11 +2770,14 @@ module.exports = { marketItemDeadline: "截止日期", marketItemIncludesShipping: "包含运费?", marketItemHighestBid: "最高出价", + marketItemHighestBidder: "最高出价者", marketItemStock: "库存", marketOutOfStock: "缺货", + marketItemBidTime: "出价时间", marketActionsUpdate: "更新", marketUpdateButton: "更新", marketActionsDelete: "删除", + marketActionsSold: "标记为已售出", marketActionsChangeStatus: "更改状态", marketActionsBuy: "购买!", marketAuctionBids: "当前出价", @@ -2428,82 +2786,19 @@ module.exports = { marketNoItems: "暂无可用商品。", marketYourBid: "你的出价", marketCreateFormImageLabel: "上传媒体(最大:50MB)", + marketSearchLabel: "搜索", marketSearchPlaceholder: "搜索标题或标签", marketMinPriceLabel: "最低价格", marketMaxPriceLabel: "最高价格", + marketSortLabel: "排序", marketSortRecent: "最近", marketSortPrice: "价格", marketSortDeadline: "截止日期", marketSearchButton: "搜索", marketAuctionEndsIn: "结束于", marketAuctionEnded: "已结束", + marketMyBidBadge: "你已出价", marketNoItemsMatch: "没有匹配的商品。", - housingTitle: "住所", - housingDescriptionText: "在你的网络中发现和管理住所。", - modulesHousingLabel: "住所", - modulesHousingDescription: "发现和管理住所的模块。", - typeHousing: "住所", - housingFilterAll: "全部", - housingFilterMine: "我的", - housingFilterRequested: "已申请", - housingFilterSale: "出售", - housingFilterRent: "出租", - housingFilterCouchsurfing: "沙发客", - housingFilterOpen: "可入住", - housingFilterClosed: "不可入住", - housingFilterRecent: "最新", - housingFilterTop: "热门", - housingType: "类型", - housingTypeSALE: "出售", - housingTypeRENT: "出租", - housingTypeCOUCHSURFING: "沙发客", - housingProperty: "房产", - housingPropertyAPARTMENT: "公寓", - housingPropertyHOUSE: "房屋", - housingPropertyROOM: "房间", - housingPropertyLAND: "土地", - housingPropertyOTHER: "其他", - housingTitleLabel: "标题", - housingTitlePlaceholder: "住所名称", - housingDescription: "描述", - housingDescriptionPlaceholder: "描述住所、社区以及包含的内容:卫生间、花园、床位…", - housingRules: "规则", - housingRulesPlaceholder: "宠物、吸烟、安静时段、最短入住…", - housingPlace: "地点", - housingPlacePlaceholder: "国家、城市、地址", - housingPrice: "价格", - housingPriceLabel: "价格(ECO)", - housingSizeLabel: "面积(m²)", - housingFree: "免费", - housingPerMonth: "/月", - housingRooms: "房间数", - housingSize: "面积", - housingCapacity: "可住人数", - housingAvailableFrom: "可入住起始日", - housingAvailableTo: "可入住截止日", - housingTags: "标签", - spreadEditWarning: "编辑后此内容将失去已有的转发", - housingRequests: "申请", - housingRequestButton: "申请", - housingCancelButton: "取消申请", - housingRequestedBadge: "已申请", - housingStatus: "状态", - housingStatusOPEN: "可入住", - housingStatusCLOSED: "不可入住", - housingSetOpen: "设为可入住", - housingSetClosed: "设为不可入住", - housingCreateButton: "创建住所", - housingUpdateButton: "更新", - housingDeleteButton: "删除", - housingSearchPlaceholder: "搜索住所...", - housingSearchButton: "搜索", - housingMinPrice: "最低价格", - housingMaxPrice: "最高价格", - housingSortRecent: "最新", - housingSortPrice: "价格最低", - housingSortRequests: "申请最多", - housingSortRating: "评价最高", - housingNoItems: "未找到住所。", jobsTitle: "工作", jobsCandidatesTitle: "推荐候选人", jobsCandidatesDescription: "技能与此工作匹配的居民。请发送私信邀请他们。", @@ -2551,6 +2846,7 @@ module.exports = { jobLocationPresencial: "现场", jobLocationRemote: "远程", jobVacantsPlaceholder: "职位数量", + jobSalaryPlaceholder: "每小时 ECO 薪资", jobImage: "上传媒体(最大:50MB)", jobTasks: "任务", jobType: "工作类型", @@ -2560,6 +2856,7 @@ module.exports = { jobsFilterTop: "热门", jobsTopTitle: "最高薪资工作", createJobButton: "发布工作", + viewDetailsButton: "查看详情", noJobsFound: "未找到工作机会。", jobAuthor: "作者", jobTypeFreelance: "自由职业", @@ -2571,6 +2868,10 @@ module.exports = { jobsHoursOffered: "提供的小时数", jobsHoursRequested: "请求的小时数", jobsExchangeSkill: "交换需要的技能", + jobsHoursOfferedPlaceholder: "如 4", + jobsHoursRequestedPlaceholder: "如 4", + jobsExchangeSkillPlaceholder: "如 木工、设计", + jobExchangeHint: "对于时间交换工作,请填写下面的字段,而不是薪水。", jobTimePartial: "兼职", jobTimeComplete: "全职", jobsDeleteButton: "删除", @@ -2578,201 +2879,31 @@ module.exports = { jobsFilterApplied: "已申请", jobsAppliedTitle: "我的申请", jobsAppliedBadge: "已申请", + jobsFilterFavs: "收藏", + jobsFavsTitle: "收藏", + jobsFilterNeeds: "需要帮助", + jobsNeedsTitle: "需要帮助", + jobsSearchLabel: "搜索", jobsSearchPlaceholder: "搜索标题、标签、描述...", jobsMinSalaryLabel: "最低薪资", jobsMaxSalaryLabel: "最高薪资", + jobsSortLabel: "排序", jobsSortRecent: "最近", jobsSortSalary: "最高薪资", jobsSortSubscribers: "最多申请者", jobsSearchButton: "搜索", + jobsFavoriteButton: "收藏", + jobsUnfavoriteButton: "取消收藏", + jobsMessageAuthorButton: "私信", jobsApplicants: "申请者", jobsUpdatedAt: "已更新", jobSetOpen: "设为开放", + jobNewBadge: "新", jobsTagsLabel: "标签", + jobsTagsPlaceholder: "标签1, 标签2, 标签3", noJobsMatch: "没有匹配的工作。", - industrySearchPlaceholder: "搜索工厂…", - industryAllSectors: "所有部门", - industryVisitButton: "访问工业", - industryAdvanceTo: "设为", - industryAmount: "金额", - industryApproveBuild: "批准", - industryBlueprint: "蓝图", - industryBlueprints: "蓝图", - industryBuild: "构建", - industryBuildNotes: "备注", - industryBuildStart: "开始日期", - industryVoteUpdateButton: "投票更新", - industryVoteDeleteButton: "投票删除", - industryRevokeVote: "撤销", - industryTimeLeft: "剩余时间", - industryBuildEnd: "结束日期", - industryBuilds: "构建", - industryBuildTitle: "标题", - industryContribute: "贡献", - industryContributeButton: "贡献", - industryContributions: "贡献", - industryCreateBlueprintButton: "创建蓝图", - industryDistribute: "分配", - industryDistributeButton: "按份额分配", - industryDistribution: "分配", - industryEcoAmount: "金额 (ECO)", - industryEditBlueprint: "编辑蓝图", - industryFacility: "工厂", - industryKindLabel: "类型", - industryKind_labor: "人工", - industryKind_material: "材料", - industryKind_eco: "资金", - industryLaborHours: "人工(小时)", - industryHours: "小时", - industryMaterialItem: "材料", - industryMaterials: "材料", - industryEstMaterials: "材料总计", - industryEstLabor: "人工总计", - industryEstTotal: "预计价格", - industryBuildingPrice: "制造价格", - industryFinalPrice: "最终价格", - industryBlueprintDescPlaceholder: "规格、尺寸、重量、文档……", - industryMaterialValue: "价值(ECO)", - industryMember: "成员", - industryNewBlueprint: "新建蓝图", - industryNewBuild: "提议构建", - industryEditBuild: "编辑生产", - industryNeedBlueprint: "请先创建蓝图:每次生产都基于蓝图。", - industryNoBlueprints: "还没有蓝图。", - industryNoBuilds: "还没有构建。", - industryNote: "备注", - industryOutput: "产出", - industryOutputKind: "产品类型", - industryOutputValue: "产出价值(ECO,例如来自销售)", - industryPostJob: "创建工作", - industryPot: "资金池", - industryProposeBuildButton: "提议构建", - industryProposer: "提议者", - industryAuthor: "作者", - industrySellOnMarket: "发送到市场", - industrySendToProjects: "创建项目", - industrySkills: "技能", - industryTreasury: "资金", - industryKind_physical: "实体", - industryKind_digital: "数字", - industryBuildStatus_PROPOSED: "已提议", - industryBuildStatus_REJECTED: "已拒绝", - industryBuildStatus_APPROVED: "已批准", - industryBuildStatus_STOCKING: "备料中", - industryBuildStatus_IN_PRODUCTION: "生产中", - industryBuildStatus_COMPLETED: "已完成", - industryBuildStatus_FAILED: "已失败", - pmBotIndustry: "IndustryBot", - pmBotHousing: "HousingBot", - pmBotJobs: "JobsBot", - jobsBotMatchTitle: "有一份工作与你的简历匹配", - jobsBotMatchIntro: "有一份工作与你的简历匹配。", - jobsBotMatchJob: "工作", - jobsBotMatchHint: "你收到这条消息是因为你的简历由 AI 管理。可在简历中关闭。", - cvAiManaged: "AI 管理", - switchOn: "开启", - switchOff: "关闭", - cvAiManagedHint: "开启后,当有工作与你的简历匹配时,JobsBot 会通过私信通知你。", - cvMatchThreshold: "达到此匹配度时通知我", - housingBotRequestedTitle: "有人申请了你的一处住所。", - housingBotCancelledTitle: "一处住所的申请已被取消。", - housingBotYouRequestedTitle: "你申请了一处住所。", - housingBotYouCancelledTitle: "你取消了一次申请。", - housingBotUnavailableTitle: "你申请过的住所已不可入住。", - housingBotUnavailableSentTitle: "你已通知申请过该住所的人。", - industryBotAdmittedTitle: "你已被接纳进入一家工厂。", - industryBotApplicationTitle: "你的工厂有新的加入申请。", - industryBotInvitedTitle: "你被邀请加入一家工厂。", - industryBotDissolvedTitle: "你所属的一家工厂已被解散。", - industryBotBuildApprovedTitle: "一个构建已获批准。", - industryBotDistributedTitle: "一个构建的产出已被分配。", - industryFilterRules: "规则", - industryRulesTitle: "规则", - industryRulesIntro: "Industry 让居民集体拥有生产资料:一家工厂是网络所有的作坊,无人私有。", - industryRulesSteward: "创始人是管理人——托管者,而非所有者:不能独自出售、私有化或解散工厂。", - industryRulesMembership: "成员政策决定如何加入:开放(立即加入)、投票(由成员投票接纳)或仅邀请(需成员先邀请)。", - industryRulesQuorum: "法定人数是一项决定生效所需投票的最少成员数,以免小工厂由一人决定。", - industryRulesMajority: "多数是通过所需的投票比例(0.5=一半,1=全体一致);当赞成票达到至少 max(法定人数, 成员×多数) 时决定通过。", - industryRulesDecisions: "成员投票接纳新人、批准构建和解散工厂;管理人不能凌驾于这些集体决定之上。", - industryRulesBlueprints: "蓝图是自由(COPYLEFT)配方——投入(材料、工时、技能)与产出(实体或数字商品)——任何人都可以复刻。", - industryRulesBuilds: "构建是一份生产订单:已提议→已批准(投票)→备料→生产中→已完成,且仅在批准后接受贡献。", - industryRulesContributions: "成员向构建贡献劳动(工时)、材料(申报的 ECO 价值)或 ECO;每次贡献获得 Karma。", - industryRulesLaborRate: "劳动费率是本工厂一工时的 ECO 价值;它把工时转为 Karma,以公平比较劳动与资本:Karma = 工时×费率 + 材料价值 + ECO。", - industryRulesShares: "你在构建中的份额是你的 Karma÷总 Karma,决定你能获得多少产出价值。", - industryRulesDistribution: "生产完成后,管理者将资金池(生产资金加销售价值)按贡献份额分配给贡献者。", - industryRulesTreasury: "ECO 贡献由管理人作为构建资金托管人保管,直到分配;所有流动都记录在网络上,纠纷可提交 Courts。", - industryJobs: "职位", - industryReportButton: "举报", - industryCreateTask: "创建任务", - industryOpenDispute: "发起争议", - industryMaterialsPlaceholder: "solar-panel:2:120\nbattery:1:80\nkit:1:15", - industryCreateInvite: "生成邀请", - industryPauseVotes: "状态票数", - industryTitle: "工业", - industryDescription: "用于集体管理生产资料的模块。", - typeIndustryBuild: "构建", - typeIndustryBlueprint: "蓝图", - typeIndustryAllocation: "分配", - typeIndustry: "工业", - modulesIndustryLabel: "工业", - modulesIndustryDescription: "用于集体管理生产资料的模块。", - industryFilterAll: "全部", - industryFilterMember: "成员", - industryFilterBlueprints: "蓝图", - industryFilterBuilds: "生产", - industryFilterMine: "我的", - industryFilterActive: "运行中", - industryFilterPaused: "已暂停", - industryFilterDissolved: "已解散", - industryAllTitle: "工业", - industryMemberTitle: "我参与的工厂", - industryMineTitle: "我的工厂", - industryActiveTitle: "运行中", - industryPausedTitle: "已暂停的工厂", - industryDissolvedTitle: "已解散的工厂", - industryNoFacilitiesFound: "未找到工厂。", - industryCreateFacility: "创建工厂", - industryMembers: "成员", - industryMemberBadge: "成员", - industryName: "名称", - industryNamePlaceholder: "工厂名称", - industryDescriptionLabel: "描述", - industryDescriptionPlaceholder: "这家工厂生产什么?", - industrySector: "部门", - industryMembershipPolicy: "成员政策", - industryQuorum: "法定人数", - industryMajority: "多数", - industryLaborRate: "劳动费率", - industryCreateButton: "创建工厂", - industryUpdateButton: "更新", - industrySteward: "管理人", - industryStatusActive: "运行中", - industryStatusPaused: "已暂停", - industryStatusDissolved: "已解散", - industryStatusLabel: "状态", - industryJoinButton: "加入", - industryApplyButton: "申请加入", - industryLeaveButton: "退出", - industryInviteButton: "邀请", - industryInvitePlaceholder: "@...=.ed25519", - industryInviteNeeded: "你需要邀请才能加入。", - industryPauseButton: "投票暂停", - industryResumeButton: "投票恢复", - industryDeleteButton: "删除", - industryPendingApplicants: "待处理申请", - industryVotesYes: "赞成", - industryVotesNo: "反对", - industryAlreadyVoted: "已投票", - industryAdmit: "接纳", - industryReject: "拒绝", - industryDissolveTitle: "解散工厂", - industryDissolveVote: "投票解散", - industryPolicy_open: "开放", - industryPolicy_vote: "投票", - industryPolicy_invite: "仅邀请", projectsTitle: "项目", projectsDescription: "创建、资助和关注你网络中的社区驱动项目。", - projectSearchPlaceholder: "搜索项目...", projectCreateProject: "创建项目", projectCreateButton: "创建项目", projectUpdateButton: "更新", @@ -2816,8 +2947,14 @@ module.exports = { projectPledgeTitle: "支持此项目", projectPledgePlaceholder: "ECO 金额", projectBounties: "悬赏", + projectBountiesInputLabel: "悬赏(每行一个:标题|金额 [ECO]|描述)", + projectBountiesPlaceholder: "修复 UI 漏洞|100|问题链接\n编写文档|250|列出使用示例", projectNoBounties: "未找到悬赏。", projectTitle: "标题", + projectAddBountyTitle: "新悬赏", + projectBountyTitle: "悬赏标题", + projectBountyAmount: "金额 (ECO)", + projectBountyDescription: "描述", projectMilestoneSelect: "选择里程碑", projectBountyCreateButton: "创建悬赏", projectBountyStatus: "悬赏状态", @@ -2832,15 +2969,19 @@ module.exports = { projectMilestoneTitle: "里程碑标题", projectMilestoneTargetPercent: "百分比 (%)", projectMilestoneDueDate: "日期", + projectMilestoneCreateButton: "创建里程碑", projectMilestoneStatus: "里程碑状态", projectMilestoneOpen: "开放", projectMilestoneDone: "已完成", projectMilestoneDue: "截止", + projectNoMilestones: "未找到里程碑。", projectMilestoneMarkDone: "标记为已完成", projectMilestoneTitlePlaceholder: "输入里程碑标题", projectMilestoneDescriptionPlaceholder: "输入此里程碑的描述", projectMilestoneDescription: "里程碑描述", + projectBudgetGoal: "预算(目标)", projectBudgetAssigned: "已分配至悬赏", + projectBudgetRemaining: "剩余", projectBudgetOver: "⚠ 超出预算:分配金额超过目标", projectFollowers: "关注者", projectFollowersTitle: "关注者", @@ -2853,6 +2994,7 @@ module.exports = { projectBackersTotalPledged: "质押总额", projectBackersYourPledge: "你的质押", projectBackersNone: "还没有质押。", + projectNoRemainingBudget: "没有剩余预算。", projectFilterBackers: "支持者", projectFilterApplied: "已申请", projectAppliedTitle: "已申请", @@ -2861,15 +3003,30 @@ module.exports = { projectBackerAmount: "总贡献", projectBackerPledges: "质押", projectBackerProjects: "项目", + projectPledgeAmount: "金额", projectSelectMilestoneOrBounty: "选择里程碑或悬赏", projectPledgeButton: "质押", footerLicense: "GPLv3", + footerPackage: "软件包", + footerVersion: "版本", modulesModuleName: "名称", modulesModuleDescription: "描述", modulesModuleStatus: "状态", modulesTotalModulesLabel: "已加载模块", modulesEnabledModulesLabel: "已启用", modulesDisabledModulesLabel: "已禁用", + modulesPopularLabel: "精选", + modulesPopularDescription: "接收热门、最多浏览或最多评论帖子的模块。", + modulesTopicsLabel: "话题", + modulesTopicsDescription: "接收基于共同兴趣的讨论类别的模块。", + modulesSummariesLabel: "摘要", + modulesSummariesDescription: "接收长讨论或帖子摘要的模块。", + modulesLatestLabel: "最新", + modulesLatestDescription: "接收最新帖子和讨论的模块。", + modulesThreadsLabel: "讨论串", + modulesThreadsDescription: "接收按话题或问题分组的对话的模块。", + modulesMultiverseLabel: "多元宇宙", + modulesMultiverseDescription: "接收来自其他联邦节点内容的模块。", modulesInvitesLabel: "邀请", modulesInvitesDescription: "管理和应用邀请码的模块。", modulesWalletLabel: "钱包", @@ -2910,12 +3067,8 @@ module.exports = { modulesOpinionsDescription: "发现和为观点投票的模块。", modulesTransfersLabel: "转账", modulesTransfersDescription: "发现和管理智能合约(转账)的模块。", - modulesFediverseLabel: "多元宇宙", - modulesBlogsLabel: "博客", - modulesBlogsDescription: "用于发现和管理博客的模块。", - modulesPollsLabel: "投票", - modulesPollsDescription: "用于向网络提问并统计答案的模块。", - modulesFediverseDescription: "管理你的其他多元宇宙账户,包括发送和接收内容。", + modulesFediverseLabel: "联邦宇宙", + modulesFediverseDescription: "管理你的其他联邦宇宙账户,包括发送和接收内容。", modulesFeedLabel: "动态", modulesFeedDescription: "发现和分享短文(动态)的模块。", modulesParliamentLabel: "议会", @@ -2951,33 +3104,37 @@ module.exports = { visibilityMakeHidden: "设为隐藏", invitesInhabitantsTitle: "居民", invitesInhabitantsFollow: "关注", - aiNavPlaceholder: "描述你需要什么...", + aiNavPlaceholder: "你想去哪里?", aiNavDisabled: "AI 导航已停用。", - aiNavResultsTitle: "AINav 建议", + aiNavResultsTitle: "AI 导航建议", aiNavQueryLabel: "查询", + aiNavResultsDescription: "选择最符合你需求的路径。", + aiNavResultPath: "路径", + aiNavResultDescription: "你可以做什么", aiNavResultMatch: "匹配度", aiNavResultsEmpty: "无匹配路径,请尝试 /search。", + aiNavSearchInstead: "改为搜索", modulesAINavLabel: "AINav", modulesAINavDescription: "用于以自然语言查询网络内容的模块。", + directConnect: "直接连接", directConnectDescription: "通过输入对方的 IP 地址、端口和公钥直接连接到节点。该节点将被添加为已关注的连接。", peerHost: "IP", peerPort: "端口(默认:8008)", peerPublicKey: "公钥 (@...ed25519)", connectAndFollow: "连接", + deviceSourceLabel: "设备来源", modulesPresetTitle: "常用配置", - workflowsTitle: "工作模式", - workflowsDescription: "工作模式会设定主题与启用的模块。", - workflowsSet: "应用工作模式", - workflow_default: "默认", - workflow_jobs: "工作", - workflow_ruling: "治理", - workflow_politics: "政治", - workflow_social: "社交", - workflow_business: "商务", - workflow_night: "夜间", - workflow_mobile: "移动端", + modulesPreset_minimal: "极简", + modulesPreset_basic: "基础", + modulesPreset_social: "社交", + modulesPreset_economy: "经济", + modulesPreset_full: "完整", statsCarbonFootprintTitle: "碳足迹", + statsCarbonFootprintNetwork: "网络碳足迹", + statsCarbonFootprintYours: "你的碳足迹", statsCarbonTombstone: "墓碑碳足迹", + feedSuccessMsg: "动态发布成功!", + dominantOpinionLabel: "主流观点", uploadMedia: "上传媒体(最大:50MB)", feedOpenDiscussion: "公开讨论", feedDetailTitle: "动态", @@ -3010,13 +3167,14 @@ module.exports = { mapDescriptionLabel: "描述", mapDescriptionPlaceholder: "描述地图或位置...", mapTypeLabel: "地图类型", - mapInviteMode: "邀请", - mapTypeSingle: "单一", - mapTypeOpen: "开放", - mapTypeClosed: "封闭", + mapTypeSingle: "单一(仅初始位置)", + mapTypeOpen: "开放(任何人可添加标记)", + mapTypeClosed: "封闭(仅创建者可添加标记)", mapTagsLabel: "标签", mapTagsPlaceholder: "输入以逗号分隔的标签", mapUrlLabel: "地图链接", + mapPickCoordLabel: "或在网格上选择位置:", + mapMarkersLabel: "标记", mapMarkersTitle: "标记", mapMarkerDefault: "标记", mapMarkerLatLabel: "标记纬度", @@ -3043,9 +3201,14 @@ module.exports = { padTitle: "协作板", modulesPadsLabel: "协作板", modulesPadsDescription: "管理协作文本编辑器的模块。", + padFilterAll: "全部", + padFilterMine: "我的", + padFilterRecent: "最近", + padFilterOpen: "开放", + padFilterClosed: "关闭", padCreate: "创建协作板", - padUpdate: "更新", - padDelete: "删除", + padUpdate: "更新协作板", + padDelete: "删除协作板", padTitleLabel: "标题", padTitlePlaceholder: "输入协作板标题...", padStatusLabel: "状态", @@ -3056,7 +3219,14 @@ module.exports = { padTagsLabel: "标签", padTagsPlaceholder: "标签1, 标签2, ...", padMembersLabel: "成员", + padVisitPad: "访问协作板", + padShareUrl: "分享链接", padCreated: "创建时间", + padAuthor: "作者", + padGenerateCode: "生成邀请码", + padInviteCodeLabel: "邀请码", + padInviteCodePlaceholder: "输入邀请码...", + padValidateInvite: "验证", padStartEditing: "开始编辑!", padEditorPlaceholder: "开始写作...", padSubmitEntry: "提交", @@ -3069,11 +3239,12 @@ module.exports = { padClosedSectionTitle: "关闭协作板", padCreateSectionTitle: "创建新协作板", padUpdateSectionTitle: "更新协作板", + padInviteGenerated: "邀请码已生成", typePad: "协作板", padNew: "新建", padAddFavorite: "添加到收藏", padRemoveFavorite: "从收藏中移除", - padClose: "关闭", + padClose: "关闭记事本", padBackToEditor: "返回编辑器", padSearchPlaceholder: "搜索共享文本...", @@ -3081,6 +3252,8 @@ module.exports = { modulesChatsDescription: "发现和管理加密聊天室的模块。", typeChat: "聊天", typeChatMessage: "聊天消息", + chatLabel: "聊天", + chatMessageLabel: "聊天消息", chatsTitle: "聊天", chatMineSectionTitle: "Your Chats", chatRecentTitle: "Recent Chats", @@ -3090,50 +3263,52 @@ module.exports = { chatDescription: "描述", chatCategory: "类别", chatStatus: "状态", + chatFilterAll: "全部", + chatFilterMine: "我的", + chatFilterRecent: "最近", + chatFilterFavorites: "收藏", + chatFilterOpen: "开放", + chatFilterClosed: "已关闭", chatCreate: "创建聊天", - chatUpdate: "更新", - chatDelete: "删除", + chatUpdate: "更新聊天", + chatDelete: "删除聊天", chatClose: "关闭聊天", chatVisitChat: "访问聊天", chatUntitled: "无标题聊天", chatNoItems: "未找到聊天。", chatParticipants: "参与者", - chatMessagesLabel: "消息", - chatThreadsLabel: "话题", + chatStartChatting: "开始聊天!", + chatGenerateCode: "生成代码", + chatShareUrl: "分享链接", chatCreatedAt: "创建时间", chatSearchPlaceholder: "搜索聊天...", chatStatusOpen: "开放", chatStatusInviteOnly: "仅限邀请", chatStatusClosed: "已关闭", chatSendMessage: "发送", - chatJumpLatest: "最新消息", - chatPickChat: "选择一个聊天以打开", - chatNoneYet: "目前还没有可用的聊天。", - activitySearchPlaceholder: "在动态中搜索...", - trendingSearchPlaceholder: "在热门中搜索...", - opinionsSearchPlaceholder: "在观点中搜索...", - votesSearchPlaceholder: "在表决中搜索...", - welcomeStepUxTitle: "选择你的界面", - welcomeStepUxText: "决定 Oasis 的外观。之后可在设置中更改。", - welcomeStepUxAction: "使用此视图", - chatRateLimitTitle: "消息过多", - chatRateLimitMessage: "你已达到该房间每小时的消息上限,请稍后再试。", chatMessagePlaceholder: "输入您的消息...", chatNoMessages: "暂无消息。", + chatLeave: "离开聊天", + chatInviteCodeLabel: "输入邀请码", + chatJoinByInvite: "加入", chatTitlePlaceholder: "聊天标题", chatDescriptionPlaceholder: "聊天描述", chatTagsPlaceholder: "标签1, 标签2", chatImageLabel: "选择图片文件 (.jpeg, .jpg, .png, .gif)", + chatInviteCode: "邀请码", chatAuthor: "作者", + chatCreated: "创建", chatAddFavorite: "添加到收藏", chatRemoveFavorite: "从收藏中移除", chatPM: "私信", chatStatusLabel: "状态", chatCategoryLabel: "类别", + chatParticipantsLabel: "参与者", gamesTitle: "游戏", gamesDescription: "发现并玩一些游戏。", gamesFilterAll: "全部", gamesPlayButton: "开始游戏!", + gamesBackToGames: "返回游戏", modulesGamesLabel: "游戏", modulesGamesDescription: "用于发现和玩游戏的模块。", modulesGraphosLabel: "Graphos", @@ -3150,6 +3325,8 @@ module.exports = { gamesArkanoidDesc: "用你的球拍和球打破所有砖块。经典街机挑战。", gamesPingPongTitle: "PingPong", gamesPingPongDesc: "与AI对战的经典乒乓球。先得5分者获胜。", + gamesOutrunTitle: "Outrun", + gamesOutrunDesc: "与时间赛跑!躲避交通障碍,在时间用完前到达终点。", gamesAsteroidsTitle: "Asteroids", gamesAsteroidsDesc: "驾驶飞船穿越小行星带。在它们撞上你之前将其摧毁。", gamesRockPaperScissorsTitle: "石头剪刀布", @@ -3170,6 +3347,8 @@ module.exports = { gamesAudioPendulumTitle: "Audio Pendulum", gamesAudioPendulumDesc: "Chaotic physics simulator with real-time audio synthesis. Angular velocities become frequencies, peaks become drum hits. No two simulations sound alike.", gamesTetrisDesc: "Classic falling blocks. Clear lines to score. How long can you last?", + gamesQuakeTitle: "Quake Arena", + gamesQuakeDesc: "First-person raycasting arena. Move and shoot your way through waves of enemies.", gamesFilterScoring: "SCORING", gamesHallOfFame: "Hall of Fame", gamesHallPlayer: "Player", @@ -3180,6 +3359,12 @@ module.exports = { modulesCalendarsLabel: "日历", modulesCalendarsDescription: "用于发现和管理日历的模块。", typeCalendar: "日历", + calendarFilterAll: "全部", + calendarFilterMine: "我的", + calendarFilterOpen: "开放", + calendarFilterClosed: "关闭", + calendarFilterRecent: "最近", + calendarFilterFavorites: "收藏", calendarCreate: "创建日历", calendarUpdate: "更新", calendarDelete: "删除", @@ -3192,17 +3377,24 @@ module.exports = { calendarTagsLabel: "标签", calendarTagsPlaceholder: "标签1, 标签2...", calendarParticipantsLabel: "参与者", + calendarParticipantsCount: "参与者", + calendarVisitCalendar: "访问日历", calendarCreated: "已创建", + calendarAuthor: "作者", calendarJoin: "加入", calendarGenerateInvite: "生成邀请", + calendarInviteCodePlaceholder: "输入邀请码...", + calendarValidateInvite: "验证邀请码", + calendarJoined: "已加入", + calendarAddDate: "添加日期", + calendarAddNote: "添加笔记", calendarDateLabel: "日期", calendarDatePlaceholder: "描述这个日期...", - calendarNoteLabel: "备注", + calendarNoteLabel: "笔记", calendarNotePlaceholder: "添加笔记...", calendarFirstDateLabel: "日期", calendarFirstNoteLabel: "备注", calendarIntervalLabel: "Interval", - calendarIntervalNone: "不重复", calendarIntervalWeekly: "Weekly", calendarIntervalMonthly: "Monthly", calendarIntervalYearly: "Yearly", @@ -3213,6 +3405,8 @@ module.exports = { calendarsDescription: "在您的网络中发现和管理日历。", calendarMonthPrev: "← 上一月", calendarMonthNext: "下一月 →", + calendarMonthLabel: "日期", + calendarsShareUrl: "分享链接", calendarAllSectionTitle: "日历", calendarRecentSectionTitle: "最近的日历", calendarFavoritesSectionTitle: "收藏", @@ -3226,6 +3420,7 @@ module.exports = { calendarRemoveFavorite: "从收藏移除", calendarSearchPlaceholder: "搜索日历...", calendarAddEntry: "添加条目", + calendarLeave: "离开日历", statsCalendar: "日历", statsCalendarDate: "日历日期", statsCalendarNote: "日历笔记", @@ -3272,6 +3467,7 @@ module.exports = { torrentSortOldest: "最旧", torrentSortTop: "最多投票", torrentNoMatch: "没有匹配的种子。", + torrentMessageAuthorButton: "给作者发消息", torrentUpdatedAt: "已更新", statsTorrent: "种子", typeTorrent: "种子", @@ -3279,18 +3475,10 @@ module.exports = { modulesTorrentsDescription: "发现和管理种子的模块。", favoritesFilterTorrents: "种子", favoritesFilterMarket: "市场", - favoritesFilterEvents: "活动", - favoritesFilterForum: "论坛", - favoritesFilterHousing: "住房", - favoritesFilterJobs: "工作", - favoritesFilterProjects: "项目", - favoritesFilterReports: "报告", - favoritesFilterTasks: "任务", - favoritesFilterTransfers: "转账", - favoritesFilterVotes: "投票", favoritesFilterShopProducts: "商店商品", tribeSectionTorrents: "种子", tribeCreateTorrent: "上传种子", + tribeMediaTypeTorrent: "种子", settingsWishTitle: "愿望", settingsWishDesc: "配置您的头像愿望。", settingsWishWhole: "Multiverse", @@ -3301,10 +3489,16 @@ module.exports = { settingsPmVisibilityWhole: "Multiverse", settingsPmVisibilityMutuals: "Only mutual-support", pmMutualNotice: "出站是 UX 保护。入站是关注过滤器。", + pmBlockedNonMutual: "只能向相互支持的居民发送私信。", inhabitantsPendingFollowsTitle: "待处理的支持请求", inhabitantsPendingAccept: "接受", inhabitantsPendingReject: "拒绝", + bxEncrypted: "已加密", + bxEncryptedHexLabel: "密文(预览)", tribeSectionGovernance: "治理", + tribeSubStatusPublic: "公开", + tribeSubStatusPrivate: "私密", + tribeSubInheritedPrivate: "私密(继承自主部落)", tribePadInviteRequired: "您无权访问 pad。请求邀请。", tribeChatInviteRequired: "您无权访问聊天。请求邀请。", tribeGovernanceDesc: "此部落的内部治理。", @@ -3344,31 +3538,44 @@ module.exports = { tribeGovNoHistorical: "暂无任何记录", logsTitle: "日志", logsDescription: "记录您在网络中的体验。", + logsReadMore: "阅读更多", logsViewTitle: "日志", logsColumnType: "类型", logsView: "查看", + logsManualPrompt: "撰写您的日志", logsUpdateButton: "更新", - logsEditTitle: "更新日志", + logsEditTitle: "编辑条目", + logsCreateDescription: "记录您的体验...", logsCreateTitle: "创建条目", logsWriteButton: "撰写", logsTextPlaceholder: "描述您的体验...", + logsTextField: "文本", + logsLabelPlaceholder: "标签...", + logsLabelField: "撰写您的日志(标签)", + logsAiDisabledWarn: "在 /modules 中启用 AI 模块以使用 AI 撰写的日志。", + logsAiContextValue: "区块链日", + logsAiContext: "上下文", + logsAiModStatus: "AImod", + logsModeApply: "应用!", logsModeAIWritten: "AI-Assistant", logsModeManual: "手动", logsModeAI: "AI", + logsColumnDelete: "删除", + logsColumnEdit: "编辑", logsColumnLog: "日志", logsColumnDate: "日期", logsDelete: "删除", - logsEdit: "更新", + logsEdit: "编辑", logsFilterAlways: "始终", logsFilterYear: "去年", logsFilterMonth: "上月", logsFilterWeek: "上周", logsFilterToday: "今天", - logsComments: "评论", - logsAuthorEntries: "条目", + logsFilterRecent: "最近", + logsFilterAll: "全部", logsCreate: "创建条目", logsExport: "导出日志", - logsExportOne: "导出日志", + logsExportOne: "导出", logsViewDetails: "查看详情", logsGenerateButton: "生成文本", logsSearchText: "在日志中搜索...", @@ -3378,25 +3585,31 @@ module.exports = { modulesLogsLabel: "日志", modulesLogsDescription: "用于(通过 AI 助手)记录体验的模块。", statsLogsTitle: "日志", + statsLogsEntries: "条目", typeLog: "日志", + blockchainCycle: "周期", larpTitle: "L.A.R.P.", larpDescription: "用于协作实验的实时角色扮演层。", - larpNoHousesMatch: "没有匹配的家族。", - larpSearchPlaceholder: "搜索家族...", larpAllHouses: "家族:", larpFilterRuling: "执政", larpFilterHouses: "家族", larpFilterRules: "FAQ", larpRulesTitle: "L.A.R.P. FAQ", + larpRulesEntryTitle: "起始家族", larpRulesEntryText: "每位居民默认从 ACADEMIA 开始。在 ACADEMIA 中,从“加入家族”面板选择一个家族:这将打开其入会测试。回答 10 道问题并提交。若达到阈值(7/10),你将被自动接纳进入该家族;否则将被拒绝,留在 ACADEMIA 中。无论如何,系统会记录你的 OasisID 和尝试周期,并在 30 个周期的冷却期结束前阻止再次尝试。", + larpRulesLeaveText: "任何家族的成员都可以随时从其家族页面返回 ACADEMIA;这样会重置其成员身份,但不会取消测试的冷却时间。", + larpRulesGovernanceTitle: "执政墙", larpRulesGovernanceText: "在其执政周期内,执政家族佩戴“执政”徽章,是唯一其墙面在“执政”标签下公开显示的家族。", - larpRulesWallText: "每个家族都有一面墙,供其成员书写。家族墙对外不公开,但执政家族的墙和 ACADEMIA 的墙人人可读。", + larpRulesSignTitle: "L.A.R.P. 徽章", larpRulesSignText: "居民可在 /profile/edit > 传感器 中启用,将当前家族的图像作为徽记显示在个人资料、作者卡和居民卡上。徽记链接到家族页面。", larpPostsTitle: "墙", larpPostsEmpty: "暂无发布。", + larpPostLabel: "新发布", larpPostPlaceholder: "这个家族想说什么?", + larpPostPublic: "公开(所有人可见,否则仅限成员)", larpPostSubmit: "发布", + larpPostMembersOnly: "仅成员", larpCycleLabel: "周期:", audioBackToBcs: "Back to BCS", audioFilterBcs: "BCS", @@ -3513,9 +3726,12 @@ module.exports = { bankTaxesLookupNote: "输入区块 ID 在本地数据流中查询。", bankTaxesUserNoteChipsClick: "点击任何标签以在 blockexplorer 中检查其区块。", mapUrlPlaceholder: "/maps/MAP_ID", + larpRulesInviteEntry: "邀请码", larpRulesInviteEntryText: "家族成员可以生成邀请码,提供对家族的直接访问。", larpRulesOneHouseText: "你在任何时刻最多只能属于一个家族。如果你不属于任何家族,你就在 ACADEMIA。每个非 ACADEMIA 的家族页面都向其成员显示一个\"离开家族\"按钮,将成员身份重置为 ACADEMIA。离开并不会取消测试冷却。", - larpRulesTestEntryText: "在 WILL 测试中,每个选项会为一个或多个家族加权;提交后系统统计得分,并自动把你录入得分最高的家族。", + larpRulesOneHouseTitle: "一次一个家族", + larpRulesTestEntry: "WILL 测试", + larpRulesTestEntryText: "每个选项对一个或多个家族加权;提交时,系统统计得分并自动将你纳入得分最高的家族。", larpWillTestHint: "完成后,你将被分配到与你的回答最匹配的家族。你的单个回答在本地处理,从不存储 — 只有最终的家族分配会发布到你的信息流。", larpWillTestTitle: "WILL 测试", settingsLanDesc: "定期向同一局域网中的其他 Oasis 实例广播此节点。禁用以停止 UDP 广播。", @@ -3534,27 +3750,31 @@ module.exports = { aiExchangeMarkHelpful: "+1 有帮助", larpCyclesUntilRuling: "下一执政周期", larpVisitTribe: "访问部落", - larpStartJourney: "开始旅程", larpGoverning: "执政:", - larpStartHere: "从这里开始:", larpMyHouse: "我的家族:", larpMembersCount: "成员", + larpMembersTitle: "成员", + larpNoMembers: "暂无成员。", larpRolesLabel: "角色", larpFunctionLabel: "功能", larpMonthLabel: "执政周期", larpLeaveToAcademia: "返回 ACADEMIA", - larpAcademiaJoinTitle: "L.A.R.P. 家族", + larpAcademiaJoinTitle: "加入家族", + larpAcademiaJoinHint: "进行心理画像测试以被分配到最适合你的家族,或使用家族成员分享的邀请码。", + larpTakeTestButton: "进行画像测试", larpInviteRedeem: "加入家族", larpInviteCreate: "生成邀请码", larpInvitePlaceholder: "邀请码", larpInviteBannerTitle: "新邀请码", larpInviteBannerHint: "将此代码分享给 ACADEMIA 中的某人。30 个周期后过期,或使用一次后失效。", + larpTestAssignedHouse: "已分配家族", larpTestRankingTitle: "各家族分数", invitesHousesTitle: "L.A.R.P.", invitesHouseInviteCodePlaceholder: "家族邀请码", invitesHouseJoinButton: "加入家族", larpLastAttemptTitle: "你最近的尝试", larpLastAttemptHouse: "家族", + larpLastAttemptResult: "结果", larpLastAttemptWhen: "时间", larpLastAttemptCooldown: "下次尝试还需", larpTestTitle: "入会测试", @@ -3563,14 +3783,37 @@ module.exports = { larpTestCooldownActive: "下次测试可用还需", larpTestCooldownDays: "周期", larpTestResultTitle: "测试结果", + larpTestPassed: "测试通过 — 欢迎!", + larpTestFailed: "测试未通过", larpTestScore: "得分", larpTestNextAttempt: "你可以在 30 个周期后再次尝试测试。", larpGoToHouse: "前往你的家族", larpBackToAcademia: "返回 ACADEMIA", + larpBackToHouses: "◀ 家族", larpBadgeYou: "你", larpBadgeRuling: "执政", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Oasis 实时角色扮演层(9 个家族)的模块。", + melodyUploadTitlePlaceholder: "Composition title", + melodyUploadDescPlaceholder: "Optional description", + ecoinTaxLabel: "ECOin Tax", + bankTaxesNetworkTitle: "Network Taxes", + bankTaxesUserTitle: "Your taxes", + bankTaxesUserNote: "Your ECO tax is deducted from the surplus the network generates on top of your UBI; the base UBI value is set as an immovable minimum. The tax is never deducted from the fixed amount that corresponds to each inhabitant as UBI. The deducted funds feed the wealth-redistribution algorithm and are channeled back to other inhabitants via their UBI claims. Those other inhabitants who receive more UBI for their projects will likely have some task dedicated to helping you reduce your ECO tax. Or they may dedicate themselves to reducing the ECO tax directly, and their ongoing contribution may be required by networking consensus.", + bankTaxesArchTaxNote: "ARCH Tax reflects the cost of growing and maintaining the network archive. It is computed from the time gap between your first published block and the newest block in the network — the longer your history has lived in the archive, the larger your share of the maintenance cost. Like ECO Tax, ARCH Tax is deducted from the surplus the network adds on top of your UBI; the base UBI is never reduced by it.", + bankTaxesExample: "Example", + bankTaxesSummaryNote: "Aggregated total across every tax type the network applies. Currently only ECO Tax (carbon footprint) is enabled; future tax types will appear as additional rows and contribute to the total.", + statsEcoTaxTitle: "ECO Tax", + audioTranscodeTitle: "BCS Transcode", + audioTranscodeDescription: "Interpretation of the audio: the original blockchain composition that produced it, plus the OasisID, timestamp and hidden message embedded into the waveform via steganography.", + audioTranscodeStegoTitle: "Embedded in the waveform", + audioBackToAudio: "Back to audio", + audioAuthorLabel: "Author", + mapZoomIn: "Zoom +", + mapZoomOut: "Zoom −", + mapClickToCreate: "Click on the map to select a location", + mapClickToAddMarker: "Click on the map to add a marker", + gameScoreLabel: "GAME SCORES", larpProfileQ1: "面对复杂问题时,你首先做什么?", larpProfileQ1O1: "与他人交谈以达成共识", larpProfileQ1O2: "构建原型进行测试", diff --git a/src/client/middleware.js b/src/client/middleware.js index 86147d0..2801b1c 100644 --- a/src/client/middleware.js +++ b/src/client/middleware.js @@ -42,7 +42,7 @@ const collectLocalIPs = () => { module.exports = ({ host, port, middleware, allowHost }) => { const assets = new Koa() - assets.use(koaStatic(join(__dirname, "..", "client", "assets"), { maxage: 60 * 60 * 1000 })); + assets.use(koaStatic(join(__dirname, "..", "client", "assets"))); const app = new Koa(); const validHosts = []; @@ -79,14 +79,12 @@ module.exports = ({ host, port, middleware, allowHost }) => { return true; }; - const httpDebug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; - app.on("error", (err, ctx) => { if (err && (err.code === 'ECONNRESET' || err.code === 'EPIPE')) { return; } if (err && (err.name === 'BadRequestError' || err.status === 400)) { - if (httpDebug) console.error(`[400] ${err.message}`); + console.error(`[400] ${err.message}`); return null; } console.error(err); @@ -116,8 +114,8 @@ module.exports = ({ host, port, middleware, allowHost }) => { app.use(koaStatic(path.join(__dirname, 'public'))); app.use(async (ctx, next) => { - - if (httpDebug) console.log(`[http] ${ctx.method} ${ctx.path}`); + + //console.log("Requesting:", ctx.path); // uncomment to check for HTTP requests const isClearnet = isClearnetPath(ctx.request); const csp = isClearnet diff --git a/src/client/public/js/karvan.js b/src/client/public/js/karvan.js deleted file mode 100644 index 04a4797..0000000 --- a/src/client/public/js/karvan.js +++ /dev/null @@ -1,316 +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 ? "🔒 P2P (WebRTC)" : "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" : ""); } - 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(" · ") : ""; } - function updateBtns() { - if (btnMic) btnMic.className = "kc-btn" + (micActive ? " on" : ""); - if (btnCam) btnCam.className = "kc-btn" + (camActive ? " on" : ""); - } - - 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("⛔ Cámara/micro denegados (en el móvil, pendiente del wrapper). El chat sigue."); - 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); }); - } - function startCall() { - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; } - setStatus("📞 pidiendo cámara y micro…"); - navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function (s) { - 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() { - 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); - }); - } -})(); diff --git a/src/client/public/js/oasis-mobile-nav.js b/src/client/public/js/oasis-mobile-nav.js new file mode 100644 index 0000000..2878122 --- /dev/null +++ b/src/client/public/js/oasis-mobile-nav.js @@ -0,0 +1,36 @@ +(function () { + function mark() { + var p = (location.pathname || "/").replace(/\/+$/, "") || "/"; + var links = document.querySelectorAll(".oasis-bottombar a.bb-item"); + var best = null, bestLen = -1; + links.forEach(function (a) { + var h = (a.getAttribute("href") || "").replace(/\/+$/, "") || "/"; + if (h === "/") return; + if (p === h || p.indexOf(h + "/") === 0) { + if (h.length > bestLen) { best = a; bestLen = h.length; } + } + }); + if (best) best.classList.add("current"); + } + + function onToggle(e) { + var t = e.target; + if (!t || !t.classList || !t.classList.contains("oasis-nav-toggle")) return; + if (!t.checked) return; + var group = t.closest(".oasis-nav-group"); + var scope = group && group.parentElement; + if (!scope) return; + var others = scope.querySelectorAll( + ":scope > .oasis-nav-group > .oasis-nav-toggle" + ); + others.forEach(function (o) { if (o !== t) o.checked = false; }); + } + + function init() { + mark(); + document.addEventListener("change", onToggle); + } + + if (document.readyState !== "loading") init(); + else document.addEventListener("DOMContentLoaded", init); +})(); diff --git a/src/client/public/js/pdf-viewer.js b/src/client/public/js/pdf-viewer.js deleted file mode 100644 index 11265ff..0000000 --- a/src/client/public/js/pdf-viewer.js +++ /dev/null @@ -1,76 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - if (typeof pdfjsLib === 'undefined') return; - pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdf.worker.min.mjs'; - - document.querySelectorAll('.pdf-viewer-container').forEach(async container => { - const pdfUrl = container.getAttribute('data-pdf-url'); - if (!pdfUrl) return; - - const pdf = await pdfjsLib.getDocument(pdfUrl).promise; - let currentPage = 1; - let scale = 1.5; - let rotation = 0; - - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - container.innerHTML = ''; - container.appendChild(canvas); - - const controls = document.createElement('div'); - controls.className = 'pdf-controls'; - controls.innerHTML = ` - - - - - - - - - `; - - container.appendChild(controls); - - const renderPage = async (num) => { - const page = await pdf.getPage(num); - const viewport = page.getViewport({ scale, rotation }); - canvas.width = viewport.width; - canvas.height = viewport.height; - ctx.clearRect(0, 0, canvas.width, canvas.height); - await page.render({ canvasContext: ctx, viewport }).promise; - }; - - const goToPage = (delta) => { - const newPage = currentPage + delta; - if (newPage >= 1 && newPage <= pdf.numPages) { - currentPage = newPage; - renderPage(currentPage); - } - }; - - renderPage(currentPage); - - controls.querySelector('#prev').onclick = () => goToPage(-1); - controls.querySelector('#next').onclick = () => goToPage(1); - controls.querySelector('#zoomIn').onclick = () => { scale += 0.2; renderPage(currentPage); }; - controls.querySelector('#zoomOut').onclick = () => { scale = Math.max(0.5, scale - 0.2); renderPage(currentPage); }; - controls.querySelector('#rotate').onclick = () => { rotation = (rotation + 90) % 360; renderPage(currentPage); }; - controls.querySelector('#download').onclick = () => { - const a = document.createElement('a'); - a.href = pdfUrl; - a.download = 'document.pdf'; - a.click(); - }; - controls.querySelector('#fullscreen').onclick = () => { - if (canvas.requestFullscreen) canvas.requestFullscreen(); - else if (canvas.webkitRequestFullscreen) canvas.webkitRequestFullscreen(); - else if (canvas.mozRequestFullScreen) canvas.mozRequestFullScreen(); - else if (canvas.msRequestFullscreen) canvas.msRequestFullscreen(); - }; - controls.querySelector('#metadata').onclick = async () => { - const info = await pdf.getMetadata(); - alert(`Title: ${info.info.Title || 'N/A'}\nAuthor: ${info.info.Author || 'N/A'}\nPDF Producer: ${info.info.Producer || 'N/A'}`); - }; - }); -}); - diff --git a/src/client/public/js/pdf.min.mjs b/src/client/public/js/pdf.min.mjs deleted file mode 100644 index 07dd89e..0000000 --- a/src/client/public/js/pdf.min.mjs +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @licstart The following is the entire license notice for the - * JavaScript code in this page - * - * Copyright 2024 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @licend The above is the entire license notice for the - * JavaScript code in this page - */ -const t=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),e=[.001,0,0,.001,0,0],i=1.35,s=1,n=2,a=4,r=16,o=32,l=64,h=128,d=256,c={DISABLE:0,ENABLE:1,ENABLE_FORMS:2,ENABLE_STORAGE:3},u={DISABLE:-1,NONE:0,FREETEXT:3,HIGHLIGHT:9,STAMP:13,INK:15,SIGNATURE:101},p={RESIZE:1,CREATE:2,FREETEXT_SIZE:11,FREETEXT_COLOR:12,FREETEXT_OPACITY:13,INK_COLOR:21,INK_THICKNESS:22,INK_OPACITY:23,HIGHLIGHT_COLOR:31,HIGHLIGHT_DEFAULT_COLOR:32,HIGHLIGHT_THICKNESS:33,HIGHLIGHT_FREE:34,HIGHLIGHT_SHOW_ALL:35,DRAW_STEP:41},g={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},f=0,m=1,b=2,A=3,w=3,y=4,v={GRAYSCALE_1BPP:1,RGB_24BPP:2,RGBA_32BPP:3},_={TEXT:1,LINK:2,FREETEXT:3,LINE:4,SQUARE:5,CIRCLE:6,POLYGON:7,POLYLINE:8,HIGHLIGHT:9,UNDERLINE:10,SQUIGGLY:11,STRIKEOUT:12,STAMP:13,CARET:14,INK:15,POPUP:16,FILEATTACHMENT:17,SOUND:18,MOVIE:19,WIDGET:20,SCREEN:21,PRINTERMARK:22,TRAPNET:23,WATERMARK:24,THREED:25,REDACT:26},x=1,E=2,S=3,C=4,T=5,M={ERRORS:0,WARNINGS:1,INFOS:5},D={dependency:1,setLineWidth:2,setLineCap:3,setLineJoin:4,setMiterLimit:5,setDash:6,setRenderingIntent:7,setFlatness:8,setGState:9,save:10,restore:11,transform:12,moveTo:13,lineTo:14,curveTo:15,curveTo2:16,curveTo3:17,closePath:18,rectangle:19,stroke:20,closeStroke:21,fill:22,eoFill:23,fillStroke:24,eoFillStroke:25,closeFillStroke:26,closeEOFillStroke:27,endPath:28,clip:29,eoClip:30,beginText:31,endText:32,setCharSpacing:33,setWordSpacing:34,setHScale:35,setLeading:36,setFont:37,setTextRenderingMode:38,setTextRise:39,moveText:40,setLeadingMoveText:41,setTextMatrix:42,nextLine:43,showText:44,showSpacedText:45,nextLineShowText:46,nextLineSetSpacingShowText:47,setCharWidth:48,setCharWidthAndBounds:49,setStrokeColorSpace:50,setFillColorSpace:51,setStrokeColor:52,setStrokeColorN:53,setFillColor:54,setFillColorN:55,setStrokeGray:56,setFillGray:57,setStrokeRGBColor:58,setFillRGBColor:59,setStrokeCMYKColor:60,setFillCMYKColor:61,shadingFill:62,beginInlineImage:63,beginImageData:64,endInlineImage:65,paintXObject:66,markPoint:67,markPointProps:68,beginMarkedContent:69,beginMarkedContentProps:70,endMarkedContent:71,beginCompat:72,endCompat:73,paintFormXObjectBegin:74,paintFormXObjectEnd:75,beginGroup:76,endGroup:77,beginAnnotation:80,endAnnotation:81,paintImageMaskXObject:83,paintImageMaskXObjectGroup:84,paintImageXObject:85,paintInlineImageXObject:86,paintInlineImageXObjectGroup:87,paintImageXObjectRepeat:88,paintImageMaskXObjectRepeat:89,paintSolidColorImageMask:90,constructPath:91,setStrokeTransparent:92,setFillTransparent:93,rawFillPath:94},P=0,k=1,I=2,R=3,F={NEED_PASSWORD:1,INCORRECT_PASSWORD:2};let L=M.WARNINGS;function setVerbosityLevel(t){Number.isInteger(t)&&(L=t)}function getVerbosityLevel(){return L}function info(t){L>=M.INFOS&&console.log(`Info: ${t}`)}function warn(t){L>=M.WARNINGS&&console.log(`Warning: ${t}`)}function unreachable(t){throw new Error(t)}function assert(t,e){t||unreachable(e)}function createValidAbsoluteUrl(t,e=null,i=null){if(!t)return null;if(i&&"string"==typeof t){if(i.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e?.length>=2&&(t=`http://${t}`)}if(i.tryConvertEncoding)try{t=function stringToUTF8String(t){return decodeURIComponent(escape(t))}(t)}catch{}}const s=e?URL.parse(t,e):URL.parse(t);return function _isValidProtocol(t){switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(s)?s:null}function updateUrlHash(t,e,i=!1){const s=URL.parse(t);if(s){s.hash=e;return s.href}return i&&createValidAbsoluteUrl(t,"http://example.com")?t.split("#",1)[0]+""+(e?`#${e}`:""):""}function shadow(t,e,i,s=!1){Object.defineProperty(t,e,{value:i,enumerable:!s,configurable:!0,writable:!1});return i}const O=function BaseExceptionClosure(){function BaseException(t,e){this.message=t;this.name=e}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends O{constructor(t,e){super(t,"PasswordException");this.code=e}}class UnknownErrorException extends O{constructor(t,e){super(t,"UnknownErrorException");this.details=e}}class InvalidPDFException extends O{constructor(t){super(t,"InvalidPDFException")}}class ResponseException extends O{constructor(t,e,i){super(t,"ResponseException");this.status=e;this.missing=i}}class FormatError extends O{constructor(t){super(t,"FormatError")}}class AbortException extends O{constructor(t){super(t,"AbortException")}}function bytesToString(t){"object"==typeof t&&void 0!==t?.length||unreachable("Invalid argument for bytesToString");const e=t.length,i=8192;if(et.toString(16).padStart(2,"0")));class Util{static makeHexColor(t,e,i){return`#${N[t]}${N[e]}${N[i]}`}static scaleMinMax(t,e){let i;if(t[0]){if(t[0]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[0];e[2]*=t[0];if(t[3]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[3];e[3]*=t[3]}else{i=e[0];e[0]=e[1];e[1]=i;i=e[2];e[2]=e[3];e[3]=i;if(t[1]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[1];e[3]*=t[1];if(t[2]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[2];e[2]*=t[2]}e[0]+=t[4];e[1]+=t[5];e[2]+=t[4];e[3]+=t[5]}static transform(t,e){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],t[0]*e[4]+t[2]*e[5]+t[4],t[1]*e[4]+t[3]*e[5]+t[5]]}static applyTransform(t,e,i=0){const s=t[i],n=t[i+1];t[i]=s*e[0]+n*e[2]+e[4];t[i+1]=s*e[1]+n*e[3]+e[5]}static applyTransformToBezier(t,e,i=0){const s=e[0],n=e[1],a=e[2],r=e[3],o=e[4],l=e[5];for(let e=0;e<6;e+=2){const h=t[i+e],d=t[i+e+1];t[i+e]=h*s+d*a+o;t[i+e+1]=h*n+d*r+l}}static applyInverseTransform(t,e){const i=t[0],s=t[1],n=e[0]*e[3]-e[1]*e[2];t[0]=(i*e[3]-s*e[2]+e[2]*e[5]-e[4]*e[3])/n;t[1]=(-i*e[1]+s*e[0]+e[4]*e[1]-e[5]*e[0])/n}static axialAlignedBoundingBox(t,e,i){const s=e[0],n=e[1],a=e[2],r=e[3],o=e[4],l=e[5],h=t[0],d=t[1],c=t[2],u=t[3];let p=s*h+o,g=p,f=s*c+o,m=f,b=r*d+l,A=b,w=r*u+l,y=w;if(0!==n||0!==a){const t=n*h,e=n*c,i=a*d,s=a*u;p+=i;m+=i;f+=s;g+=s;b+=t;y+=t;w+=e;A+=e}i[0]=Math.min(i[0],p,f,g,m);i[1]=Math.min(i[1],b,w,A,y);i[2]=Math.max(i[2],p,f,g,m);i[3]=Math.max(i[3],b,w,A,y)}static inverseTransform(t){const e=t[0]*t[3]-t[1]*t[2];return[t[3]/e,-t[1]/e,-t[2]/e,t[0]/e,(t[2]*t[5]-t[4]*t[3])/e,(t[4]*t[1]-t[5]*t[0])/e]}static singularValueDecompose2dScale(t,e){const i=t[0],s=t[1],n=t[2],a=t[3],r=i**2+s**2,o=i*n+s*a,l=n**2+a**2,h=(r+l)/2,d=Math.sqrt(h**2-(r*l-o**2));e[0]=Math.sqrt(h+d||1);e[1]=Math.sqrt(h-d||1)}static normalizeRect(t){const e=t.slice(0);if(t[0]>t[2]){e[0]=t[2];e[2]=t[0]}if(t[1]>t[3]){e[1]=t[3];e[3]=t[1]}return e}static intersect(t,e){const i=Math.max(Math.min(t[0],t[2]),Math.min(e[0],e[2])),s=Math.min(Math.max(t[0],t[2]),Math.max(e[0],e[2]));if(i>s)return null;const n=Math.max(Math.min(t[1],t[3]),Math.min(e[1],e[3])),a=Math.min(Math.max(t[1],t[3]),Math.max(e[1],e[3]));return n>a?null:[i,n,s,a]}static pointBoundingBox(t,e,i){i[0]=Math.min(i[0],t);i[1]=Math.min(i[1],e);i[2]=Math.max(i[2],t);i[3]=Math.max(i[3],e)}static rectBoundingBox(t,e,i,s,n){n[0]=Math.min(n[0],t,i);n[1]=Math.min(n[1],e,s);n[2]=Math.max(n[2],t,i);n[3]=Math.max(n[3],e,s)}static#t(t,e,i,s,n,a,r,o,l,h){if(l<=0||l>=1)return;const d=1-l,c=l*l,u=c*l,p=d*(d*(d*t+3*l*e)+3*c*i)+u*s,g=d*(d*(d*n+3*l*a)+3*c*r)+u*o;h[0]=Math.min(h[0],p);h[1]=Math.min(h[1],g);h[2]=Math.max(h[2],p);h[3]=Math.max(h[3],g)}static#e(t,e,i,s,n,a,r,o,l,h,d,c){if(Math.abs(l)<1e-12){Math.abs(h)>=1e-12&&this.#t(t,e,i,s,n,a,r,o,-d/h,c);return}const u=h**2-4*d*l;if(u<0)return;const p=Math.sqrt(u),g=2*l;this.#t(t,e,i,s,n,a,r,o,(-h+p)/g,c);this.#t(t,e,i,s,n,a,r,o,(-h-p)/g,c)}static bezierBoundingBox(t,e,i,s,n,a,r,o,l){l[0]=Math.min(l[0],t,r);l[1]=Math.min(l[1],e,o);l[2]=Math.max(l[2],t,r);l[3]=Math.max(l[3],e,o);this.#e(t,i,n,r,e,s,a,o,3*(3*(i-n)-t+r),6*(t-2*i+n),3*(i-t),l);this.#e(t,i,n,r,e,s,a,o,3*(3*(s-a)-e+o),6*(e-2*s+a),3*(s-e),l)}}let B=null,H=null;function normalizeUnicode(t){if(!B){B=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;H=new Map([["ſt","ſt"]])}return t.replaceAll(B,((t,e,i)=>e?e.normalize("NFKC"):H.get(i)))}function getUuid(){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();const t=new Uint8Array(32);crypto.getRandomValues(t);return bytesToString(t)}const U="pdfjs_internal_id_";function MathClamp(t,e,i){return Math.min(Math.max(t,e),i)}function toBase64Util(t){return Uint8Array.prototype.toBase64?t.toBase64():btoa(bytesToString(t))}"function"!=typeof Promise.try&&(Promise.try=function(t,...e){return new Promise((i=>{i(t(...e))}))});"function"!=typeof Math.sumPrecise&&(Math.sumPrecise=function(t){return t.reduce(((t,e)=>t+e),0)});const z="http://www.w3.org/2000/svg";class PixelsPerInch{static CSS=96;static PDF=72;static PDF_TO_CSS_UNITS=this.CSS/this.PDF}async function fetchData(t,e="text"){if(isValidFetchUrl(t,document.baseURI)){const i=await fetch(t);if(!i.ok)throw new Error(i.statusText);switch(e){case"arraybuffer":return i.arrayBuffer();case"blob":return i.blob();case"json":return i.json()}return i.text()}return new Promise(((i,s)=>{const n=new XMLHttpRequest;n.open("GET",t,!0);n.responseType=e;n.onreadystatechange=()=>{if(n.readyState===XMLHttpRequest.DONE)if(200!==n.status&&0!==n.status)s(new Error(n.statusText));else{switch(e){case"arraybuffer":case"blob":case"json":i(n.response);return}i(n.responseText)}};n.send(null)}))}class PageViewport{constructor({viewBox:t,userUnit:e,scale:i,rotation:s,offsetX:n=0,offsetY:a=0,dontFlip:r=!1}){this.viewBox=t;this.userUnit=e;this.scale=i;this.rotation=s;this.offsetX=n;this.offsetY=a;i*=e;const o=(t[2]+t[0])/2,l=(t[3]+t[1])/2;let h,d,c,u,p,g,f,m;(s%=360)<0&&(s+=360);switch(s){case 180:h=-1;d=0;c=0;u=1;break;case 90:h=0;d=1;c=1;u=0;break;case 270:h=0;d=-1;c=-1;u=0;break;case 0:h=1;d=0;c=0;u=-1;break;default:throw new Error("PageViewport: Invalid rotation, must be a multiple of 90 degrees.")}if(r){c=-c;u=-u}if(0===h){p=Math.abs(l-t[1])*i+n;g=Math.abs(o-t[0])*i+a;f=(t[3]-t[1])*i;m=(t[2]-t[0])*i}else{p=Math.abs(o-t[0])*i+n;g=Math.abs(l-t[1])*i+a;f=(t[2]-t[0])*i;m=(t[3]-t[1])*i}this.transform=[h*i,d*i,c*i,u*i,p-h*i*o-c*i*l,g-d*i*o-u*i*l];this.width=f;this.height=m}get rawDims(){const t=this.viewBox;return shadow(this,"rawDims",{pageWidth:t[2]-t[0],pageHeight:t[3]-t[1],pageX:t[0],pageY:t[1]})}clone({scale:t=this.scale,rotation:e=this.rotation,offsetX:i=this.offsetX,offsetY:s=this.offsetY,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.viewBox.slice(),userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}convertToViewportPoint(t,e){const i=[t,e];Util.applyTransform(i,this.transform);return i}convertToViewportRectangle(t){const e=[t[0],t[1]];Util.applyTransform(e,this.transform);const i=[t[2],t[3]];Util.applyTransform(i,this.transform);return[e[0],e[1],i[0],i[1]]}convertToPdfPoint(t,e){const i=[t,e];Util.applyInverseTransform(i,this.transform);return i}}class RenderingCancelledException extends O{constructor(t,e=0){super(t,"RenderingCancelledException");this.extraDelay=e}}function isDataScheme(t){const e=t.length;let i=0;for(;i=1&&s<=12?s-1:0;let n=parseInt(e[3],10);n=n>=1&&n<=31?n:1;let a=parseInt(e[4],10);a=a>=0&&a<=23?a:0;let r=parseInt(e[5],10);r=r>=0&&r<=59?r:0;let o=parseInt(e[6],10);o=o>=0&&o<=59?o:0;const l=e[7]||"Z";let h=parseInt(e[8],10);h=h>=0&&h<=23?h:0;let d=parseInt(e[9],10)||0;d=d>=0&&d<=59?d:0;if("-"===l){a+=h;r+=d}else if("+"===l){a-=h;r-=d}return new Date(Date.UTC(i,s,n,a,r,o))}}function getXfaPageViewport(t,{scale:e=1,rotation:i=0}){const{width:s,height:n}=t.attributes.style,a=[0,0,parseInt(s),parseInt(n)];return new PageViewport({viewBox:a,userUnit:1,scale:e,rotation:i})}function getRGB(t){if(t.startsWith("#")){const e=parseInt(t.slice(1),16);return[(16711680&e)>>16,(65280&e)>>8,255&e]}if(t.startsWith("rgb("))return t.slice(4,-1).split(",").map((t=>parseInt(t)));if(t.startsWith("rgba("))return t.slice(5,-1).split(",").map((t=>parseInt(t))).slice(0,3);warn(`Not a valid color format: "${t}"`);return[0,0,0]}function getCurrentTransform(t){const{a:e,b:i,c:s,d:n,e:a,f:r}=t.getTransform();return[e,i,s,n,a,r]}function getCurrentTransformInverse(t){const{a:e,b:i,c:s,d:n,e:a,f:r}=t.getTransform().invertSelf();return[e,i,s,n,a,r]}function setLayerDimensions(t,e,i=!1,s=!0){if(e instanceof PageViewport){const{pageWidth:s,pageHeight:n}=e.rawDims,{style:a}=t,r=util_FeatureTest.isCSSRoundSupported,o=`var(--total-scale-factor) * ${s}px`,l=`var(--total-scale-factor) * ${n}px`,h=r?`round(down, ${o}, var(--scale-round-x))`:`calc(${o})`,d=r?`round(down, ${l}, var(--scale-round-y))`:`calc(${l})`;if(i&&e.rotation%180!=0){a.width=d;a.height=h}else{a.width=h;a.height=d}}s&&t.setAttribute("data-main-rotation",e.rotation)}class OutputScale{constructor(){const{pixelRatio:t}=OutputScale;this.sx=t;this.sy=t}get scaled(){return 1!==this.sx||1!==this.sy}get symmetric(){return this.sx===this.sy}limitCanvas(t,e,i,s){let n=1/0,a=1/0,r=1/0;i>0&&(n=Math.sqrt(i/(t*e)));if(-1!==s){a=s/t;r=s/e}const o=Math.min(n,a,r);if(this.sx>o||this.sy>o){this.sx=o;this.sy=o;return!0}return!1}static get pixelRatio(){return globalThis.devicePixelRatio||1}}const G=["image/apng","image/avif","image/bmp","image/gif","image/jpeg","image/png","image/svg+xml","image/webp","image/x-icon"];class EditorToolbar{#s=null;#n=null;#a;#r=null;#o=null;#l=null;static#h=null;constructor(t){this.#a=t;EditorToolbar.#h||=Object.freeze({freetext:"pdfjs-editor-remove-freetext-button",highlight:"pdfjs-editor-remove-highlight-button",ink:"pdfjs-editor-remove-ink-button",stamp:"pdfjs-editor-remove-stamp-button",signature:"pdfjs-editor-remove-signature-button"})}render(){const t=this.#s=document.createElement("div");t.classList.add("editToolbar","hidden");t.setAttribute("role","toolbar");const e=this.#a._uiManager._signal;t.addEventListener("contextmenu",noContextMenu,{signal:e});t.addEventListener("pointerdown",EditorToolbar.#d,{signal:e});const i=this.#r=document.createElement("div");i.className="buttons";t.append(i);const s=this.#a.toolbarPosition;if(s){const{style:e}=t,i="ltr"===this.#a._uiManager.direction?1-s[0]:s[0];e.insetInlineEnd=100*i+"%";e.top=`calc(${100*s[1]}% + var(--editor-toolbar-vert-offset))`}this.#c();return t}get div(){return this.#s}static#d(t){t.stopPropagation()}#u(t){this.#a._focusEventsAllowed=!1;stopEvent(t)}#p(t){this.#a._focusEventsAllowed=!0;stopEvent(t)}#g(t){const e=this.#a._uiManager._signal;t.addEventListener("focusin",this.#u.bind(this),{capture:!0,signal:e});t.addEventListener("focusout",this.#p.bind(this),{capture:!0,signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e})}hide(){this.#s.classList.add("hidden");this.#n?.hideDropdown()}show(){this.#s.classList.remove("hidden");this.#o?.shown()}#c(){const{editorType:t,_uiManager:e}=this.#a,i=document.createElement("button");i.className="delete";i.tabIndex=0;i.setAttribute("data-l10n-id",EditorToolbar.#h[t]);this.#g(i);i.addEventListener("click",(t=>{e.delete()}),{signal:e._signal});this.#r.append(i)}get#f(){const t=document.createElement("div");t.className="divider";return t}async addAltText(t){const e=await t.render();this.#g(e);this.#r.prepend(e,this.#f);this.#o=t}addColorPicker(t){this.#n=t;const e=t.renderButton();this.#g(e);this.#r.prepend(e,this.#f)}async addEditSignatureButton(t){const e=this.#l=await t.renderEditButton(this.#a);this.#g(e);this.#r.prepend(e,this.#f)}updateEditSignatureButton(t){this.#l&&(this.#l.title=t)}remove(){this.#s.remove();this.#n?.destroy();this.#n=null}}class HighlightToolbar{#r=null;#s=null;#m;constructor(t){this.#m=t}#b(){const t=this.#s=document.createElement("div");t.className="editToolbar";t.setAttribute("role","toolbar");t.addEventListener("contextmenu",noContextMenu,{signal:this.#m._signal});const e=this.#r=document.createElement("div");e.className="buttons";t.append(e);this.#A();return t}#w(t,e){let i=0,s=0;for(const n of t){const t=n.y+n.height;if(ti){s=a;i=t}else e?a>s&&(s=a):a{this.#m.highlightSelection("floating_button")}),{signal:i});this.#r.append(t)}}function bindEvents(t,e,i){for(const s of i)e.addEventListener(s,t[s].bind(t))}class IdManager{#y=0;get id(){return"pdfjs_internal_editor_"+this.#y++}}class ImageManager{#v=getUuid();#y=0;#_=null;static get _isSVGFittingCanvas(){const t=new OffscreenCanvas(1,3).getContext("2d",{willReadFrequently:!0}),e=new Image;e.src='data:image/svg+xml;charset=UTF-8,';return shadow(this,"_isSVGFittingCanvas",e.decode().then((()=>{t.drawImage(e,0,0,1,1,0,0,1,3);return 0===new Uint32Array(t.getImageData(0,0,1,1).data.buffer)[0]})))}async#x(t,e){this.#_||=new Map;let i=this.#_.get(t);if(null===i)return null;if(i?.bitmap){i.refCounter+=1;return i}try{i||={bitmap:null,id:`image_${this.#v}_${this.#y++}`,refCounter:0,isSvg:!1};let t;if("string"==typeof e){i.url=e;t=await fetchData(e,"blob")}else e instanceof File?t=i.file=e:e instanceof Blob&&(t=e);if("image/svg+xml"===t.type){const e=ImageManager._isSVGFittingCanvas,s=new FileReader,n=new Image,a=new Promise(((t,a)=>{n.onload=()=>{i.bitmap=n;i.isSvg=!0;t()};s.onload=async()=>{const t=i.svgUrl=s.result;n.src=await e?`${t}#svgView(preserveAspectRatio(none))`:t};n.onerror=s.onerror=a}));s.readAsDataURL(t);await a}else i.bitmap=await createImageBitmap(t);i.refCounter=1}catch(t){warn(t);i=null}this.#_.set(t,i);i&&this.#_.set(i.id,i);return i}async getFromFile(t){const{lastModified:e,name:i,size:s,type:n}=t;return this.#x(`${e}_${i}_${s}_${n}`,t)}async getFromUrl(t){return this.#x(t,t)}async getFromBlob(t,e){const i=await e;return this.#x(t,i)}async getFromId(t){this.#_||=new Map;const e=this.#_.get(t);if(!e)return null;if(e.bitmap){e.refCounter+=1;return e}if(e.file)return this.getFromFile(e.file);if(e.blobPromise){const{blobPromise:t}=e;delete e.blobPromise;return this.getFromBlob(e.id,t)}return this.getFromUrl(e.url)}getFromCanvas(t,e){this.#_||=new Map;let i=this.#_.get(t);if(i?.bitmap){i.refCounter+=1;return i}const s=new OffscreenCanvas(e.width,e.height);s.getContext("2d").drawImage(e,0,0);i={bitmap:s.transferToImageBitmap(),id:`image_${this.#v}_${this.#y++}`,refCounter:1,isSvg:!1};this.#_.set(t,i);this.#_.set(i.id,i);return i}getSvgUrl(t){const e=this.#_.get(t);return e?.isSvg?e.svgUrl:null}deleteId(t){this.#_||=new Map;const e=this.#_.get(t);if(!e)return;e.refCounter-=1;if(0!==e.refCounter)return;const{bitmap:i}=e;if(!e.url&&!e.file){const t=new OffscreenCanvas(i.width,i.height);t.getContext("bitmaprenderer").transferFromImageBitmap(i);e.blobPromise=t.convertToBlob()}i.close?.();e.bitmap=null}isValidId(t){return t.startsWith(`image_${this.#v}_`)}}class CommandManager{#E=[];#S=!1;#C;#T=-1;constructor(t=128){this.#C=t}add({cmd:t,undo:e,post:i,mustExec:s,type:n=NaN,overwriteIfSameType:a=!1,keepUndo:r=!1}){s&&t();if(this.#S)return;const o={cmd:t,undo:e,post:i,type:n};if(-1===this.#T){this.#E.length>0&&(this.#E.length=0);this.#T=0;this.#E.push(o);return}if(a&&this.#E[this.#T].type===n){r&&(o.undo=this.#E[this.#T].undo);this.#E[this.#T]=o;return}const l=this.#T+1;if(l===this.#C)this.#E.splice(0,1);else{this.#T=l;l=0;e--)if(this.#E[e].type!==t){this.#E.splice(e+1,this.#T-e);this.#T=e;return}this.#E.length=0;this.#T=-1}}destroy(){this.#E=null}}class KeyboardManager{constructor(t){this.buffer=[];this.callbacks=new Map;this.allKeys=new Set;const{isMac:e}=util_FeatureTest.platform;for(const[i,s,n={}]of t)for(const t of i){const i=t.startsWith("mac+");if(e&&i){this.callbacks.set(t.slice(4),{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}else if(!e&&!i){this.callbacks.set(t,{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}}}#M(t){t.altKey&&this.buffer.push("alt");t.ctrlKey&&this.buffer.push("ctrl");t.metaKey&&this.buffer.push("meta");t.shiftKey&&this.buffer.push("shift");this.buffer.push(t.key);const e=this.buffer.join("+");this.buffer.length=0;return e}exec(t,e){if(!this.allKeys.has(e.key))return;const i=this.callbacks.get(this.#M(e));if(!i)return;const{callback:s,options:{bubbles:n=!1,args:a=[],checker:r=null}}=i;if(!r||r(t,e)){s.bind(t,...a,e)();n||stopEvent(e)}}}class ColorManager{static _colorsMapping=new Map([["CanvasText",[0,0,0]],["Canvas",[255,255,255]]]);get _colors(){const t=new Map([["CanvasText",null],["Canvas",null]]);!function getColorValues(t){const e=document.createElement("span");e.style.visibility="hidden";e.style.colorScheme="only light";document.body.append(e);for(const i of t.keys()){e.style.color=i;const s=window.getComputedStyle(e).color;t.set(i,getRGB(s))}e.remove()}(t);return shadow(this,"_colors",t)}convert(t){const e=getRGB(t);if(!window.matchMedia("(forced-colors: active)").matches)return e;for(const[t,i]of this._colors)if(i.every(((t,i)=>t===e[i])))return ColorManager._colorsMapping.get(t);return e}getHexCode(t){const e=this._colors.get(t);return e?Util.makeHexColor(...e):t}}class AnnotationEditorUIManager{#D=new AbortController;#P=null;#k=new Map;#I=new Map;#R=null;#F=null;#L=null;#O=new CommandManager;#N=null;#B=null;#H=0;#U=new Set;#z=null;#G=null;#$=new Set;_editorUndoBar=null;#W=!1;#V=!1;#j=!1;#q=null;#X=null;#K=null;#Y=null;#Q=!1;#J=null;#Z=new IdManager;#tt=!1;#et=!1;#it=null;#st=null;#nt=null;#at=null;#rt=null;#ot=u.NONE;#lt=new Set;#ht=null;#dt=null;#ct=null;#ut=null;#pt={isEditing:!1,isEmpty:!0,hasSomethingToUndo:!1,hasSomethingToRedo:!1,hasSelectedEditor:!1,hasSelectedText:!1};#gt=[0,0];#ft=null;#mt=null;#bt=null;#At=null;static TRANSLATE_SMALL=1;static TRANSLATE_BIG=10;static get _keyboardManager(){const t=AnnotationEditorUIManager.prototype,arrowChecker=t=>t.#mt.contains(document.activeElement)&&"BUTTON"!==document.activeElement.tagName&&t.hasSomethingToControl(),textInputChecker=(t,{target:e})=>{if(e instanceof HTMLInputElement){const{type:t}=e;return"text"!==t&&"number"!==t}return!0},e=this.TRANSLATE_SMALL,i=this.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+a","mac+meta+a"],t.selectAll,{checker:textInputChecker}],[["ctrl+z","mac+meta+z"],t.undo,{checker:textInputChecker}],[["ctrl+y","ctrl+shift+z","mac+meta+shift+z","ctrl+shift+Z","mac+meta+shift+Z"],t.redo,{checker:textInputChecker}],[["Backspace","alt+Backspace","ctrl+Backspace","shift+Backspace","mac+Backspace","mac+alt+Backspace","mac+ctrl+Backspace","Delete","ctrl+Delete","shift+Delete","mac+Delete"],t.delete,{checker:textInputChecker}],[["Enter","mac+Enter"],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#mt.contains(e)&&!t.isEnterHandled}],[[" ","mac+ "],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#mt.contains(document.activeElement)}],[["Escape","mac+Escape"],t.unselectAll],[["ArrowLeft","mac+ArrowLeft"],t.translateSelectedEditors,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t.translateSelectedEditors,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t.translateSelectedEditors,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t.translateSelectedEditors,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t.translateSelectedEditors,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t.translateSelectedEditors,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t.translateSelectedEditors,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t.translateSelectedEditors,{args:[0,i],checker:arrowChecker}]]))}constructor(t,e,i,s,n,a,r,o,l,h,d,c,u,p){const g=this._signal=this.#D.signal;this.#mt=t;this.#bt=e;this.#R=i;this.#dt=s;this._eventBus=n;n._on("editingaction",this.onEditingAction.bind(this),{signal:g});n._on("pagechanging",this.onPageChanging.bind(this),{signal:g});n._on("scalechanging",this.onScaleChanging.bind(this),{signal:g});n._on("rotationchanging",this.onRotationChanging.bind(this),{signal:g});n._on("setpreference",this.onSetPreference.bind(this),{signal:g});n._on("switchannotationeditorparams",(t=>this.updateParams(t.type,t.value)),{signal:g});this.#wt();this.#yt();this.#vt();this.#F=a.annotationStorage;this.#q=a.filterFactory;this.#ct=r;this.#Y=o||null;this.#W=l;this.#V=h;this.#j=d;this.#rt=c||null;this.viewParameters={realScale:PixelsPerInch.PDF_TO_CSS_UNITS,rotation:0};this.isShiftKeyDown=!1;this._editorUndoBar=u||null;this._supportsPinchToZoom=!1!==p}destroy(){this.#At?.resolve();this.#At=null;this.#D?.abort();this.#D=null;this._signal=null;for(const t of this.#I.values())t.destroy();this.#I.clear();this.#k.clear();this.#$.clear();this.#at?.clear();this.#P=null;this.#lt.clear();this.#O.destroy();this.#R?.destroy();this.#dt?.destroy();this.#J?.hide();this.#J=null;this.#nt?.destroy();this.#nt=null;if(this.#X){clearTimeout(this.#X);this.#X=null}if(this.#ft){clearTimeout(this.#ft);this.#ft=null}this._editorUndoBar?.destroy()}combinedSignal(t){return AbortSignal.any([this._signal,t.signal])}get mlManager(){return this.#rt}get useNewAltTextFlow(){return this.#V}get useNewAltTextWhenAddingImage(){return this.#j}get hcmFilter(){return shadow(this,"hcmFilter",this.#ct?this.#q.addHCMFilter(this.#ct.foreground,this.#ct.background):"none")}get direction(){return shadow(this,"direction",getComputedStyle(this.#mt).direction)}get highlightColors(){return shadow(this,"highlightColors",this.#Y?new Map(this.#Y.split(",").map((t=>t.split("=").map((t=>t.trim()))))):null)}get highlightColorNames(){return shadow(this,"highlightColorNames",this.highlightColors?new Map(Array.from(this.highlightColors,(t=>t.reverse()))):null)}setCurrentDrawingSession(t){if(t){this.unselectAll();this.disableUserSelect(!0)}else this.disableUserSelect(!1);this.#B=t}setMainHighlightColorPicker(t){this.#nt=t}editAltText(t,e=!1){this.#R?.editAltText(this,t,e)}getSignature(t){this.#dt?.getSignature({uiManager:this,editor:t})}get signatureManager(){return this.#dt}switchToMode(t,e){this._eventBus.on("annotationeditormodechanged",e,{once:!0,signal:this._signal});this._eventBus.dispatch("showannotationeditorui",{source:this,mode:t})}setPreference(t,e){this._eventBus.dispatch("setpreference",{source:this,name:t,value:e})}onSetPreference({name:t,value:e}){if("enableNewAltTextWhenAddingImage"===t)this.#j=e}onPageChanging({pageNumber:t}){this.#H=t-1}focusMainContainer(){this.#mt.focus()}findParent(t,e){for(const i of this.#I.values()){const{x:s,y:n,width:a,height:r}=i.div.getBoundingClientRect();if(t>=s&&t<=s+a&&e>=n&&e<=n+r)return i}return null}disableUserSelect(t=!1){this.#bt.classList.toggle("noUserSelect",t)}addShouldRescale(t){this.#$.add(t)}removeShouldRescale(t){this.#$.delete(t)}onScaleChanging({scale:t}){this.commitOrRemove();this.viewParameters.realScale=t*PixelsPerInch.PDF_TO_CSS_UNITS;for(const t of this.#$)t.onScaleChanging();this.#B?.onScaleChanging()}onRotationChanging({pagesRotation:t}){this.commitOrRemove();this.viewParameters.rotation=t}#_t({anchorNode:t}){return t.nodeType===Node.TEXT_NODE?t.parentElement:t}#xt(t){const{currentLayer:e}=this;if(e.hasTextLayer(t))return e;for(const e of this.#I.values())if(e.hasTextLayer(t))return e;return null}highlightSelection(t=""){const e=document.getSelection();if(!e||e.isCollapsed)return;const{anchorNode:i,anchorOffset:s,focusNode:n,focusOffset:a}=e,r=e.toString(),o=this.#_t(e).closest(".textLayer"),l=this.getSelectionBoxes(o);if(!l)return;e.empty();const h=this.#xt(o),d=this.#ot===u.NONE,callback=()=>{h?.createAndAddNewEditor({x:0,y:0},!1,{methodOfCreation:t,boxes:l,anchorNode:i,anchorOffset:s,focusNode:n,focusOffset:a,text:r});d&&this.showAllEditors("highlight",!0,!0)};d?this.switchToMode(u.HIGHLIGHT,callback):callback()}#Et(){const t=document.getSelection();if(!t||t.isCollapsed)return;const e=this.#_t(t).closest(".textLayer"),i=this.getSelectionBoxes(e);if(i){this.#J||=new HighlightToolbar(this);this.#J.show(e,i,"ltr"===this.direction)}}addToAnnotationStorage(t){t.isEmpty()||!this.#F||this.#F.has(t.id)||this.#F.setValue(t.id,t)}#St(){const t=document.getSelection();if(!t||t.isCollapsed){if(this.#ht){this.#J?.hide();this.#ht=null;this.#Ct({hasSelectedText:!1})}return}const{anchorNode:e}=t;if(e===this.#ht)return;const i=this.#_t(t).closest(".textLayer");if(i){this.#J?.hide();this.#ht=e;this.#Ct({hasSelectedText:!0});if(this.#ot===u.HIGHLIGHT||this.#ot===u.NONE){this.#ot===u.HIGHLIGHT&&this.showAllEditors("highlight",!0,!0);this.#Q=this.isShiftKeyDown;if(!this.isShiftKeyDown){const t=this.#ot===u.HIGHLIGHT?this.#xt(i):null;t?.toggleDrawing();const e=new AbortController,s=this.combinedSignal(e),pointerup=i=>{if("pointerup"!==i.type||0===i.button){e.abort();t?.toggleDrawing(!0);"pointerup"===i.type&&this.#Tt("main_toolbar")}};window.addEventListener("pointerup",pointerup,{signal:s});window.addEventListener("blur",pointerup,{signal:s})}}}else if(this.#ht){this.#J?.hide();this.#ht=null;this.#Ct({hasSelectedText:!1})}}#Tt(t=""){this.#ot===u.HIGHLIGHT?this.highlightSelection(t):this.#W&&this.#Et()}#wt(){document.addEventListener("selectionchange",this.#St.bind(this),{signal:this._signal})}#Mt(){if(this.#K)return;this.#K=new AbortController;const t=this.combinedSignal(this.#K);window.addEventListener("focus",this.focus.bind(this),{signal:t});window.addEventListener("blur",this.blur.bind(this),{signal:t})}#Dt(){this.#K?.abort();this.#K=null}blur(){this.isShiftKeyDown=!1;if(this.#Q){this.#Q=!1;this.#Tt("main_toolbar")}if(!this.hasSelection)return;const{activeElement:t}=document;for(const e of this.#lt)if(e.div.contains(t)){this.#st=[e,t];e._focusEventsAllowed=!1;break}}focus(){if(!this.#st)return;const[t,e]=this.#st;this.#st=null;e.addEventListener("focusin",(()=>{t._focusEventsAllowed=!0}),{once:!0,signal:this._signal});e.focus()}#vt(){if(this.#it)return;this.#it=new AbortController;const t=this.combinedSignal(this.#it);window.addEventListener("keydown",this.keydown.bind(this),{signal:t});window.addEventListener("keyup",this.keyup.bind(this),{signal:t})}#Pt(){this.#it?.abort();this.#it=null}#kt(){if(this.#N)return;this.#N=new AbortController;const t=this.combinedSignal(this.#N);document.addEventListener("copy",this.copy.bind(this),{signal:t});document.addEventListener("cut",this.cut.bind(this),{signal:t});document.addEventListener("paste",this.paste.bind(this),{signal:t})}#It(){this.#N?.abort();this.#N=null}#yt(){const t=this._signal;document.addEventListener("dragover",this.dragOver.bind(this),{signal:t});document.addEventListener("drop",this.drop.bind(this),{signal:t})}addEditListeners(){this.#vt();this.#kt()}removeEditListeners(){this.#Pt();this.#It()}dragOver(t){for(const{type:e}of t.dataTransfer.items)for(const i of this.#G)if(i.isHandlingMimeForPasting(e)){t.dataTransfer.dropEffect="copy";t.preventDefault();return}}drop(t){for(const e of t.dataTransfer.items)for(const i of this.#G)if(i.isHandlingMimeForPasting(e.type)){i.paste(e,this.currentLayer);t.preventDefault();return}}copy(t){t.preventDefault();this.#P?.commitOrRemove();if(!this.hasSelection)return;const e=[];for(const t of this.#lt){const i=t.serialize(!0);i&&e.push(i)}0!==e.length&&t.clipboardData.setData("application/pdfjs",JSON.stringify(e))}cut(t){this.copy(t);this.delete()}async paste(t){t.preventDefault();const{clipboardData:e}=t;for(const t of e.items)for(const e of this.#G)if(e.isHandlingMimeForPasting(t.type)){e.paste(t,this.currentLayer);return}let i=e.getData("application/pdfjs");if(!i)return;try{i=JSON.parse(i)}catch(t){warn(`paste: "${t.message}".`);return}if(!Array.isArray(i))return;this.unselectAll();const s=this.currentLayer;try{const t=[];for(const e of i){const i=await s.deserialize(e);if(!i)return;t.push(i)}const cmd=()=>{for(const e of t)this.#Rt(e);this.#Ft(t)},undo=()=>{for(const e of t)e.remove()};this.addCommands({cmd,undo,mustExec:!0})}catch(t){warn(`paste: "${t.message}".`)}}keydown(t){this.isShiftKeyDown||"Shift"!==t.key||(this.isShiftKeyDown=!0);this.#ot===u.NONE||this.isEditorHandlingKeyboard||AnnotationEditorUIManager._keyboardManager.exec(this,t)}keyup(t){if(this.isShiftKeyDown&&"Shift"===t.key){this.isShiftKeyDown=!1;if(this.#Q){this.#Q=!1;this.#Tt("main_toolbar")}}}onEditingAction({name:t}){switch(t){case"undo":case"redo":case"delete":case"selectAll":this[t]();break;case"highlightSelection":this.highlightSelection("context_menu")}}#Ct(t){if(Object.entries(t).some((([t,e])=>this.#pt[t]!==e))){this._eventBus.dispatch("annotationeditorstateschanged",{source:this,details:Object.assign(this.#pt,t)});this.#ot===u.HIGHLIGHT&&!1===t.hasSelectedEditor&&this.#Lt([[p.HIGHLIGHT_FREE,!0]])}}#Lt(t){this._eventBus.dispatch("annotationeditorparamschanged",{source:this,details:t})}setEditingState(t){if(t){this.#Mt();this.#kt();this.#Ct({isEditing:this.#ot!==u.NONE,isEmpty:this.#Ot(),hasSomethingToUndo:this.#O.hasSomethingToUndo(),hasSomethingToRedo:this.#O.hasSomethingToRedo(),hasSelectedEditor:!1})}else{this.#Dt();this.#It();this.#Ct({isEditing:!1});this.disableUserSelect(!1)}}registerEditorTypes(t){if(!this.#G){this.#G=t;for(const t of this.#G)this.#Lt(t.defaultPropertiesToUpdate)}}getId(){return this.#Z.id}get currentLayer(){return this.#I.get(this.#H)}getLayer(t){return this.#I.get(t)}get currentPageIndex(){return this.#H}addLayer(t){this.#I.set(t.pageIndex,t);this.#tt?t.enable():t.disable()}removeLayer(t){this.#I.delete(t.pageIndex)}async updateMode(t,e=null,i=!1){if(this.#ot!==t){if(this.#At){await this.#At.promise;if(!this.#At)return}this.#At=Promise.withResolvers();this.#B?.commitOrRemove();this.#ot=t;if(t!==u.NONE){t===u.SIGNATURE&&await(this.#dt?.loadSignatures());this.setEditingState(!0);await this.#Nt();this.unselectAll();for(const e of this.#I.values())e.updateMode(t);if(e){for(const t of this.#k.values())if(t.annotationElementId===e){this.setSelected(t);t.enterInEditMode()}else t.unselect();this.#At.resolve()}else{i&&this.addNewEditorFromKeyboard();this.#At.resolve()}}else{this.setEditingState(!1);this.#Bt();this._editorUndoBar?.hide();this.#At.resolve()}}}addNewEditorFromKeyboard(){this.currentLayer.canCreateNewEmptyEditor()&&this.currentLayer.addNewEditor()}updateToolbar(t){t!==this.#ot&&this._eventBus.dispatch("switchannotationeditormode",{source:this,mode:t})}updateParams(t,e){if(this.#G){switch(t){case p.CREATE:this.currentLayer.addNewEditor(e);return;case p.HIGHLIGHT_DEFAULT_COLOR:this.#nt?.updateColor(e);break;case p.HIGHLIGHT_SHOW_ALL:this._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:{type:"highlight",action:"toggle_visibility"}}});(this.#ut||=new Map).set(t,e);this.showAllEditors("highlight",e)}for(const i of this.#lt)i.updateParams(t,e);for(const i of this.#G)i.updateDefaultParams(t,e)}}showAllEditors(t,e,i=!1){for(const i of this.#k.values())i.editorType===t&&i.show(e);(this.#ut?.get(p.HIGHLIGHT_SHOW_ALL)??!0)!==e&&this.#Lt([[p.HIGHLIGHT_SHOW_ALL,e]])}enableWaiting(t=!1){if(this.#et!==t){this.#et=t;for(const e of this.#I.values()){t?e.disableClick():e.enableClick();e.div.classList.toggle("waiting",t)}}}async#Nt(){if(!this.#tt){this.#tt=!0;const t=[];for(const e of this.#I.values())t.push(e.enable());await Promise.all(t);for(const t of this.#k.values())t.enable()}}#Bt(){this.unselectAll();if(this.#tt){this.#tt=!1;for(const t of this.#I.values())t.disable();for(const t of this.#k.values())t.disable()}}getEditors(t){const e=[];for(const i of this.#k.values())i.pageIndex===t&&e.push(i);return e}getEditor(t){return this.#k.get(t)}addEditor(t){this.#k.set(t.id,t)}removeEditor(t){if(t.div.contains(document.activeElement)){this.#X&&clearTimeout(this.#X);this.#X=setTimeout((()=>{this.focusMainContainer();this.#X=null}),0)}this.#k.delete(t.id);t.annotationElementId&&this.#at?.delete(t.annotationElementId);this.unselect(t);t.annotationElementId&&this.#U.has(t.annotationElementId)||this.#F?.remove(t.id)}addDeletedAnnotationElement(t){this.#U.add(t.annotationElementId);this.addChangedExistingAnnotation(t);t.deleted=!0}isDeletedAnnotationElement(t){return this.#U.has(t)}removeDeletedAnnotationElement(t){this.#U.delete(t.annotationElementId);this.removeChangedExistingAnnotation(t);t.deleted=!1}#Rt(t){const e=this.#I.get(t.pageIndex);if(e)e.addOrRebuild(t);else{this.addEditor(t);this.addToAnnotationStorage(t)}}setActiveEditor(t){if(this.#P!==t){this.#P=t;t&&this.#Lt(t.propertiesToUpdate)}}get#Ht(){let t=null;for(t of this.#lt);return t}updateUI(t){this.#Ht===t&&this.#Lt(t.propertiesToUpdate)}updateUIForDefaultProperties(t){this.#Lt(t.defaultPropertiesToUpdate)}toggleSelected(t){if(this.#lt.has(t)){this.#lt.delete(t);t.unselect();this.#Ct({hasSelectedEditor:this.hasSelection})}else{this.#lt.add(t);t.select();this.#Lt(t.propertiesToUpdate);this.#Ct({hasSelectedEditor:!0})}}setSelected(t){this.#B?.commitOrRemove();for(const e of this.#lt)e!==t&&e.unselect();this.#lt.clear();this.#lt.add(t);t.select();this.#Lt(t.propertiesToUpdate);this.#Ct({hasSelectedEditor:!0})}isSelected(t){return this.#lt.has(t)}get firstSelectedEditor(){return this.#lt.values().next().value}unselect(t){t.unselect();this.#lt.delete(t);this.#Ct({hasSelectedEditor:this.hasSelection})}get hasSelection(){return 0!==this.#lt.size}get isEnterHandled(){return 1===this.#lt.size&&this.firstSelectedEditor.isEnterHandled}undo(){this.#O.undo();this.#Ct({hasSomethingToUndo:this.#O.hasSomethingToUndo(),hasSomethingToRedo:!0,isEmpty:this.#Ot()});this._editorUndoBar?.hide()}redo(){this.#O.redo();this.#Ct({hasSomethingToUndo:!0,hasSomethingToRedo:this.#O.hasSomethingToRedo(),isEmpty:this.#Ot()})}addCommands(t){this.#O.add(t);this.#Ct({hasSomethingToUndo:!0,hasSomethingToRedo:!1,isEmpty:this.#Ot()})}cleanUndoStack(t){this.#O.cleanType(t)}#Ot(){if(0===this.#k.size)return!0;if(1===this.#k.size)for(const t of this.#k.values())return t.isEmpty();return!1}delete(){this.commitOrRemove();const t=this.currentLayer?.endDrawingSession(!0);if(!this.hasSelection&&!t)return;const e=t?[t]:[...this.#lt],undo=()=>{for(const t of e)this.#Rt(t)};this.addCommands({cmd:()=>{this._editorUndoBar?.show(undo,1===e.length?e[0].editorType:e.length);for(const t of e)t.remove()},undo,mustExec:!0})}commitOrRemove(){this.#P?.commitOrRemove()}hasSomethingToControl(){return this.#P||this.hasSelection}#Ft(t){for(const t of this.#lt)t.unselect();this.#lt.clear();for(const e of t)if(!e.isEmpty()){this.#lt.add(e);e.select()}this.#Ct({hasSelectedEditor:this.hasSelection})}selectAll(){for(const t of this.#lt)t.commit();this.#Ft(this.#k.values())}unselectAll(){if(this.#P){this.#P.commitOrRemove();if(this.#ot!==u.NONE)return}if(!this.#B?.commitOrRemove()&&this.hasSelection){for(const t of this.#lt)t.unselect();this.#lt.clear();this.#Ct({hasSelectedEditor:!1})}}translateSelectedEditors(t,e,i=!1){i||this.commitOrRemove();if(!this.hasSelection)return;this.#gt[0]+=t;this.#gt[1]+=e;const[s,n]=this.#gt,a=[...this.#lt];this.#ft&&clearTimeout(this.#ft);this.#ft=setTimeout((()=>{this.#ft=null;this.#gt[0]=this.#gt[1]=0;this.addCommands({cmd:()=>{for(const t of a)if(this.#k.has(t.id)){t.translateInPage(s,n);t.translationDone()}},undo:()=>{for(const t of a)if(this.#k.has(t.id)){t.translateInPage(-s,-n);t.translationDone()}},mustExec:!1})}),1e3);for(const i of a){i.translateInPage(t,e);i.translationDone()}}setUpDragSession(){if(this.hasSelection){this.disableUserSelect(!0);this.#z=new Map;for(const t of this.#lt)this.#z.set(t,{savedX:t.x,savedY:t.y,savedPageIndex:t.pageIndex,newX:0,newY:0,newPageIndex:-1})}}endDragSession(){if(!this.#z)return!1;this.disableUserSelect(!1);const t=this.#z;this.#z=null;let e=!1;for(const[{x:i,y:s,pageIndex:n},a]of t){a.newX=i;a.newY=s;a.newPageIndex=n;e||=i!==a.savedX||s!==a.savedY||n!==a.savedPageIndex}if(!e)return!1;const move=(t,e,i,s)=>{if(this.#k.has(t.id)){const n=this.#I.get(s);if(n)t._setParentAndPosition(n,e,i);else{t.pageIndex=s;t.x=e;t.y=i}}};this.addCommands({cmd:()=>{for(const[e,{newX:i,newY:s,newPageIndex:n}]of t)move(e,i,s,n)},undo:()=>{for(const[e,{savedX:i,savedY:s,savedPageIndex:n}]of t)move(e,i,s,n)},mustExec:!0});return!0}dragSelectedEditors(t,e){if(this.#z)for(const i of this.#z.keys())i.drag(t,e)}rebuild(t){if(null===t.parent){const e=this.getLayer(t.pageIndex);if(e){e.changeParent(t);e.addOrRebuild(t)}else{this.addEditor(t);this.addToAnnotationStorage(t);t.rebuild()}}else t.parent.addOrRebuild(t)}get isEditorHandlingKeyboard(){return this.getActive()?.shouldGetKeyboardEvents()||1===this.#lt.size&&this.firstSelectedEditor.shouldGetKeyboardEvents()}isActive(t){return this.#P===t}getActive(){return this.#P}getMode(){return this.#ot}get imageManager(){return shadow(this,"imageManager",new ImageManager)}getSelectionBoxes(t){if(!t)return null;const e=document.getSelection();for(let i=0,s=e.rangeCount;i({x:(e-s)/a,y:1-(t+r-i)/n,width:o/a,height:r/n});break;case"180":r=(t,e,r,o)=>({x:1-(t+r-i)/n,y:1-(e+o-s)/a,width:r/n,height:o/a});break;case"270":r=(t,e,r,o)=>({x:1-(e+o-s)/a,y:(t-i)/n,width:o/a,height:r/n});break;default:r=(t,e,r,o)=>({x:(t-i)/n,y:(e-s)/a,width:r/n,height:o/a})}const o=[];for(let t=0,i=e.rangeCount;tt.stopPropagation()),{signal:i});const onClick=t=>{t.preventDefault();this.#a._uiManager.editAltText(this.#a);this.#Kt&&this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_clicked",data:{label:this.#Qt}})};t.addEventListener("click",onClick,{capture:!0,signal:i});t.addEventListener("keydown",(e=>{if(e.target===t&&"Enter"===e.key){this.#Vt=!0;onClick(e)}}),{signal:i});await this.#Jt();return t}get#Qt(){return(this.#o?"added":null===this.#o&&this.guessedText&&"review")||"missing"}finish(){if(this.#zt){this.#zt.focus({focusVisible:this.#Vt});this.#Vt=!1}}isEmpty(){return this.#Kt?null===this.#o:!this.#o&&!this.#Ut}hasData(){return this.#Kt?null!==this.#o||!!this.#qt:this.isEmpty()}get guessedText(){return this.#qt}async setGuessedText(t){if(null===this.#o){this.#qt=t;this.#Xt=await AltText._l10n.get("pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer",{generatedAltText:t});this.#Jt()}}toggleAltTextBadge(t=!1){if(this.#Kt&&!this.#o){if(!this.#jt){const t=this.#jt=document.createElement("div");t.className="noAltTextBadge";this.#a.div.append(t)}this.#jt.classList.toggle("hidden",!t)}else{this.#jt?.remove();this.#jt=null}}serialize(t){let e=this.#o;t||this.#qt!==e||(e=this.#Xt);return{altText:e,decorative:this.#Ut,guessedText:this.#qt,textWithDisclaimer:this.#Xt}}get data(){return{altText:this.#o,decorative:this.#Ut}}set data({altText:t,decorative:e,guessedText:i,textWithDisclaimer:s,cancel:n=!1}){if(i){this.#qt=i;this.#Xt=s}if(this.#o!==t||this.#Ut!==e){if(!n){this.#o=t;this.#Ut=e}this.#Jt()}}toggle(t=!1){if(this.#zt){if(!t&&this.#Wt){clearTimeout(this.#Wt);this.#Wt=null}this.#zt.disabled=!t}}shown(){this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_displayed",data:{label:this.#Qt}})}destroy(){this.#zt?.remove();this.#zt=null;this.#Gt=null;this.#$t=null;this.#jt?.remove();this.#jt=null}async#Jt(){const t=this.#zt;if(!t)return;if(this.#Kt){t.classList.toggle("done",!!this.#o);t.setAttribute("data-l10n-id",AltText.#Yt[this.#Qt]);this.#Gt?.setAttribute("data-l10n-id",AltText.#Yt[`${this.#Qt}-label`]);if(!this.#o){this.#$t?.remove();return}}else{if(!this.#o&&!this.#Ut){t.classList.remove("done");this.#$t?.remove();return}t.classList.add("done");t.setAttribute("data-l10n-id","pdfjs-editor-alt-text-edit-button")}let e=this.#$t;if(!e){this.#$t=e=document.createElement("span");e.className="tooltip";e.setAttribute("role","tooltip");e.id=`alt-text-tooltip-${this.#a.id}`;const i=100,s=this.#a._uiManager._signal;s.addEventListener("abort",(()=>{clearTimeout(this.#Wt);this.#Wt=null}),{once:!0});t.addEventListener("mouseenter",(()=>{this.#Wt=setTimeout((()=>{this.#Wt=null;this.#$t.classList.add("show");this.#a._reportTelemetry({action:"alt_text_tooltip"})}),i)}),{signal:s});t.addEventListener("mouseleave",(()=>{if(this.#Wt){clearTimeout(this.#Wt);this.#Wt=null}this.#$t?.classList.remove("show")}),{signal:s})}if(this.#Ut)e.setAttribute("data-l10n-id","pdfjs-editor-alt-text-decorative-tooltip");else{e.removeAttribute("data-l10n-id");e.textContent=this.#o}e.parentNode||t.append(e);const i=this.#a.getElementForAltText();i?.setAttribute("aria-describedby",e.id)}}class TouchManager{#mt;#Zt=!1;#te=null;#ee;#ie;#se;#ne;#ae=null;#re;#oe=null;#le;#he=null;constructor({container:t,isPinchingDisabled:e=null,isPinchingStopped:i=null,onPinchStart:s=null,onPinching:n=null,onPinchEnd:a=null,signal:r}){this.#mt=t;this.#te=i;this.#ee=e;this.#ie=s;this.#se=n;this.#ne=a;this.#le=new AbortController;this.#re=AbortSignal.any([r,this.#le.signal]);t.addEventListener("touchstart",this.#de.bind(this),{passive:!1,signal:this.#re})}get MIN_TOUCH_DISTANCE_TO_PINCH(){return 35/OutputScale.pixelRatio}#de(t){if(this.#ee?.())return;if(1===t.touches.length){if(this.#ae)return;const t=this.#ae=new AbortController,e=AbortSignal.any([this.#re,t.signal]),i=this.#mt,s={capture:!0,signal:e,passive:!1},cancelPointerDown=t=>{if("touch"===t.pointerType){this.#ae?.abort();this.#ae=null}};i.addEventListener("pointerdown",(t=>{if("touch"===t.pointerType){stopEvent(t);cancelPointerDown(t)}}),s);i.addEventListener("pointerup",cancelPointerDown,s);i.addEventListener("pointercancel",cancelPointerDown,s);return}if(!this.#he){this.#he=new AbortController;const t=AbortSignal.any([this.#re,this.#he.signal]),e=this.#mt,i={signal:t,capture:!1,passive:!1};e.addEventListener("touchmove",this.#ce.bind(this),i);const s=this.#ue.bind(this);e.addEventListener("touchend",s,i);e.addEventListener("touchcancel",s,i);i.capture=!0;e.addEventListener("pointerdown",stopEvent,i);e.addEventListener("pointermove",stopEvent,i);e.addEventListener("pointercancel",stopEvent,i);e.addEventListener("pointerup",stopEvent,i);this.#ie?.()}stopEvent(t);if(2!==t.touches.length||this.#te?.()){this.#oe=null;return}let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);this.#oe={touch0X:e.screenX,touch0Y:e.screenY,touch1X:i.screenX,touch1Y:i.screenY}}#ce(t){if(!this.#oe||2!==t.touches.length)return;stopEvent(t);let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);const{screenX:s,screenY:n}=e,{screenX:a,screenY:r}=i,o=this.#oe,{touch0X:l,touch0Y:h,touch1X:d,touch1Y:c}=o,u=d-l,p=c-h,g=a-s,f=r-n,m=Math.hypot(g,f)||1,b=Math.hypot(u,p)||1;if(!this.#Zt&&Math.abs(b-m)<=TouchManager.MIN_TOUCH_DISTANCE_TO_PINCH)return;o.touch0X=s;o.touch0Y=n;o.touch1X=a;o.touch1Y=r;if(!this.#Zt){this.#Zt=!0;return}const A=[(s+a)/2,(n+r)/2];this.#se?.(A,b,m)}#ue(t){if(!(t.touches.length>=2)){if(this.#he){this.#he.abort();this.#he=null;this.#ne?.()}if(this.#oe){stopEvent(t);this.#oe=null;this.#Zt=!1}}}destroy(){this.#le?.abort();this.#le=null;this.#ae?.abort();this.#ae=null}}class AnnotationEditor{#pe=null;#ge=null;#o=null;#fe=!1;#me=null;#be="";#Ae=!1;#we=null;#ye=null;#ve=null;#_e=null;#xe="";#Ee=!1;#Se=null;#Ce=!1;#Te=!1;#Me=!1;#De=null;#Pe=0;#ke=0;#Ie=null;#Re=null;_isCopy=!1;_editToolbar=null;_initialOptions=Object.create(null);_initialData=null;_isVisible=!0;_uiManager=null;_focusEventsAllowed=!0;static _l10n=null;static _l10nResizer=null;#Fe=!1;#Le=AnnotationEditor._zIndex++;static _borderLineWidth=-1;static _colorManager=new ColorManager;static _zIndex=1;static _telemetryTimeout=1e3;static get _resizerKeyboardManager(){const t=AnnotationEditor.prototype._resizeWithKeyboard,e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_resizerKeyboardManager",new KeyboardManager([[["ArrowLeft","mac+ArrowLeft"],t,{args:[-e,0]}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t,{args:[-i,0]}],[["ArrowRight","mac+ArrowRight"],t,{args:[e,0]}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t,{args:[i,0]}],[["ArrowUp","mac+ArrowUp"],t,{args:[0,-e]}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t,{args:[0,-i]}],[["ArrowDown","mac+ArrowDown"],t,{args:[0,e]}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t,{args:[0,i]}],[["Escape","mac+Escape"],AnnotationEditor.prototype._stopResizingWithKeyboard]]))}constructor(t){this.parent=t.parent;this.id=t.id;this.width=this.height=null;this.pageIndex=t.parent.pageIndex;this.name=t.name;this.div=null;this._uiManager=t.uiManager;this.annotationElementId=null;this._willKeepAspectRatio=!1;this._initialOptions.isCentered=t.isCentered;this._structTreeParentId=null;const{rotation:e,rawDims:{pageWidth:i,pageHeight:s,pageX:n,pageY:a}}=this.parent.viewport;this.rotation=e;this.pageRotation=(360+e-this._uiManager.viewParameters.rotation)%360;this.pageDimensions=[i,s];this.pageTranslation=[n,a];const[r,o]=this.parentDimensions;this.x=t.x/r;this.y=t.y/o;this.isAttachedToDOM=!1;this.deleted=!1}get editorType(){return Object.getPrototypeOf(this).constructor._type}static get isDrawer(){return!1}static get _defaultLineColor(){return shadow(this,"_defaultLineColor",this._colorManager.getHexCode("CanvasText"))}static deleteAnnotationElement(t){const e=new FakeEditor({id:t.parent.getNextId(),parent:t.parent,uiManager:t._uiManager});e.annotationElementId=t.annotationElementId;e.deleted=!0;e._uiManager.addToAnnotationStorage(e)}static initialize(t,e){AnnotationEditor._l10n??=t;AnnotationEditor._l10nResizer||=Object.freeze({topLeft:"pdfjs-editor-resizer-top-left",topMiddle:"pdfjs-editor-resizer-top-middle",topRight:"pdfjs-editor-resizer-top-right",middleRight:"pdfjs-editor-resizer-middle-right",bottomRight:"pdfjs-editor-resizer-bottom-right",bottomMiddle:"pdfjs-editor-resizer-bottom-middle",bottomLeft:"pdfjs-editor-resizer-bottom-left",middleLeft:"pdfjs-editor-resizer-middle-left"});if(-1!==AnnotationEditor._borderLineWidth)return;const i=getComputedStyle(document.documentElement);AnnotationEditor._borderLineWidth=parseFloat(i.getPropertyValue("--outline-width"))||0}static updateDefaultParams(t,e){}static get defaultPropertiesToUpdate(){return[]}static isHandlingMimeForPasting(t){return!1}static paste(t,e){unreachable("Not implemented")}get propertiesToUpdate(){return[]}get _isDraggable(){return this.#Fe}set _isDraggable(t){this.#Fe=t;this.div?.classList.toggle("draggable",t)}get isEnterHandled(){return!0}center(){const[t,e]=this.pageDimensions;switch(this.parentRotation){case 90:this.x-=this.height*e/(2*t);this.y+=this.width*t/(2*e);break;case 180:this.x+=this.width/2;this.y+=this.height/2;break;case 270:this.x+=this.height*e/(2*t);this.y-=this.width*t/(2*e);break;default:this.x-=this.width/2;this.y-=this.height/2}this.fixAndSetPosition()}addCommands(t){this._uiManager.addCommands(t)}get currentLayer(){return this._uiManager.currentLayer}setInBackground(){this.div.style.zIndex=0}setInForeground(){this.div.style.zIndex=this.#Le}setParent(t){if(null!==t){this.pageIndex=t.pageIndex;this.pageDimensions=t.pageDimensions}else this.#Oe();this.parent=t}focusin(t){this._focusEventsAllowed&&(this.#Ee?this.#Ee=!1:this.parent.setSelected(this))}focusout(t){if(!this._focusEventsAllowed)return;if(!this.isAttachedToDOM)return;const e=t.relatedTarget;if(!e?.closest(`#${this.id}`)){t.preventDefault();this.parent?.isMultipleSelection||this.commitOrRemove()}}commitOrRemove(){this.isEmpty()?this.remove():this.commit()}commit(){this.addToAnnotationStorage()}addToAnnotationStorage(){this._uiManager.addToAnnotationStorage(this)}setAt(t,e,i,s){const[n,a]=this.parentDimensions;[i,s]=this.screenToPageTranslation(i,s);this.x=(t+i)/n;this.y=(e+s)/a;this.fixAndSetPosition()}_moveAfterPaste(t,e){const[i,s]=this.parentDimensions;this.setAt(t*i,e*s,this.width*i,this.height*s);this._onTranslated()}#Ne([t,e],i,s){[i,s]=this.screenToPageTranslation(i,s);this.x+=i/t;this.y+=s/e;this._onTranslating(this.x,this.y);this.fixAndSetPosition()}translate(t,e){this.#Ne(this.parentDimensions,t,e)}translateInPage(t,e){this.#Se||=[this.x,this.y,this.width,this.height];this.#Ne(this.pageDimensions,t,e);this.div.scrollIntoView({block:"nearest"})}translationDone(){this._onTranslated(this.x,this.y)}drag(t,e){this.#Se||=[this.x,this.y,this.width,this.height];const{div:i,parentDimensions:[s,n]}=this;this.x+=t/s;this.y+=e/n;if(this.parent&&(this.x<0||this.x>1||this.y<0||this.y>1)){const{x:t,y:e}=this.div.getBoundingClientRect();if(this.parent.findNewParent(this,t,e)){this.x-=Math.floor(this.x);this.y-=Math.floor(this.y)}}let{x:a,y:r}=this;const[o,l]=this.getBaseTranslation();a+=o;r+=l;const{style:h}=i;h.left=`${(100*a).toFixed(2)}%`;h.top=`${(100*r).toFixed(2)}%`;this._onTranslating(a,r);i.scrollIntoView({block:"nearest"})}_onTranslating(t,e){}_onTranslated(t,e){}get _hasBeenMoved(){return!!this.#Se&&(this.#Se[0]!==this.x||this.#Se[1]!==this.y)}get _hasBeenResized(){return!!this.#Se&&(this.#Se[2]!==this.width||this.#Se[3]!==this.height)}getBaseTranslation(){const[t,e]=this.parentDimensions,{_borderLineWidth:i}=AnnotationEditor,s=i/t,n=i/e;switch(this.rotation){case 90:return[-s,n];case 180:return[s,n];case 270:return[s,-n];default:return[-s,-n]}}get _mustFixPosition(){return!0}fixAndSetPosition(t=this.rotation){const{div:{style:e},pageDimensions:[i,s]}=this;let{x:n,y:a,width:r,height:o}=this;r*=i;o*=s;n*=i;a*=s;if(this._mustFixPosition)switch(t){case 0:n=MathClamp(n,0,i-r);a=MathClamp(a,0,s-o);break;case 90:n=MathClamp(n,0,i-o);a=MathClamp(a,r,s);break;case 180:n=MathClamp(n,r,i);a=MathClamp(a,o,s);break;case 270:n=MathClamp(n,o,i);a=MathClamp(a,0,s-r)}this.x=n/=i;this.y=a/=s;const[l,h]=this.getBaseTranslation();n+=l;a+=h;e.left=`${(100*n).toFixed(2)}%`;e.top=`${(100*a).toFixed(2)}%`;this.moveInDOM()}static#Be(t,e,i){switch(i){case 90:return[e,-t];case 180:return[-t,-e];case 270:return[-e,t];default:return[t,e]}}screenToPageTranslation(t,e){return AnnotationEditor.#Be(t,e,this.parentRotation)}pageTranslationToScreen(t,e){return AnnotationEditor.#Be(t,e,360-this.parentRotation)}#He(t){switch(t){case 90:{const[t,e]=this.pageDimensions;return[0,-t/e,e/t,0]}case 180:return[-1,0,0,-1];case 270:{const[t,e]=this.pageDimensions;return[0,t/e,-e/t,0]}default:return[1,0,0,1]}}get parentScale(){return this._uiManager.viewParameters.realScale}get parentRotation(){return(this._uiManager.viewParameters.rotation+this.pageRotation)%360}get parentDimensions(){const{parentScale:t,pageDimensions:[e,i]}=this;return[e*t,i*t]}setDims(t,e){const[i,s]=this.parentDimensions,{style:n}=this.div;n.width=`${(100*t/i).toFixed(2)}%`;this.#Ae||(n.height=`${(100*e/s).toFixed(2)}%`)}fixDims(){const{style:t}=this.div,{height:e,width:i}=t,s=i.endsWith("%"),n=!this.#Ae&&e.endsWith("%");if(s&&n)return;const[a,r]=this.parentDimensions;s||(t.width=`${(100*parseFloat(i)/a).toFixed(2)}%`);this.#Ae||n||(t.height=`${(100*parseFloat(e)/r).toFixed(2)}%`)}getInitialTranslation(){return[0,0]}#Ue(){if(this.#we)return;this.#we=document.createElement("div");this.#we.classList.add("resizers");const t=this._willKeepAspectRatio?["topLeft","topRight","bottomRight","bottomLeft"]:["topLeft","topMiddle","topRight","middleRight","bottomRight","bottomMiddle","bottomLeft","middleLeft"],e=this._uiManager._signal;for(const i of t){const t=document.createElement("div");this.#we.append(t);t.classList.add("resizer",i);t.setAttribute("data-resizer-name",i);t.addEventListener("pointerdown",this.#ze.bind(this,i),{signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e});t.tabIndex=-1}this.div.prepend(this.#we)}#ze(t,e){e.preventDefault();const{isMac:i}=util_FeatureTest.platform;if(0!==e.button||e.ctrlKey&&i)return;this.#o?.toggle(!1);const s=this._isDraggable;this._isDraggable=!1;this.#ye=[e.screenX,e.screenY];const n=new AbortController,a=this._uiManager.combinedSignal(n);this.parent.togglePointerEvents(!1);window.addEventListener("pointermove",this.#Ge.bind(this,t),{passive:!0,capture:!0,signal:a});window.addEventListener("touchmove",stopEvent,{passive:!1,signal:a});window.addEventListener("contextmenu",noContextMenu,{signal:a});this.#ve={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const r=this.parent.div.style.cursor,o=this.div.style.cursor;this.div.style.cursor=this.parent.div.style.cursor=window.getComputedStyle(e.target).cursor;const pointerUpCallback=()=>{n.abort();this.parent.togglePointerEvents(!0);this.#o?.toggle(!0);this._isDraggable=s;this.parent.div.style.cursor=r;this.div.style.cursor=o;this.#$e()};window.addEventListener("pointerup",pointerUpCallback,{signal:a});window.addEventListener("blur",pointerUpCallback,{signal:a})}#We(t,e,i,s){this.width=i;this.height=s;this.x=t;this.y=e;const[n,a]=this.parentDimensions;this.setDims(n*i,a*s);this.fixAndSetPosition();this._onResized()}_onResized(){}#$e(){if(!this.#ve)return;const{savedX:t,savedY:e,savedWidth:i,savedHeight:s}=this.#ve;this.#ve=null;const n=this.x,a=this.y,r=this.width,o=this.height;n===t&&a===e&&r===i&&o===s||this.addCommands({cmd:this.#We.bind(this,n,a,r,o),undo:this.#We.bind(this,t,e,i,s),mustExec:!0})}static _round(t){return Math.round(1e4*t)/1e4}#Ge(t,e){const[i,s]=this.parentDimensions,n=this.x,a=this.y,r=this.width,o=this.height,l=AnnotationEditor.MIN_SIZE/i,h=AnnotationEditor.MIN_SIZE/s,d=this.#He(this.rotation),transf=(t,e)=>[d[0]*t+d[2]*e,d[1]*t+d[3]*e],c=this.#He(360-this.rotation);let u,p,g=!1,f=!1;switch(t){case"topLeft":g=!0;u=(t,e)=>[0,0];p=(t,e)=>[t,e];break;case"topMiddle":u=(t,e)=>[t/2,0];p=(t,e)=>[t/2,e];break;case"topRight":g=!0;u=(t,e)=>[t,0];p=(t,e)=>[0,e];break;case"middleRight":f=!0;u=(t,e)=>[t,e/2];p=(t,e)=>[0,e/2];break;case"bottomRight":g=!0;u=(t,e)=>[t,e];p=(t,e)=>[0,0];break;case"bottomMiddle":u=(t,e)=>[t/2,e];p=(t,e)=>[t/2,0];break;case"bottomLeft":g=!0;u=(t,e)=>[0,e];p=(t,e)=>[t,0];break;case"middleLeft":f=!0;u=(t,e)=>[0,e/2];p=(t,e)=>[t,e/2]}const m=u(r,o),b=p(r,o);let A=transf(...b);const w=AnnotationEditor._round(n+A[0]),y=AnnotationEditor._round(a+A[1]);let v,_,x=1,E=1;if(e.fromKeyboard)({deltaX:v,deltaY:_}=e);else{const{screenX:t,screenY:i}=e,[s,n]=this.#ye;[v,_]=this.screenToPageTranslation(t-s,i-n);this.#ye[0]=t;this.#ye[1]=i}[v,_]=(S=v/i,C=_/s,[c[0]*S+c[2]*C,c[1]*S+c[3]*C]);var S,C;if(g){const t=Math.hypot(r,o);x=E=Math.max(Math.min(Math.hypot(b[0]-m[0]-v,b[1]-m[1]-_)/t,1/r,1/o),l/r,h/o)}else f?x=MathClamp(Math.abs(b[0]-m[0]-v),l,1)/r:E=MathClamp(Math.abs(b[1]-m[1]-_),h,1)/o;const T=AnnotationEditor._round(r*x),M=AnnotationEditor._round(o*E);A=transf(...p(T,M));const D=w-A[0],P=y-A[1];this.#Se||=[this.x,this.y,this.width,this.height];this.width=T;this.height=M;this.x=D;this.y=P;this.setDims(i*T,s*M);this.fixAndSetPosition();this._onResizing()}_onResizing(){}altTextFinish(){this.#o?.finish()}async addEditToolbar(){if(this._editToolbar||this.#Te)return this._editToolbar;this._editToolbar=new EditorToolbar(this);this.div.append(this._editToolbar.render());this.#o&&await this._editToolbar.addAltText(this.#o);return this._editToolbar}removeEditToolbar(){if(this._editToolbar){this._editToolbar.remove();this._editToolbar=null;this.#o?.destroy()}}addContainer(t){const e=this._editToolbar?.div;e?e.before(t):this.div.append(t)}getClientDimensions(){return this.div.getBoundingClientRect()}async addAltTextButton(){if(!this.#o){AltText.initialize(AnnotationEditor._l10n);this.#o=new AltText(this);if(this.#pe){this.#o.data=this.#pe;this.#pe=null}await this.addEditToolbar()}}get altTextData(){return this.#o?.data}set altTextData(t){this.#o&&(this.#o.data=t)}get guessedAltText(){return this.#o?.guessedText}async setGuessedAltText(t){await(this.#o?.setGuessedText(t))}serializeAltText(t){return this.#o?.serialize(t)}hasAltText(){return!!this.#o&&!this.#o.isEmpty()}hasAltTextData(){return this.#o?.hasData()??!1}render(){const t=this.div=document.createElement("div");t.setAttribute("data-editor-rotation",(360-this.rotation)%360);t.className=this.name;t.setAttribute("id",this.id);t.tabIndex=this.#fe?-1:0;t.setAttribute("role","application");this.defaultL10nId&&t.setAttribute("data-l10n-id",this.defaultL10nId);this._isVisible||t.classList.add("hidden");this.setInForeground();this.#Ve();const[e,i]=this.parentDimensions;if(this.parentRotation%180!=0){t.style.maxWidth=`${(100*i/e).toFixed(2)}%`;t.style.maxHeight=`${(100*e/i).toFixed(2)}%`}const[s,n]=this.getInitialTranslation();this.translate(s,n);bindEvents(this,t,["keydown","pointerdown"]);this.isResizable&&this._uiManager._supportsPinchToZoom&&(this.#Re||=new TouchManager({container:t,isPinchingDisabled:()=>!this.isSelected,onPinchStart:this.#je.bind(this),onPinching:this.#qe.bind(this),onPinchEnd:this.#Xe.bind(this),signal:this._uiManager._signal}));this._uiManager._editorUndoBar?.hide();return t}#je(){this.#ve={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};this.#o?.toggle(!1);this.parent.togglePointerEvents(!1)}#qe(t,e,i){let s=i/e*.7+1-.7;if(1===s)return;const n=this.#He(this.rotation),transf=(t,e)=>[n[0]*t+n[2]*e,n[1]*t+n[3]*e],[a,r]=this.parentDimensions,o=this.x,l=this.y,h=this.width,d=this.height,c=AnnotationEditor.MIN_SIZE/a,u=AnnotationEditor.MIN_SIZE/r;s=Math.max(Math.min(s,1/h,1/d),c/h,u/d);const p=AnnotationEditor._round(h*s),g=AnnotationEditor._round(d*s);if(p===h&&g===d)return;this.#Se||=[o,l,h,d];const f=transf(h/2,d/2),m=AnnotationEditor._round(o+f[0]),b=AnnotationEditor._round(l+f[1]),A=transf(p/2,g/2);this.x=m-A[0];this.y=b-A[1];this.width=p;this.height=g;this.setDims(a*p,r*g);this.fixAndSetPosition();this._onResizing()}#Xe(){this.#o?.toggle(!0);this.parent.togglePointerEvents(!0);this.#$e()}pointerdown(t){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)t.preventDefault();else{this.#Ee=!0;this._isDraggable?this.#Ke(t):this.#Ye(t)}}get isSelected(){return this._uiManager.isSelected(this)}#Ye(t){const{isMac:e}=util_FeatureTest.platform;t.ctrlKey&&!e||t.shiftKey||t.metaKey&&e?this.parent.toggleSelected(this):this.parent.setSelected(this)}#Ke(t){const{isSelected:e}=this;this._uiManager.setUpDragSession();let i=!1;const s=new AbortController,n=this._uiManager.combinedSignal(s),a={capture:!0,passive:!1,signal:n},cancelDrag=t=>{s.abort();this.#me=null;this.#Ee=!1;this._uiManager.endDragSession()||this.#Ye(t);i&&this._onStopDragging()};if(e){this.#Pe=t.clientX;this.#ke=t.clientY;this.#me=t.pointerId;this.#be=t.pointerType;window.addEventListener("pointermove",(t=>{if(!i){i=!0;this._onStartDragging()}const{clientX:e,clientY:s,pointerId:n}=t;if(n!==this.#me){stopEvent(t);return}const[a,r]=this.screenToPageTranslation(e-this.#Pe,s-this.#ke);this.#Pe=e;this.#ke=s;this._uiManager.dragSelectedEditors(a,r)}),a);window.addEventListener("touchmove",stopEvent,a);window.addEventListener("pointerdown",(t=>{t.pointerType===this.#be&&(this.#Re||t.isPrimary)&&cancelDrag(t);stopEvent(t)}),a)}const pointerUpCallback=t=>{this.#me&&this.#me!==t.pointerId?stopEvent(t):cancelDrag(t)};window.addEventListener("pointerup",pointerUpCallback,{signal:n});window.addEventListener("blur",pointerUpCallback,{signal:n})}_onStartDragging(){}_onStopDragging(){}moveInDOM(){this.#De&&clearTimeout(this.#De);this.#De=setTimeout((()=>{this.#De=null;this.parent?.moveEditorInDOM(this)}),0)}_setParentAndPosition(t,e,i){t.changeParent(this);this.x=e;this.y=i;this.fixAndSetPosition();this._onTranslated()}getRect(t,e,i=this.rotation){const s=this.parentScale,[n,a]=this.pageDimensions,[r,o]=this.pageTranslation,l=t/s,h=e/s,d=this.x*n,c=this.y*a,u=this.width*n,p=this.height*a;switch(i){case 0:return[d+l+r,a-c-h-p+o,d+l+u+r,a-c-h+o];case 90:return[d+h+r,a-c+l+o,d+h+p+r,a-c+l+u+o];case 180:return[d-l-u+r,a-c+h+o,d-l+r,a-c+h+p+o];case 270:return[d-h-p+r,a-c-l-u+o,d-h+r,a-c-l+o];default:throw new Error("Invalid rotation")}}getRectInCurrentCoords(t,e){const[i,s,n,a]=t,r=n-i,o=a-s;switch(this.rotation){case 0:return[i,e-a,r,o];case 90:return[i,e-s,o,r];case 180:return[n,e-s,r,o];case 270:return[n,e-a,o,r];default:throw new Error("Invalid rotation")}}onceAdded(t){}isEmpty(){return!1}enableEditMode(){this.#Te=!0}disableEditMode(){this.#Te=!1}isInEditMode(){return this.#Te}shouldGetKeyboardEvents(){return this.#Me}needsToBeRebuilt(){return this.div&&!this.isAttachedToDOM}get isOnScreen(){const{top:t,left:e,bottom:i,right:s}=this.getClientDimensions(),{innerHeight:n,innerWidth:a}=window;return e0&&t0}#Ve(){if(this.#_e||!this.div)return;this.#_e=new AbortController;const t=this._uiManager.combinedSignal(this.#_e);this.div.addEventListener("focusin",this.focusin.bind(this),{signal:t});this.div.addEventListener("focusout",this.focusout.bind(this),{signal:t})}rebuild(){this.#Ve()}rotate(t){}resize(){}serializeDeleted(){return{id:this.annotationElementId,deleted:!0,pageIndex:this.pageIndex,popupRef:this._initialData?.popupRef||""}}serialize(t=!1,e=null){unreachable("An editor must be serializable")}static async deserialize(t,e,i){const s=new this.prototype.constructor({parent:e,id:e.getNextId(),uiManager:i});s.rotation=t.rotation;s.#pe=t.accessibilityData;s._isCopy=t.isCopy||!1;const[n,a]=s.pageDimensions,[r,o,l,h]=s.getRectInCurrentCoords(t.rect,a);s.x=r/n;s.y=o/a;s.width=l/n;s.height=h/a;return s}get hasBeenModified(){return!!this.annotationElementId&&(this.deleted||null!==this.serialize())}remove(){this.#_e?.abort();this.#_e=null;this.isEmpty()||this.commit();this.parent?this.parent.remove(this):this._uiManager.removeEditor(this);if(this.#De){clearTimeout(this.#De);this.#De=null}this.#Oe();this.removeEditToolbar();if(this.#Ie){for(const t of this.#Ie.values())clearTimeout(t);this.#Ie=null}this.parent=null;this.#Re?.destroy();this.#Re=null}get isResizable(){return!1}makeResizable(){if(this.isResizable){this.#Ue();this.#we.classList.remove("hidden")}}get toolbarPosition(){return null}keydown(t){if(!this.isResizable||t.target!==this.div||"Enter"!==t.key)return;this._uiManager.setSelected(this);this.#ve={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const e=this.#we.children;if(!this.#ge){this.#ge=Array.from(e);const t=this.#Qe.bind(this),i=this.#Je.bind(this),s=this._uiManager._signal;for(const e of this.#ge){const n=e.getAttribute("data-resizer-name");e.setAttribute("role","spinbutton");e.addEventListener("keydown",t,{signal:s});e.addEventListener("blur",i,{signal:s});e.addEventListener("focus",this.#Ze.bind(this,n),{signal:s});e.setAttribute("data-l10n-id",AnnotationEditor._l10nResizer[n])}}const i=this.#ge[0];let s=0;for(const t of e){if(t===i)break;s++}const n=(360-this.rotation+this.parentRotation)%360/90*(this.#ge.length/4);if(n!==s){if(ns)for(let t=0;t{this.div?.classList.contains("selectedEditor")&&this._editToolbar?.show()}))}unselect(){this.#we?.classList.add("hidden");this.div?.classList.remove("selectedEditor");this.div?.contains(document.activeElement)&&this._uiManager.currentLayer.div.focus({preventScroll:!0});this._editToolbar?.hide();this.#o?.toggleAltTextBadge(!0)}updateParams(t,e){}disableEditing(){}enableEditing(){}enterInEditMode(){}getElementForAltText(){return this.div}get contentDiv(){return this.div}get isEditing(){return this.#Ce}set isEditing(t){this.#Ce=t;if(this.parent)if(t){this.parent.setSelected(this);this.parent.setActiveEditor(this)}else this.parent.setActiveEditor(null)}setAspectRatio(t,e){this.#Ae=!0;const i=t/e,{style:s}=this.div;s.aspectRatio=i;s.height="auto"}static get MIN_SIZE(){return 16}static canCreateNewEmptyEditor(){return!0}get telemetryInitialData(){return{action:"added"}}get telemetryFinalData(){return null}_reportTelemetry(t,e=!1){if(e){this.#Ie||=new Map;const{action:e}=t;let i=this.#Ie.get(e);i&&clearTimeout(i);i=setTimeout((()=>{this._reportTelemetry(t);this.#Ie.delete(e);0===this.#Ie.size&&(this.#Ie=null)}),AnnotationEditor._telemetryTimeout);this.#Ie.set(e,i)}else{t.type||=this.editorType;this._uiManager._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:t}})}}show(t=this._isVisible){this.div.classList.toggle("hidden",!t);this._isVisible=t}enable(){this.div&&(this.div.tabIndex=0);this.#fe=!1}disable(){this.div&&(this.div.tabIndex=-1);this.#fe=!0}renderAnnotationElement(t){let e=t.container.querySelector(".annotationContent");if(e){if("CANVAS"===e.nodeName){const t=e;e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.before(e)}}else{e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.container.prepend(e)}return e}resetAnnotationElement(t){const{firstChild:e}=t.container;"DIV"===e?.nodeName&&e.classList.contains("annotationContent")&&e.remove()}}class FakeEditor extends AnnotationEditor{constructor(t){super(t);this.annotationElementId=t.annotationElementId;this.deleted=!0}serialize(){return this.serializeDeleted()}}const $=3285377520,W=4294901760,V=65535;class MurmurHash3_64{constructor(t){this.h1=t?4294967295&t:$;this.h2=t?4294967295&t:$}update(t){let e,i;if("string"==typeof t){e=new Uint8Array(2*t.length);i=0;for(let s=0,n=t.length;s>>8;e[i++]=255&n}}}else{if(!ArrayBuffer.isView(t))throw new Error("Invalid data format, must be a string or TypedArray.");e=t.slice();i=e.byteLength}const s=i>>2,n=i-4*s,a=new Uint32Array(e.buffer,0,s);let r=0,o=0,l=this.h1,h=this.h2;const d=3432918353,c=461845907,u=11601,p=13715;for(let t=0;t>>17;r=r*c&W|r*p&V;l^=r;l=l<<13|l>>>19;l=5*l+3864292196}else{o=a[t];o=o*d&W|o*u&V;o=o<<15|o>>>17;o=o*c&W|o*p&V;h^=o;h=h<<13|h>>>19;h=5*h+3864292196}r=0;switch(n){case 3:r^=e[4*s+2]<<16;case 2:r^=e[4*s+1]<<8;case 1:r^=e[4*s];r=r*d&W|r*u&V;r=r<<15|r>>>17;r=r*c&W|r*p&V;1&s?l^=r:h^=r}this.h1=l;this.h2=h}hexdigest(){let t=this.h1,e=this.h2;t^=e>>>1;t=3981806797*t&W|36045*t&V;e=4283543511*e&W|(2950163797*(e<<16|t>>>16)&W)>>>16;t^=e>>>1;t=444984403*t&W|60499*t&V;e=3301882366*e&W|(3120437893*(e<<16|t>>>16)&W)>>>16;t^=e>>>1;return(t>>>0).toString(16).padStart(8,"0")+(e>>>0).toString(16).padStart(8,"0")}}const j=Object.freeze({map:null,hash:"",transfer:void 0});class AnnotationStorage{#ei=!1;#ii=null;#si=new Map;constructor(){this.onSetModified=null;this.onResetModified=null;this.onAnnotationEditor=null}getValue(t,e){const i=this.#si.get(t);return void 0===i?e:Object.assign(e,i)}getRawValue(t){return this.#si.get(t)}remove(t){this.#si.delete(t);0===this.#si.size&&this.resetModified();if("function"==typeof this.onAnnotationEditor){for(const t of this.#si.values())if(t instanceof AnnotationEditor)return;this.onAnnotationEditor(null)}}setValue(t,e){const i=this.#si.get(t);let s=!1;if(void 0!==i){for(const[t,n]of Object.entries(e))if(i[t]!==n){s=!0;i[t]=n}}else{s=!0;this.#si.set(t,e)}s&&this.#ni();e instanceof AnnotationEditor&&"function"==typeof this.onAnnotationEditor&&this.onAnnotationEditor(e.constructor._type)}has(t){return this.#si.has(t)}get size(){return this.#si.size}#ni(){if(!this.#ei){this.#ei=!0;"function"==typeof this.onSetModified&&this.onSetModified()}}resetModified(){if(this.#ei){this.#ei=!1;"function"==typeof this.onResetModified&&this.onResetModified()}}get print(){return new PrintAnnotationStorage(this)}get serializable(){if(0===this.#si.size)return j;const t=new Map,e=new MurmurHash3_64,i=[],s=Object.create(null);let n=!1;for(const[i,a]of this.#si){const r=a instanceof AnnotationEditor?a.serialize(!1,s):a;if(r){t.set(i,r);e.update(`${i}:${JSON.stringify(r)}`);n||=!!r.bitmap}}if(n)for(const e of t.values())e.bitmap&&i.push(e.bitmap);return t.size>0?{map:t,hash:e.hexdigest(),transfer:i}:j}get editorStats(){let t=null;const e=new Map;for(const i of this.#si.values()){if(!(i instanceof AnnotationEditor))continue;const s=i.telemetryFinalData;if(!s)continue;const{type:n}=s;e.has(n)||e.set(n,Object.getPrototypeOf(i).constructor);t||=Object.create(null);const a=t[n]||=new Map;for(const[t,e]of Object.entries(s)){if("type"===t)continue;let i=a.get(t);if(!i){i=new Map;a.set(t,i)}const s=i.get(e)??0;i.set(e,s+1)}}for(const[i,s]of e)t[i]=s.computeTelemetryFinalData(t[i]);return t}resetModifiedIds(){this.#ii=null}get modifiedIds(){if(this.#ii)return this.#ii;const t=[];for(const e of this.#si.values())e instanceof AnnotationEditor&&e.annotationElementId&&e.serialize()&&t.push(e.annotationElementId);return this.#ii={ids:new Set(t),hash:t.join(",")}}[Symbol.iterator](){return this.#si.entries()}}class PrintAnnotationStorage extends AnnotationStorage{#ai;constructor(t){super();const{map:e,hash:i,transfer:s}=t.serializable,n=structuredClone(e,s?{transfer:s}:null);this.#ai={map:n,hash:i,transfer:s}}get print(){unreachable("Should not call PrintAnnotationStorage.print")}get serializable(){return this.#ai}get modifiedIds(){return shadow(this,"modifiedIds",{ids:new Set,hash:""})}}class FontLoader{#ri=new Set;constructor({ownerDocument:t=globalThis.document,styleElement:e=null}){this._document=t;this.nativeFontFaces=new Set;this.styleElement=null;this.loadingRequests=[];this.loadTestFontId=0}addNativeFontFace(t){this.nativeFontFaces.add(t);this._document.fonts.add(t)}removeNativeFontFace(t){this.nativeFontFaces.delete(t);this._document.fonts.delete(t)}insertRule(t){if(!this.styleElement){this.styleElement=this._document.createElement("style");this._document.documentElement.getElementsByTagName("head")[0].append(this.styleElement)}const e=this.styleElement.sheet;e.insertRule(t,e.cssRules.length)}clear(){for(const t of this.nativeFontFaces)this._document.fonts.delete(t);this.nativeFontFaces.clear();this.#ri.clear();if(this.styleElement){this.styleElement.remove();this.styleElement=null}}async loadSystemFont({systemFontInfo:t,disableFontFace:e,_inspectFont:i}){if(t&&!this.#ri.has(t.loadedName)){assert(!e,"loadSystemFont shouldn't be called when `disableFontFace` is set.");if(this.isFontLoadingAPISupported){const{loadedName:e,src:s,style:n}=t,a=new FontFace(e,s,n);this.addNativeFontFace(a);try{await a.load();this.#ri.add(e);i?.(t)}catch{warn(`Cannot load system font: ${t.baseFontName}, installing it could help to improve PDF rendering.`);this.removeNativeFontFace(a)}}else unreachable("Not implemented: loadSystemFont without the Font Loading API.")}}async bind(t){if(t.attached||t.missingFile&&!t.systemFontInfo)return;t.attached=!0;if(t.systemFontInfo){await this.loadSystemFont(t);return}if(this.isFontLoadingAPISupported){const e=t.createNativeFontFace();if(e){this.addNativeFontFace(e);try{await e.loaded}catch(i){warn(`Failed to load font '${e.family}': '${i}'.`);t.disableFontFace=!0;throw i}}return}const e=t.createFontFaceRule();if(e){this.insertRule(e);if(this.isSyncFontLoadingSupported)return;await new Promise((e=>{const i=this._queueLoadingCallback(e);this._prepareFontLoadEvent(t,i)}))}}get isFontLoadingAPISupported(){return shadow(this,"isFontLoadingAPISupported",!!this._document?.fonts)}get isSyncFontLoadingSupported(){return shadow(this,"isSyncFontLoadingSupported",t||util_FeatureTest.platform.isFirefox)}_queueLoadingCallback(t){const{loadingRequests:e}=this,i={done:!1,complete:function completeRequest(){assert(!i.done,"completeRequest() cannot be called twice.");i.done=!0;for(;e.length>0&&e[0].done;){const t=e.shift();setTimeout(t.callback,0)}},callback:t};e.push(i);return i}get _loadTestFont(){return shadow(this,"_loadTestFont",atob("T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQAABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwAAAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbmFtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAAAADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAAMQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAAAAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAAAAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQAAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMAAQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAAEAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgcA/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQAAAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAgABAAAAAAAAAAAD6AAAAAAAAA=="))}_prepareFontLoadEvent(t,e){function int32(t,e){return t.charCodeAt(e)<<24|t.charCodeAt(e+1)<<16|t.charCodeAt(e+2)<<8|255&t.charCodeAt(e+3)}function spliceString(t,e,i,s){return t.substring(0,e)+s+t.substring(e+i)}let i,s;const n=this._document.createElement("canvas");n.width=1;n.height=1;const a=n.getContext("2d");let r=0;const o=`lt${Date.now()}${this.loadTestFontId++}`;let l=this._loadTestFont;l=spliceString(l,976,o.length,o);const h=1482184792;let d=int32(l,16);for(i=0,s=o.length-3;i>24&255,t>>16&255,t>>8&255,255&t)}(d));const c=`@font-face {font-family:"${o}";src:${`url(data:font/opentype;base64,${btoa(l)});`}}`;this.insertRule(c);const u=this._document.createElement("div");u.style.visibility="hidden";u.style.width=u.style.height="10px";u.style.position="absolute";u.style.top=u.style.left="0px";for(const e of[t.loadedName,o]){const t=this._document.createElement("span");t.textContent="Hi";t.style.fontFamily=e;u.append(t)}this._document.body.append(u);!function isFontReady(t,e){if(++r>30){warn("Load test font never loaded.");e();return}a.font="30px "+t;a.fillText(".",0,20);a.getImageData(0,0,1,1).data[3]>0?e():setTimeout(isFontReady.bind(null,t,e))}(o,(()=>{u.remove();e.complete()}))}}class FontFaceObject{constructor(t,e=null){this.compiledGlyphs=Object.create(null);for(const e in t)this[e]=t[e];this._inspectFont=e}createNativeFontFace(){if(!this.data||this.disableFontFace)return null;let t;if(this.cssFontInfo){const e={weight:this.cssFontInfo.fontWeight};this.cssFontInfo.italicAngle&&(e.style=`oblique ${this.cssFontInfo.italicAngle}deg`);t=new FontFace(this.cssFontInfo.fontFamily,this.data,e)}else t=new FontFace(this.loadedName,this.data,{});this._inspectFont?.(this);return t}createFontFaceRule(){if(!this.data||this.disableFontFace)return null;const t=`url(data:${this.mimetype};base64,${toBase64Util(this.data)});`;let e;if(this.cssFontInfo){let i=`font-weight: ${this.cssFontInfo.fontWeight};`;this.cssFontInfo.italicAngle&&(i+=`font-style: oblique ${this.cssFontInfo.italicAngle}deg;`);e=`@font-face {font-family:"${this.cssFontInfo.fontFamily}";${i}src:${t}}`}else e=`@font-face {font-family:"${this.loadedName}";src:${t}}`;this._inspectFont?.(this,t);return e}getPathGenerator(t,e){if(void 0!==this.compiledGlyphs[e])return this.compiledGlyphs[e];const i=this.loadedName+"_path_"+e;let s;try{s=t.get(i)}catch(t){warn(`getPathGenerator - ignoring character: "${t}".`)}const n=new Path2D(s||"");this.fontExtraProperties||t.delete(i);return this.compiledGlyphs[e]=n}}const q=1,X=2,K=1,Y=2,Q=3,J=4,Z=5,tt=6,et=7,it=8;function onFn(){}function wrapReason(t){if(t instanceof AbortException||t instanceof InvalidPDFException||t instanceof PasswordException||t instanceof ResponseException||t instanceof UnknownErrorException)return t;t instanceof Error||"object"==typeof t&&null!==t||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(t.name){case"AbortException":return new AbortException(t.message);case"InvalidPDFException":return new InvalidPDFException(t.message);case"PasswordException":return new PasswordException(t.message,t.code);case"ResponseException":return new ResponseException(t.message,t.status,t.missing);case"UnknownErrorException":return new UnknownErrorException(t.message,t.details)}return new UnknownErrorException(t.message,t.toString())}class MessageHandler{#oi=new AbortController;constructor(t,e,i){this.sourceName=t;this.targetName=e;this.comObj=i;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);i.addEventListener("message",this.#li.bind(this),{signal:this.#oi.signal})}#li({data:t}){if(t.targetName!==this.sourceName)return;if(t.stream){this.#hi(t);return}if(t.callback){const e=t.callbackId,i=this.callbackCapabilities[e];if(!i)throw new Error(`Cannot resolve callback ${e}`);delete this.callbackCapabilities[e];if(t.callback===q)i.resolve(t.data);else{if(t.callback!==X)throw new Error("Unexpected callback case");i.reject(wrapReason(t.reason))}return}const e=this.actionHandler[t.action];if(!e)throw new Error(`Unknown action from worker: ${t.action}`);if(t.callbackId){const i=this.sourceName,s=t.sourceName,n=this.comObj;Promise.try(e,t.data).then((function(e){n.postMessage({sourceName:i,targetName:s,callback:q,callbackId:t.callbackId,data:e})}),(function(e){n.postMessage({sourceName:i,targetName:s,callback:X,callbackId:t.callbackId,reason:wrapReason(e)})}))}else t.streamId?this.#di(t):e(t.data)}on(t,e){const i=this.actionHandler;if(i[t])throw new Error(`There is already an actionName called "${t}"`);i[t]=e}send(t,e,i){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,data:e},i)}sendWithPromise(t,e,i){const s=this.callbackId++,n=Promise.withResolvers();this.callbackCapabilities[s]=n;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,callbackId:s,data:e},i)}catch(t){n.reject(t)}return n.promise}sendWithStream(t,e,i,s){const n=this.streamId++,a=this.sourceName,r=this.targetName,o=this.comObj;return new ReadableStream({start:i=>{const l=Promise.withResolvers();this.streamControllers[n]={controller:i,startCall:l,pullCall:null,cancelCall:null,isClosed:!1};o.postMessage({sourceName:a,targetName:r,action:t,streamId:n,data:e,desiredSize:i.desiredSize},s);return l.promise},pull:t=>{const e=Promise.withResolvers();this.streamControllers[n].pullCall=e;o.postMessage({sourceName:a,targetName:r,stream:tt,streamId:n,desiredSize:t.desiredSize});return e.promise},cancel:t=>{assert(t instanceof Error,"cancel must have a valid reason");const e=Promise.withResolvers();this.streamControllers[n].cancelCall=e;this.streamControllers[n].isClosed=!0;o.postMessage({sourceName:a,targetName:r,stream:K,streamId:n,reason:wrapReason(t)});return e.promise}},i)}#di(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,a=this,r=this.actionHandler[t.action],o={enqueue(t,a=1,r){if(this.isCancelled)return;const o=this.desiredSize;this.desiredSize-=a;if(o>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}n.postMessage({sourceName:i,targetName:s,stream:J,streamId:e,chunk:t},r)},close(){if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:Q,streamId:e});delete a.streamSinks[e]}},error(t){assert(t instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:Z,streamId:e,reason:wrapReason(t)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:t.desiredSize,ready:null};o.sinkCapability.resolve();o.ready=o.sinkCapability.promise;this.streamSinks[e]=o;Promise.try(r,t.data,o).then((function(){n.postMessage({sourceName:i,targetName:s,stream:it,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:it,streamId:e,reason:wrapReason(t)})}))}#hi(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,a=this.streamControllers[e],r=this.streamSinks[e];switch(t.stream){case it:t.success?a.startCall.resolve():a.startCall.reject(wrapReason(t.reason));break;case et:t.success?a.pullCall.resolve():a.pullCall.reject(wrapReason(t.reason));break;case tt:if(!r){n.postMessage({sourceName:i,targetName:s,stream:et,streamId:e,success:!0});break}r.desiredSize<=0&&t.desiredSize>0&&r.sinkCapability.resolve();r.desiredSize=t.desiredSize;Promise.try(r.onPull||onFn).then((function(){n.postMessage({sourceName:i,targetName:s,stream:et,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:et,streamId:e,reason:wrapReason(t)})}));break;case J:assert(a,"enqueue should have stream controller");if(a.isClosed)break;a.controller.enqueue(t.chunk);break;case Q:assert(a,"close should have stream controller");if(a.isClosed)break;a.isClosed=!0;a.controller.close();this.#ci(a,e);break;case Z:assert(a,"error should have stream controller");a.controller.error(wrapReason(t.reason));this.#ci(a,e);break;case Y:t.success?a.cancelCall.resolve():a.cancelCall.reject(wrapReason(t.reason));this.#ci(a,e);break;case K:if(!r)break;const o=wrapReason(t.reason);Promise.try(r.onCancel||onFn,o).then((function(){n.postMessage({sourceName:i,targetName:s,stream:Y,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:Y,streamId:e,reason:wrapReason(t)})}));r.sinkCapability.reject(o);r.isCancelled=!0;delete this.streamSinks[e];break;default:throw new Error("Unexpected stream case")}}async#ci(t,e){await Promise.allSettled([t.startCall?.promise,t.pullCall?.promise,t.cancelCall?.promise]);delete this.streamControllers[e]}destroy(){this.#oi?.abort();this.#oi=null}}class BaseCanvasFactory{#ui=!1;constructor({enableHWA:t=!1}){this.#ui=t}create(t,e){if(t<=0||e<=0)throw new Error("Invalid canvas size");const i=this._createCanvas(t,e);return{canvas:i,context:i.getContext("2d",{willReadFrequently:!this.#ui})}}reset(t,e,i){if(!t.canvas)throw new Error("Canvas is not specified");if(e<=0||i<=0)throw new Error("Invalid canvas size");t.canvas.width=e;t.canvas.height=i}destroy(t){if(!t.canvas)throw new Error("Canvas is not specified");t.canvas.width=0;t.canvas.height=0;t.canvas=null;t.context=null}_createCanvas(t,e){unreachable("Abstract method `_createCanvas` called.")}}class DOMCanvasFactory extends BaseCanvasFactory{constructor({ownerDocument:t=globalThis.document,enableHWA:e=!1}){super({enableHWA:e});this._document=t}_createCanvas(t,e){const i=this._document.createElement("canvas");i.width=t;i.height=e;return i}}class BaseCMapReaderFactory{constructor({baseUrl:t=null,isCompressed:e=!0}){this.baseUrl=t;this.isCompressed=e}async fetch({name:t}){if(!this.baseUrl)throw new Error("Ensure that the `cMapUrl` and `cMapPacked` API parameters are provided.");if(!t)throw new Error("CMap name must be specified.");const e=this.baseUrl+t+(this.isCompressed?".bcmap":"");return this._fetch(e).then((t=>({cMapData:t,isCompressed:this.isCompressed}))).catch((t=>{throw new Error(`Unable to load ${this.isCompressed?"binary ":""}CMap at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMCMapReaderFactory extends BaseCMapReaderFactory{async _fetch(t){const e=await fetchData(t,this.isCompressed?"arraybuffer":"text");return e instanceof ArrayBuffer?new Uint8Array(e):stringToBytes(e)}}class BaseFilterFactory{addFilter(t){return"none"}addHCMFilter(t,e){return"none"}addAlphaFilter(t){return"none"}addLuminosityFilter(t){return"none"}addHighlightHCMFilter(t,e,i,s,n){return"none"}destroy(t=!1){}}class DOMFilterFactory extends BaseFilterFactory{#pi;#gi;#fi;#mi;#bi;#Ai;#y=0;constructor({docId:t,ownerDocument:e=globalThis.document}){super();this.#mi=t;this.#bi=e}get#_(){return this.#gi||=new Map}get#wi(){return this.#Ai||=new Map}get#yi(){if(!this.#fi){const t=this.#bi.createElement("div"),{style:e}=t;e.visibility="hidden";e.contain="strict";e.width=e.height=0;e.position="absolute";e.top=e.left=0;e.zIndex=-1;const i=this.#bi.createElementNS(z,"svg");i.setAttribute("width",0);i.setAttribute("height",0);this.#fi=this.#bi.createElementNS(z,"defs");t.append(i);i.append(this.#fi);this.#bi.body.append(t)}return this.#fi}#vi(t){if(1===t.length){const e=t[0],i=new Array(256);for(let t=0;t<256;t++)i[t]=e[t]/255;const s=i.join(",");return[s,s,s]}const[e,i,s]=t,n=new Array(256),a=new Array(256),r=new Array(256);for(let t=0;t<256;t++){n[t]=e[t]/255;a[t]=i[t]/255;r[t]=s[t]/255}return[n.join(","),a.join(","),r.join(",")]}#_i(t){if(void 0===this.#pi){this.#pi="";const t=this.#bi.URL;t!==this.#bi.baseURI&&(isDataScheme(t)?warn('#createUrl: ignore "data:"-URL for performance reasons.'):this.#pi=updateUrlHash(t,""))}return`url(${this.#pi}#${t})`}addFilter(t){if(!t)return"none";let e=this.#_.get(t);if(e)return e;const[i,s,n]=this.#vi(t),a=1===t.length?i:`${i}${s}${n}`;e=this.#_.get(a);if(e){this.#_.set(t,e);return e}const r=`g_${this.#mi}_transfer_map_${this.#y++}`,o=this.#_i(r);this.#_.set(t,o);this.#_.set(a,o);const l=this.#xi(r);this.#Ei(i,s,n,l);return o}addHCMFilter(t,e){const i=`${t}-${e}`,s="base";let n=this.#wi.get(s);if(n?.key===i)return n.url;if(n){n.filter?.remove();n.key=i;n.url="none";n.filter=null}else{n={key:i,url:"none",filter:null};this.#wi.set(s,n)}if(!t||!e)return n.url;const a=this.#Si(t);t=Util.makeHexColor(...a);const r=this.#Si(e);e=Util.makeHexColor(...r);this.#yi.style.color="";if("#000000"===t&&"#ffffff"===e||t===e)return n.url;const o=new Array(256);for(let t=0;t<=255;t++){const e=t/255;o[t]=e<=.03928?e/12.92:((e+.055)/1.055)**2.4}const l=o.join(","),h=`g_${this.#mi}_hcm_filter`,d=n.filter=this.#xi(h);this.#Ei(l,l,l,d);this.#Ci(d);const getSteps=(t,e)=>{const i=a[t]/255,s=r[t]/255,n=new Array(e+1);for(let t=0;t<=e;t++)n[t]=i+t/e*(s-i);return n.join(",")};this.#Ei(getSteps(0,5),getSteps(1,5),getSteps(2,5),d);n.url=this.#_i(h);return n.url}addAlphaFilter(t){let e=this.#_.get(t);if(e)return e;const[i]=this.#vi([t]),s=`alpha_${i}`;e=this.#_.get(s);if(e){this.#_.set(t,e);return e}const n=`g_${this.#mi}_alpha_map_${this.#y++}`,a=this.#_i(n);this.#_.set(t,a);this.#_.set(s,a);const r=this.#xi(n);this.#Ti(i,r);return a}addLuminosityFilter(t){let e,i,s=this.#_.get(t||"luminosity");if(s)return s;if(t){[e]=this.#vi([t]);i=`luminosity_${e}`}else i="luminosity";s=this.#_.get(i);if(s){this.#_.set(t,s);return s}const n=`g_${this.#mi}_luminosity_map_${this.#y++}`,a=this.#_i(n);this.#_.set(t,a);this.#_.set(i,a);const r=this.#xi(n);this.#Mi(r);t&&this.#Ti(e,r);return a}addHighlightHCMFilter(t,e,i,s,n){const a=`${e}-${i}-${s}-${n}`;let r=this.#wi.get(t);if(r?.key===a)return r.url;if(r){r.filter?.remove();r.key=a;r.url="none";r.filter=null}else{r={key:a,url:"none",filter:null};this.#wi.set(t,r)}if(!e||!i)return r.url;const[o,l]=[e,i].map(this.#Si.bind(this));let h=Math.round(.2126*o[0]+.7152*o[1]+.0722*o[2]),d=Math.round(.2126*l[0]+.7152*l[1]+.0722*l[2]),[c,u]=[s,n].map(this.#Si.bind(this));d{const s=new Array(256),n=(d-h)/i,a=t/255,r=(e-t)/(255*i);let o=0;for(let t=0;t<=i;t++){const e=Math.round(h+t*n),i=a+t*r;for(let t=o;t<=e;t++)s[t]=i;o=e+1}for(let t=o;t<256;t++)s[t]=s[o-1];return s.join(",")},p=`g_${this.#mi}_hcm_${t}_filter`,g=r.filter=this.#xi(p);this.#Ci(g);this.#Ei(getSteps(c[0],u[0],5),getSteps(c[1],u[1],5),getSteps(c[2],u[2],5),g);r.url=this.#_i(p);return r.url}destroy(t=!1){if(!t||!this.#Ai?.size){this.#fi?.parentNode.parentNode.remove();this.#fi=null;this.#gi?.clear();this.#gi=null;this.#Ai?.clear();this.#Ai=null;this.#y=0}}#Mi(t){const e=this.#bi.createElementNS(z,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0.59 0.11 0 0");t.append(e)}#Ci(t){const e=this.#bi.createElementNS(z,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0");t.append(e)}#xi(t){const e=this.#bi.createElementNS(z,"filter");e.setAttribute("color-interpolation-filters","sRGB");e.setAttribute("id",t);this.#yi.append(e);return e}#Di(t,e,i){const s=this.#bi.createElementNS(z,e);s.setAttribute("type","discrete");s.setAttribute("tableValues",i);t.append(s)}#Ei(t,e,i,s){const n=this.#bi.createElementNS(z,"feComponentTransfer");s.append(n);this.#Di(n,"feFuncR",t);this.#Di(n,"feFuncG",e);this.#Di(n,"feFuncB",i)}#Ti(t,e){const i=this.#bi.createElementNS(z,"feComponentTransfer");e.append(i);this.#Di(i,"feFuncA",t)}#Si(t){this.#yi.style.color=t;return getRGB(getComputedStyle(this.#yi).getPropertyValue("color"))}}class BaseStandardFontDataFactory{constructor({baseUrl:t=null}){this.baseUrl=t}async fetch({filename:t}){if(!this.baseUrl)throw new Error("Ensure that the `standardFontDataUrl` API parameter is provided.");if(!t)throw new Error("Font filename must be specified.");const e=`${this.baseUrl}${t}`;return this._fetch(e).catch((t=>{throw new Error(`Unable to load font data at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMStandardFontDataFactory extends BaseStandardFontDataFactory{async _fetch(t){const e=await fetchData(t,"arraybuffer");return new Uint8Array(e)}}class BaseWasmFactory{constructor({baseUrl:t=null}){this.baseUrl=t}async fetch({filename:t}){if(!this.baseUrl)throw new Error("Ensure that the `wasmUrl` API parameter is provided.");if(!t)throw new Error("Wasm filename must be specified.");const e=`${this.baseUrl}${t}`;return this._fetch(e).catch((t=>{throw new Error(`Unable to load wasm data at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMWasmFactory extends BaseWasmFactory{async _fetch(t){const e=await fetchData(t,"arraybuffer");return new Uint8Array(e)}}t&&warn("Please use the `legacy` build in Node.js environments.");async function node_utils_fetchData(t){const e=process.getBuiltinModule("fs"),i=await e.promises.readFile(t);return new Uint8Array(i)}class NodeFilterFactory extends BaseFilterFactory{}class NodeCanvasFactory extends BaseCanvasFactory{_createCanvas(t,e){return process.getBuiltinModule("module").createRequire(import.meta.url)("@napi-rs/canvas").createCanvas(t,e)}}class NodeCMapReaderFactory extends BaseCMapReaderFactory{async _fetch(t){return node_utils_fetchData(t)}}class NodeStandardFontDataFactory extends BaseStandardFontDataFactory{async _fetch(t){return node_utils_fetchData(t)}}class NodeWasmFactory extends BaseWasmFactory{async _fetch(t){return node_utils_fetchData(t)}}const st="Fill",nt="Stroke",at="Shading";function applyBoundingBox(t,e){if(!e)return;const i=e[2]-e[0],s=e[3]-e[1],n=new Path2D;n.rect(e[0],e[1],i,s);t.clip(n)}class BaseShadingPattern{isModifyingCurrentTransform(){return!1}getPattern(){unreachable("Abstract method `getPattern` called.")}}class RadialAxialShadingPattern extends BaseShadingPattern{constructor(t){super();this._type=t[1];this._bbox=t[2];this._colorStops=t[3];this._p0=t[4];this._p1=t[5];this._r0=t[6];this._r1=t[7];this.matrix=null}_createGradient(t){let e;"axial"===this._type?e=t.createLinearGradient(this._p0[0],this._p0[1],this._p1[0],this._p1[1]):"radial"===this._type&&(e=t.createRadialGradient(this._p0[0],this._p0[1],this._r0,this._p1[0],this._p1[1],this._r1));for(const t of this._colorStops)e.addColorStop(t[0],t[1]);return e}getPattern(t,e,i,s){let n;if(s===nt||s===st){const a=e.current.getClippedPathBoundingBox(s,getCurrentTransform(t))||[0,0,0,0],r=Math.ceil(a[2]-a[0])||1,o=Math.ceil(a[3]-a[1])||1,l=e.cachedCanvases.getCanvas("pattern",r,o),h=l.context;h.clearRect(0,0,h.canvas.width,h.canvas.height);h.beginPath();h.rect(0,0,h.canvas.width,h.canvas.height);h.translate(-a[0],-a[1]);i=Util.transform(i,[1,0,0,1,a[0],a[1]]);h.transform(...e.baseTransform);this.matrix&&h.transform(...this.matrix);applyBoundingBox(h,this._bbox);h.fillStyle=this._createGradient(h);h.fill();n=t.createPattern(l.canvas,"no-repeat");const d=new DOMMatrix(i);n.setTransform(d)}else{applyBoundingBox(t,this._bbox);n=this._createGradient(t)}return n}}function drawTriangle(t,e,i,s,n,a,r,o){const l=e.coords,h=e.colors,d=t.data,c=4*t.width;let u;if(l[i+1]>l[s+1]){u=i;i=s;s=u;u=a;a=r;r=u}if(l[s+1]>l[n+1]){u=s;s=n;n=u;u=r;r=o;o=u}if(l[i+1]>l[s+1]){u=i;i=s;s=u;u=a;a=r;r=u}const p=(l[i]+e.offsetX)*e.scaleX,g=(l[i+1]+e.offsetY)*e.scaleY,f=(l[s]+e.offsetX)*e.scaleX,m=(l[s+1]+e.offsetY)*e.scaleY,b=(l[n]+e.offsetX)*e.scaleX,A=(l[n+1]+e.offsetY)*e.scaleY;if(g>=A)return;const w=h[a],y=h[a+1],v=h[a+2],_=h[r],x=h[r+1],E=h[r+2],S=h[o],C=h[o+1],T=h[o+2],M=Math.round(g),D=Math.round(A);let P,k,I,R,F,L,O,N;for(let t=M;t<=D;t++){if(tA?1:m===A?0:(m-t)/(m-A);P=f-(f-b)*e;k=_-(_-S)*e;I=x-(x-C)*e;R=E-(E-T)*e}let e;e=tA?1:(g-t)/(g-A);F=p-(p-b)*e;L=w-(w-S)*e;O=y-(y-C)*e;N=v-(v-T)*e;const i=Math.round(Math.min(P,F)),s=Math.round(Math.max(P,F));let n=c*t+4*i;for(let t=i;t<=s;t++){e=(P-t)/(P-F);e<0?e=0:e>1&&(e=1);d[n++]=k-(k-L)*e|0;d[n++]=I-(I-O)*e|0;d[n++]=R-(R-N)*e|0;d[n++]=255}}}function drawFigure(t,e,i){const s=e.coords,n=e.colors;let a,r;switch(e.type){case"lattice":const o=e.verticesPerRow,l=Math.floor(s.length/o)-1,h=o-1;for(a=0;a=Math.ceil(p*A)?y=o:_=!0;S>=Math.ceil(g*w)?v=l:x=!0;const C=this.getSizeAndScale(y,this.ctx.canvas.width,A),T=this.getSizeAndScale(v,this.ctx.canvas.height,w),M=t.cachedCanvases.getCanvas("pattern",C.size,T.size),D=M.context,P=r.createCanvasGraphics(D);P.groupLevel=t.groupLevel;this.setFillAndStrokeStyleToContext(P,s,a);D.translate(-C.scale*h,-T.scale*d);P.transform(C.scale,0,0,T.scale,0,0);D.save();this.clipBbox(P,h,d,c,u);P.baseTransform=getCurrentTransform(P.ctx);P.executeOperatorList(i);P.endDrawing();D.restore();if(_||x){const e=M.canvas;_&&(y=o);x&&(v=l);const i=this.getSizeAndScale(y,this.ctx.canvas.width,A),s=this.getSizeAndScale(v,this.ctx.canvas.height,w),n=i.size,a=s.size,r=t.cachedCanvases.getCanvas("pattern-workaround",n,a),c=r.context,u=_?Math.floor(p/o):0,f=x?Math.floor(g/l):0;for(let t=0;t<=u;t++)for(let i=0;i<=f;i++)c.drawImage(e,n*t,a*i,n,a,0,0,n,a);return{canvas:r.canvas,scaleX:i.scale,scaleY:s.scale,offsetX:h,offsetY:d}}return{canvas:M.canvas,scaleX:C.scale,scaleY:T.scale,offsetX:h,offsetY:d}}getSizeAndScale(t,e,i){const s=Math.max(TilingPattern.MAX_PATTERN_SIZE,e);let n=Math.ceil(t*i);n>=s?n=s:i=n/t;return{scale:i,size:n}}clipBbox(t,e,i,s,n){const a=s-e,r=n-i;t.ctx.rect(e,i,a,r);Util.axialAlignedBoundingBox([e,i,s,n],getCurrentTransform(t.ctx),t.current.minMax);t.clip();t.endPath()}setFillAndStrokeStyleToContext(t,e,i){const s=t.ctx,n=t.current;switch(e){case rt:const t=this.ctx;s.fillStyle=t.fillStyle;s.strokeStyle=t.strokeStyle;n.fillColor=t.fillStyle;n.strokeColor=t.strokeStyle;break;case ot:const a=Util.makeHexColor(i[0],i[1],i[2]);s.fillStyle=a;s.strokeStyle=a;n.fillColor=a;n.strokeColor=a;break;default:throw new FormatError(`Unsupported paint type: ${e}`)}}isModifyingCurrentTransform(){return!1}getPattern(t,e,i,s){let n=i;if(s!==at){n=Util.transform(n,e.baseTransform);this.matrix&&(n=Util.transform(n,this.matrix))}const a=this.createPatternCanvas(e);let r=new DOMMatrix(n);r=r.translate(a.offsetX,a.offsetY);r=r.scale(1/a.scaleX,1/a.scaleY);const o=t.createPattern(a.canvas,"repeat");o.setTransform(r);return o}}function convertBlackAndWhiteToRGBA({src:t,srcPos:e=0,dest:i,width:s,height:n,nonBlackColor:a=4294967295,inverseDecode:r=!1}){const o=util_FeatureTest.isLittleEndian?4278190080:255,[l,h]=r?[a,o]:[o,a],d=s>>3,c=7&s,u=t.length;i=new Uint32Array(i.buffer);let p=0;for(let s=0;s>2),f=i.length,m=s+7>>3,b=4294967295,A=util_FeatureTest.isLittleEndian?4278190080:255;for(u=0;um?s:8*t-7,r=-8&a;let o=0,c=0;for(;n>=1}}for(;l=a){g=n;f=s*g}l=0;for(p=f;p--;){c[l++]=d[h++];c[l++]=d[h++];c[l++]=d[h++];c[l++]=255}t.putImageData(o,0,u*lt)}}}function putBinaryImageMask(t,e){if(e.bitmap){t.drawImage(e.bitmap,0,0);return}const i=e.height,s=e.width,n=i%lt,a=(i-n)/lt,r=0===n?a:a+1,o=t.createImageData(s,lt);let l=0;const h=e.data,d=o.data;for(let e=0;e10&&"function"==typeof i,h=l?Date.now()+15:0;let d=0;const c=this.commonObjs,u=this.objs;let p;for(;;){if(void 0!==s&&r===s.nextBreakPoint){s.breakIt(r,i);return r}p=a[r];if(p!==D.dependency)this[p].apply(this,n[r]);else for(const t of n[r]){const e=t.startsWith("g_")?c:u;if(!e.has(t)){e.get(t,i);return r}}r++;if(r===o)return r;if(l&&++d>10){if(Date.now()>h){i();return r}d=0}}}#Pi(){for(;this.stateStack.length||this.inSMaskMode;)this.restore();this.current.activeSMask=null;this.ctx.restore();if(this.transparentCanvas){this.ctx=this.compositeCtx;this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.drawImage(this.transparentCanvas,0,0);this.ctx.restore();this.transparentCanvas=null}}endDrawing(){this.#Pi();this.cachedCanvases.clear();this.cachedPatterns.clear();for(const t of this._cachedBitmapsMap.values()){for(const e of t.values())"undefined"!=typeof HTMLCanvasElement&&e instanceof HTMLCanvasElement&&(e.width=e.height=0);t.clear()}this._cachedBitmapsMap.clear();this.#ki()}#ki(){if(this.pageColors){const t=this.filterFactory.addHCMFilter(this.pageColors.foreground,this.pageColors.background);if("none"!==t){const e=this.ctx.filter;this.ctx.filter=t;this.ctx.drawImage(this.ctx.canvas,0,0);this.ctx.filter=e}}}_scaleImage(t,e){const i=t.width??t.displayWidth,s=t.height??t.displayHeight;let n,a,r=Math.max(Math.hypot(e[0],e[1]),1),o=Math.max(Math.hypot(e[2],e[3]),1),l=i,h=s,d="prescale1";for(;r>2&&l>1||o>2&&h>1;){let e=l,i=h;if(r>2&&l>1){e=l>=16384?Math.floor(l/2)-1||1:Math.ceil(l/2);r/=l/e}if(o>2&&h>1){i=h>=16384?Math.floor(h/2)-1||1:Math.ceil(h)/2;o/=h/i}n=this.cachedCanvases.getCanvas(d,e,i);a=n.context;a.clearRect(0,0,e,i);a.drawImage(t,0,0,l,h,0,0,e,i);t=n.canvas;l=e;h=i;d="prescale1"===d?"prescale2":"prescale1"}return{img:t,paintWidth:l,paintHeight:h}}_createMaskCanvas(t){const e=this.ctx,{width:i,height:s}=t,n=this.current.fillColor,a=this.current.patternFill,r=getCurrentTransform(e);let o,l,h,d;if((t.bitmap||t.data)&&t.count>1){const e=t.bitmap||t.data.buffer;l=JSON.stringify(a?r:[r.slice(0,4),n]);o=this._cachedBitmapsMap.get(e);if(!o){o=new Map;this._cachedBitmapsMap.set(e,o)}const i=o.get(l);if(i&&!a){return{canvas:i,offsetX:Math.round(Math.min(r[0],r[2])+r[4]),offsetY:Math.round(Math.min(r[1],r[3])+r[5])}}h=i}if(!h){d=this.cachedCanvases.getCanvas("maskCanvas",i,s);putBinaryImageMask(d.context,t)}let c=Util.transform(r,[1/i,0,0,-1/s,0,0]);c=Util.transform(c,[1,0,0,1,0,-s]);const u=ct.slice();Util.axialAlignedBoundingBox([0,0,i,s],c,u);const[p,g,f,m]=u,b=Math.round(f-p)||1,A=Math.round(m-g)||1,w=this.cachedCanvases.getCanvas("fillCanvas",b,A),y=w.context,v=p,_=g;y.translate(-v,-_);y.transform(...c);if(!h){h=this._scaleImage(d.canvas,getCurrentTransformInverse(y));h=h.img;o&&a&&o.set(l,h)}y.imageSmoothingEnabled=getImageSmoothingEnabled(getCurrentTransform(y),t.interpolate);drawImageAtIntegerCoords(y,h,0,0,h.width,h.height,0,0,i,s);y.globalCompositeOperation="source-in";const x=Util.transform(getCurrentTransformInverse(y),[1,0,0,1,-v,-_]);y.fillStyle=a?n.getPattern(e,this,x,st):n;y.fillRect(0,0,i,s);if(o&&!a){this.cachedCanvases.delete("fillCanvas");o.set(l,w.canvas)}return{canvas:w.canvas,offsetX:Math.round(v),offsetY:Math.round(_)}}setLineWidth(t){t!==this.current.lineWidth&&(this._cachedScaleForStroking[0]=-1);this.current.lineWidth=t;this.ctx.lineWidth=t}setLineCap(t){this.ctx.lineCap=ut[t]}setLineJoin(t){this.ctx.lineJoin=pt[t]}setMiterLimit(t){this.ctx.miterLimit=t}setDash(t,e){const i=this.ctx;if(void 0!==i.setLineDash){i.setLineDash(t);i.lineDashOffset=e}}setRenderingIntent(t){}setFlatness(t){}setGState(t){for(const[e,i]of t)switch(e){case"LW":this.setLineWidth(i);break;case"LC":this.setLineCap(i);break;case"LJ":this.setLineJoin(i);break;case"ML":this.setMiterLimit(i);break;case"D":this.setDash(i[0],i[1]);break;case"RI":this.setRenderingIntent(i);break;case"FL":this.setFlatness(i);break;case"Font":this.setFont(i[0],i[1]);break;case"CA":this.current.strokeAlpha=i;break;case"ca":this.ctx.globalAlpha=this.current.fillAlpha=i;break;case"BM":this.ctx.globalCompositeOperation=i;break;case"SMask":this.current.activeSMask=i?this.tempSMask:null;this.tempSMask=null;this.checkSMaskState();break;case"TR":this.ctx.filter=this.current.transferMaps=this.filterFactory.addFilter(i)}}get inSMaskMode(){return!!this.suspendedCtx}checkSMaskState(){const t=this.inSMaskMode;this.current.activeSMask&&!t?this.beginSMaskMode():!this.current.activeSMask&&t&&this.endSMaskMode()}beginSMaskMode(){if(this.inSMaskMode)throw new Error("beginSMaskMode called while already in smask mode");const t=this.ctx.canvas.width,e=this.ctx.canvas.height,i="smaskGroupAt"+this.groupLevel,s=this.cachedCanvases.getCanvas(i,t,e);this.suspendedCtx=this.ctx;const n=this.ctx=s.context;n.setTransform(this.suspendedCtx.getTransform());copyCtxState(this.suspendedCtx,n);!function mirrorContextOperations(t,e){if(t._removeMirroring)throw new Error("Context is already forwarding operations.");t.__originalSave=t.save;t.__originalRestore=t.restore;t.__originalRotate=t.rotate;t.__originalScale=t.scale;t.__originalTranslate=t.translate;t.__originalTransform=t.transform;t.__originalSetTransform=t.setTransform;t.__originalResetTransform=t.resetTransform;t.__originalClip=t.clip;t.__originalMoveTo=t.moveTo;t.__originalLineTo=t.lineTo;t.__originalBezierCurveTo=t.bezierCurveTo;t.__originalRect=t.rect;t.__originalClosePath=t.closePath;t.__originalBeginPath=t.beginPath;t._removeMirroring=()=>{t.save=t.__originalSave;t.restore=t.__originalRestore;t.rotate=t.__originalRotate;t.scale=t.__originalScale;t.translate=t.__originalTranslate;t.transform=t.__originalTransform;t.setTransform=t.__originalSetTransform;t.resetTransform=t.__originalResetTransform;t.clip=t.__originalClip;t.moveTo=t.__originalMoveTo;t.lineTo=t.__originalLineTo;t.bezierCurveTo=t.__originalBezierCurveTo;t.rect=t.__originalRect;t.closePath=t.__originalClosePath;t.beginPath=t.__originalBeginPath;delete t._removeMirroring};t.save=function(){e.save();this.__originalSave()};t.restore=function(){e.restore();this.__originalRestore()};t.translate=function(t,i){e.translate(t,i);this.__originalTranslate(t,i)};t.scale=function(t,i){e.scale(t,i);this.__originalScale(t,i)};t.transform=function(t,i,s,n,a,r){e.transform(t,i,s,n,a,r);this.__originalTransform(t,i,s,n,a,r)};t.setTransform=function(t,i,s,n,a,r){e.setTransform(t,i,s,n,a,r);this.__originalSetTransform(t,i,s,n,a,r)};t.resetTransform=function(){e.resetTransform();this.__originalResetTransform()};t.rotate=function(t){e.rotate(t);this.__originalRotate(t)};t.clip=function(t){e.clip(t);this.__originalClip(t)};t.moveTo=function(t,i){e.moveTo(t,i);this.__originalMoveTo(t,i)};t.lineTo=function(t,i){e.lineTo(t,i);this.__originalLineTo(t,i)};t.bezierCurveTo=function(t,i,s,n,a,r){e.bezierCurveTo(t,i,s,n,a,r);this.__originalBezierCurveTo(t,i,s,n,a,r)};t.rect=function(t,i,s,n){e.rect(t,i,s,n);this.__originalRect(t,i,s,n)};t.closePath=function(){e.closePath();this.__originalClosePath()};t.beginPath=function(){e.beginPath();this.__originalBeginPath()}}(n,this.suspendedCtx);this.setGState([["BM","source-over"]])}endSMaskMode(){if(!this.inSMaskMode)throw new Error("endSMaskMode called while not in smask mode");this.ctx._removeMirroring();copyCtxState(this.ctx,this.suspendedCtx);this.ctx=this.suspendedCtx;this.suspendedCtx=null}compose(t){if(!this.current.activeSMask)return;if(t){t[0]=Math.floor(t[0]);t[1]=Math.floor(t[1]);t[2]=Math.ceil(t[2]);t[3]=Math.ceil(t[3])}else t=[0,0,this.ctx.canvas.width,this.ctx.canvas.height];const e=this.current.activeSMask,i=this.suspendedCtx;this.composeSMask(i,e,this.ctx,t);this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height);this.ctx.restore()}composeSMask(t,e,i,s){const n=s[0],a=s[1],r=s[2]-n,o=s[3]-a;if(0!==r&&0!==o){this.genericComposeSMask(e.context,i,r,o,e.subtype,e.backdrop,e.transferMap,n,a,e.offsetX,e.offsetY);t.save();t.globalAlpha=1;t.globalCompositeOperation="source-over";t.setTransform(1,0,0,1,0,0);t.drawImage(i.canvas,0,0);t.restore()}}genericComposeSMask(t,e,i,s,n,a,r,o,l,h,d){let c=t.canvas,u=o-h,p=l-d;if(a){const e=Util.makeHexColor(...a);if(u<0||p<0||u+i>c.width||p+s>c.height){const t=this.cachedCanvases.getCanvas("maskExtension",i,s),n=t.context;n.drawImage(c,-u,-p);n.globalCompositeOperation="destination-atop";n.fillStyle=e;n.fillRect(0,0,i,s);n.globalCompositeOperation="source-over";c=t.canvas;u=p=0}else{t.save();t.globalAlpha=1;t.setTransform(1,0,0,1,0,0);const n=new Path2D;n.rect(u,p,i,s);t.clip(n);t.globalCompositeOperation="destination-atop";t.fillStyle=e;t.fillRect(u,p,i,s);t.restore()}}e.save();e.globalAlpha=1;e.setTransform(1,0,0,1,0,0);"Alpha"===n&&r?e.filter=this.filterFactory.addAlphaFilter(r):"Luminosity"===n&&(e.filter=this.filterFactory.addLuminosityFilter(r));const g=new Path2D;g.rect(o,l,i,s);e.clip(g);e.globalCompositeOperation="destination-in";e.drawImage(c,u,p,i,s,o,l,i,s);e.restore()}save(){this.inSMaskMode&©CtxState(this.ctx,this.suspendedCtx);this.ctx.save();const t=this.current;this.stateStack.push(t);this.current=t.clone()}restore(){if(0!==this.stateStack.length){this.current=this.stateStack.pop();this.ctx.restore();this.inSMaskMode&©CtxState(this.suspendedCtx,this.ctx);this.checkSMaskState();this.pendingClip=null;this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}else this.inSMaskMode&&this.endSMaskMode()}transform(t,e,i,s,n,a){this.ctx.transform(t,e,i,s,n,a);this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}constructPath(t,e,i){let[s]=e;if(i){if(!(s instanceof Path2D)){const t=e[0]=new Path2D;for(let e=0,i=s.length;e100&&(h=100);this.current.fontSizeScale=i/h;this.ctx.font=`${l} ${o} ${h}px ${r}`}setTextRenderingMode(t){this.current.textRenderingMode=t}setTextRise(t){this.current.textRise=t}moveText(t,e){this.current.x=this.current.lineX+=t;this.current.y=this.current.lineY+=e}setLeadingMoveText(t,e){this.setLeading(-e);this.moveText(t,e)}setTextMatrix(t){const{current:e}=this;e.textMatrix=t;e.textMatrixScale=Math.hypot(t[0],t[1]);e.x=e.lineX=0;e.y=e.lineY=0}nextLine(){this.moveText(0,this.current.leading)}#Ii(t,e,i){const s=new Path2D;s.addPath(t,new DOMMatrix(i).invertSelf().multiplySelf(e));return s}paintChar(t,e,i,s,n){const a=this.ctx,r=this.current,o=r.font,l=r.textRenderingMode,h=r.fontSize/r.fontSizeScale,d=l&w,c=!!(l&y),u=r.patternFill&&!o.missingFile,p=r.patternStroke&&!o.missingFile;let g;(o.disableFontFace||c||u||p)&&(g=o.getPathGenerator(this.commonObjs,t));if(o.disableFontFace||u||p){a.save();a.translate(e,i);a.scale(h,-h);let t;if(d===f||d===b)if(s){t=a.getTransform();a.setTransform(...s);a.fill(this.#Ii(g,t,s))}else a.fill(g);if(d===m||d===b)if(n){t||=a.getTransform();a.setTransform(...n);const{a:e,b:i,c:s,d:r}=t,o=Util.inverseTransform(n),l=Util.transform([e,i,s,r,0,0],o);Util.singularValueDecompose2dScale(l,dt);a.lineWidth*=Math.max(dt[0],dt[1])/h;a.stroke(this.#Ii(g,t,n))}else{a.lineWidth/=h;a.stroke(g)}a.restore()}else{d!==f&&d!==b||a.fillText(t,e,i);d!==m&&d!==b||a.strokeText(t,e,i)}if(c){(this.pendingTextPaths||=[]).push({transform:getCurrentTransform(a),x:e,y:i,fontSize:h,path:g})}}get isFontSubpixelAAEnabled(){const{context:t}=this.cachedCanvases.getCanvas("isFontSubpixelAAEnabled",10,10);t.scale(1.5,1);t.fillText("I",0,10);const e=t.getImageData(0,0,10,10).data;let i=!1;for(let t=3;t0&&e[t]<255){i=!0;break}return shadow(this,"isFontSubpixelAAEnabled",i)}showText(t){const e=this.current,i=e.font;if(i.isType3Font)return this.showType3Text(t);const s=e.fontSize;if(0===s)return;const n=this.ctx,a=e.fontSizeScale,r=e.charSpacing,o=e.wordSpacing,l=e.fontDirection,h=e.textHScale*l,d=t.length,c=i.vertical,u=c?1:-1,p=i.defaultVMetrics,g=s*e.fontMatrix[0],A=e.textRenderingMode===f&&!i.disableFontFace&&!e.patternFill;n.save();e.textMatrix&&n.transform(...e.textMatrix);n.translate(e.x,e.y+e.textRise);l>0?n.scale(h,-1):n.scale(h,1);let y,v;if(e.patternFill){n.save();const t=e.fillColor.getPattern(n,this,getCurrentTransformInverse(n),st);y=getCurrentTransform(n);n.restore();n.fillStyle=t}if(e.patternStroke){n.save();const t=e.strokeColor.getPattern(n,this,getCurrentTransformInverse(n),nt);v=getCurrentTransform(n);n.restore();n.strokeStyle=t}let _=e.lineWidth;const x=e.textMatrixScale;if(0===x||0===_){const t=e.textRenderingMode&w;t!==m&&t!==b||(_=this.getSinglePixelWidth())}else _/=x;if(1!==a){n.scale(a,a);_/=a}n.lineWidth=_;if(i.isInvalidPDFjsFont){const i=[];let s=0;for(const e of t){i.push(e.unicode);s+=e.width}n.fillText(i.join(""),0,0);e.x+=s*g*h;n.restore();this.compose();return}let E,S=0;for(E=0;E0){const t=1e3*n.measureText(f).width/s*a;if(_new CanvasGraphics(t,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:this.optionalContentConfig,markedContentStack:this.markedContentStack})};e=new TilingPattern(t,this.ctx,s,i)}else e=this._getPattern(t[1],t[2]);return e}setStrokeColorN(){this.current.strokeColor=this.getColorN_Pattern(arguments);this.current.patternStroke=!0}setFillColorN(){this.current.fillColor=this.getColorN_Pattern(arguments);this.current.patternFill=!0}setStrokeRGBColor(t,e,i){this.ctx.strokeStyle=this.current.strokeColor=Util.makeHexColor(t,e,i);this.current.patternStroke=!1}setStrokeTransparent(){this.ctx.strokeStyle=this.current.strokeColor="transparent";this.current.patternStroke=!1}setFillRGBColor(t,e,i){this.ctx.fillStyle=this.current.fillColor=Util.makeHexColor(t,e,i);this.current.patternFill=!1}setFillTransparent(){this.ctx.fillStyle=this.current.fillColor="transparent";this.current.patternFill=!1}_getPattern(t,e=null){let i;if(this.cachedPatterns.has(t))i=this.cachedPatterns.get(t);else{i=function getShadingPattern(t){switch(t[0]){case"RadialAxial":return new RadialAxialShadingPattern(t);case"Mesh":return new MeshShadingPattern(t);case"Dummy":return new DummyShadingPattern}throw new Error(`Unknown IR type: ${t[0]}`)}(this.getObject(t));this.cachedPatterns.set(t,i)}e&&(i.matrix=e);return i}shadingFill(t){if(!this.contentVisible)return;const e=this.ctx;this.save();const i=this._getPattern(t);e.fillStyle=i.getPattern(e,this,getCurrentTransformInverse(e),at);const s=getCurrentTransformInverse(e);if(s){const{width:t,height:i}=e.canvas,n=ct.slice();Util.axialAlignedBoundingBox([0,0,t,i],s,n);const[a,r,o,l]=n;this.ctx.fillRect(a,r,o-a,l-r)}else this.ctx.fillRect(-1e10,-1e10,2e10,2e10);this.compose(this.current.getClippedPathBoundingBox());this.restore()}beginInlineImage(){unreachable("Should not call beginInlineImage")}beginImageData(){unreachable("Should not call beginImageData")}paintFormXObjectBegin(t,e){if(this.contentVisible){this.save();this.baseTransformStack.push(this.baseTransform);t&&this.transform(...t);this.baseTransform=getCurrentTransform(this.ctx);if(e){Util.axialAlignedBoundingBox(e,this.baseTransform,this.current.minMax);const[t,i,s,n]=e,a=new Path2D;a.rect(t,i,s-t,n-i);this.ctx.clip(a);this.endPath()}}}paintFormXObjectEnd(){if(this.contentVisible){this.restore();this.baseTransform=this.baseTransformStack.pop()}}beginGroup(t){if(!this.contentVisible)return;this.save();if(this.inSMaskMode){this.endSMaskMode();this.current.activeSMask=null}const e=this.ctx;t.isolated||info("TODO: Support non-isolated groups.");t.knockout&&warn("Knockout groups not supported.");const i=getCurrentTransform(e);t.matrix&&e.transform(...t.matrix);if(!t.bbox)throw new Error("Bounding box is required.");let s=ct.slice();Util.axialAlignedBoundingBox(t.bbox,getCurrentTransform(e),s);const n=[0,0,e.canvas.width,e.canvas.height];s=Util.intersect(s,n)||[0,0,0,0];const a=Math.floor(s[0]),r=Math.floor(s[1]),o=Math.max(Math.ceil(s[2])-a,1),l=Math.max(Math.ceil(s[3])-r,1);this.current.startNewPathAndClipBox([0,0,o,l]);let h="groupAt"+this.groupLevel;t.smask&&(h+="_smask_"+this.smaskCounter++%2);const d=this.cachedCanvases.getCanvas(h,o,l),c=d.context;c.translate(-a,-r);c.transform(...i);let u=new Path2D;const[p,g,f,m]=t.bbox;u.rect(p,g,f-p,m-g);if(t.matrix){const e=new Path2D;e.addPath(u,new DOMMatrix(t.matrix));u=e}c.clip(u);if(t.smask)this.smaskStack.push({canvas:d.canvas,context:c,offsetX:a,offsetY:r,subtype:t.smask.subtype,backdrop:t.smask.backdrop,transferMap:t.smask.transferMap||null,startTransformInverse:null});else{e.setTransform(1,0,0,1,0,0);e.translate(a,r);e.save()}copyCtxState(e,c);this.ctx=c;this.setGState([["BM","source-over"],["ca",1],["CA",1]]);this.groupStack.push(e);this.groupLevel++}endGroup(t){if(!this.contentVisible)return;this.groupLevel--;const e=this.ctx,i=this.groupStack.pop();this.ctx=i;this.ctx.imageSmoothingEnabled=!1;if(t.smask){this.tempSMask=this.smaskStack.pop();this.restore()}else{this.ctx.restore();const t=getCurrentTransform(this.ctx);this.restore();this.ctx.save();this.ctx.setTransform(...t);const i=ct.slice();Util.axialAlignedBoundingBox([0,0,e.canvas.width,e.canvas.height],t,i);this.ctx.drawImage(e.canvas,0,0);this.ctx.restore();this.compose(i)}}beginAnnotation(t,e,i,s,n){this.#Pi();resetCtxToDefault(this.ctx);this.ctx.save();this.save();this.baseTransform&&this.ctx.setTransform(...this.baseTransform);if(e){const s=e[2]-e[0],a=e[3]-e[1];if(n&&this.annotationCanvasMap){(i=i.slice())[4]-=e[0];i[5]-=e[1];(e=e.slice())[0]=e[1]=0;e[2]=s;e[3]=a;Util.singularValueDecompose2dScale(getCurrentTransform(this.ctx),dt);const{viewportScale:n}=this,r=Math.ceil(s*this.outputScaleX*n),o=Math.ceil(a*this.outputScaleY*n);this.annotationCanvas=this.canvasFactory.create(r,o);const{canvas:l,context:h}=this.annotationCanvas;this.annotationCanvasMap.set(t,l);this.annotationCanvas.savedCtx=this.ctx;this.ctx=h;this.ctx.save();this.ctx.setTransform(dt[0],0,0,-dt[1],0,a*dt[1]);resetCtxToDefault(this.ctx)}else{resetCtxToDefault(this.ctx);this.endPath();const t=new Path2D;t.rect(e[0],e[1],s,a);this.ctx.clip(t)}}this.current=new CanvasExtraState(this.ctx.canvas.width,this.ctx.canvas.height);this.transform(...i);this.transform(...s)}endAnnotation(){if(this.annotationCanvas){this.ctx.restore();this.#ki();this.ctx=this.annotationCanvas.savedCtx;delete this.annotationCanvas.savedCtx;delete this.annotationCanvas}}paintImageMaskXObject(t){if(!this.contentVisible)return;const e=t.count;(t=this.getObject(t.data,t)).count=e;const i=this.ctx,s=this._createMaskCanvas(t),n=s.canvas;i.save();i.setTransform(1,0,0,1,0,0);i.drawImage(n,s.offsetX,s.offsetY);i.restore();this.compose()}paintImageMaskXObjectRepeat(t,e,i=0,s=0,n,a){if(!this.contentVisible)return;t=this.getObject(t.data,t);const r=this.ctx;r.save();const o=getCurrentTransform(r);r.transform(e,i,s,n,0,0);const l=this._createMaskCanvas(t);r.setTransform(1,0,0,1,l.offsetX-o[4],l.offsetY-o[5]);for(let t=0,h=a.length;te?h/e:1;r=l>e?l/e:1}}this._cachedScaleForStroking[0]=a;this._cachedScaleForStroking[1]=r}return this._cachedScaleForStroking}rescaleAndStroke(t,e){const{ctx:i,current:{lineWidth:s}}=this,[n,a]=this.getScaleForStroking();if(n===a){i.lineWidth=(s||1)*n;i.stroke(t);return}const r=i.getLineDash();e&&i.save();i.scale(n,a);ht.a=1/n;ht.d=1/a;const o=new Path2D;o.addPath(t,ht);if(r.length>0){const t=Math.max(n,a);i.setLineDash(r.map((e=>e/t)));i.lineDashOffset/=t}i.lineWidth=s||1;i.stroke(o);e&&i.restore()}isContentVisible(){for(let t=this.markedContentStack.length-1;t>=0;t--)if(!this.markedContentStack[t].visible)return!1;return!0}}for(const t in D)void 0!==CanvasGraphics.prototype[t]&&(CanvasGraphics.prototype[D[t]]=CanvasGraphics.prototype[t]);class GlobalWorkerOptions{static#Ri=null;static#Fi="";static get workerPort(){return this.#Ri}static set workerPort(t){if(!("undefined"!=typeof Worker&&t instanceof Worker)&&null!==t)throw new Error("Invalid `workerPort` type.");this.#Ri=t}static get workerSrc(){return this.#Fi}static set workerSrc(t){if("string"!=typeof t)throw new Error("Invalid `workerSrc` type.");this.#Fi=t}}class Metadata{#Li;#Oi;constructor({parsedData:t,rawData:e}){this.#Li=t;this.#Oi=e}getRaw(){return this.#Oi}get(t){return this.#Li.get(t)??null}[Symbol.iterator](){return this.#Li.entries()}}const mt=Symbol("INTERNAL");class OptionalContentGroup{#Ni=!1;#Bi=!1;#Hi=!1;#Ui=!0;constructor(t,{name:e,intent:i,usage:s,rbGroups:r}){this.#Ni=!!(t&n);this.#Bi=!!(t&a);this.name=e;this.intent=i;this.usage=s;this.rbGroups=r}get visible(){if(this.#Hi)return this.#Ui;if(!this.#Ui)return!1;const{print:t,view:e}=this.usage;return this.#Ni?"OFF"!==e?.viewState:!this.#Bi||"OFF"!==t?.printState}_setVisible(t,e,i=!1){t!==mt&&unreachable("Internal method `_setVisible` called.");this.#Hi=i;this.#Ui=e}}class OptionalContentConfig{#zi=null;#Gi=new Map;#$i=null;#Wi=null;constructor(t,e=n){this.renderingIntent=e;this.name=null;this.creator=null;if(null!==t){this.name=t.name;this.creator=t.creator;this.#Wi=t.order;for(const i of t.groups)this.#Gi.set(i.id,new OptionalContentGroup(e,i));if("OFF"===t.baseState)for(const t of this.#Gi.values())t._setVisible(mt,!1);for(const e of t.on)this.#Gi.get(e)._setVisible(mt,!0);for(const e of t.off)this.#Gi.get(e)._setVisible(mt,!1);this.#$i=this.getHash()}}#Vi(t){const e=t.length;if(e<2)return!0;const i=t[0];for(let s=1;s0){const t=n instanceof Uint8Array&&n.byteLength===n.buffer.byteLength?n.buffer:new Uint8Array(n).buffer;this._queuedChunks.push(t)}this._pdfDataRangeTransport=t;this._isStreamingSupported=!i;this._isRangeSupported=!e;this._contentLength=s;this._fullRequestReader=null;this._rangeReaders=[];t.addRangeListener(((t,e)=>{this._onReceiveData({begin:t,chunk:e})}));t.addProgressListener(((t,e)=>{this._onProgress({loaded:t,total:e})}));t.addProgressiveReadListener((t=>{this._onReceiveData({chunk:t})}));t.addProgressiveDoneListener((()=>{this._onProgressiveDone()}));t.transportReady()}_onReceiveData({begin:t,chunk:e}){const i=e instanceof Uint8Array&&e.byteLength===e.buffer.byteLength?e.buffer:new Uint8Array(e).buffer;if(void 0===t)this._fullRequestReader?this._fullRequestReader._enqueue(i):this._queuedChunks.push(i);else{assert(this._rangeReaders.some((function(e){if(e._begin!==t)return!1;e._enqueue(i);return!0})),"_onReceiveData - no `PDFDataTransportStreamRangeReader` instance found.")}}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}_onProgress(t){void 0===t.total?this._rangeReaders[0]?.onProgress?.({loaded:t.loaded}):this._fullRequestReader?.onProgress?.({loaded:t.loaded,total:t.total})}_onProgressiveDone(){this._fullRequestReader?.progressiveDone();this._progressiveDone=!0}_removeRangeReader(t){const e=this._rangeReaders.indexOf(t);e>=0&&this._rangeReaders.splice(e,1)}getFullReader(){assert(!this._fullRequestReader,"PDFDataTransportStream.getFullReader can only be called once.");const t=this._queuedChunks;this._queuedChunks=null;return new PDFDataTransportStreamReader(this,t,this._progressiveDone,this._contentDispositionFilename)}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new PDFDataTransportStreamRangeReader(this,t,e);this._pdfDataRangeTransport.requestDataRange(t,e);this._rangeReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeReaders.slice(0))e.cancel(t);this._pdfDataRangeTransport.abort()}}class PDFDataTransportStreamReader{constructor(t,e,i=!1,s=null){this._stream=t;this._done=i||!1;this._filename=isPdfFile(s)?s:null;this._queuedChunks=e||[];this._loaded=0;for(const t of this._queuedChunks)this._loaded+=t.byteLength;this._requests=[];this._headersReady=Promise.resolve();t._fullRequestReader=this;this.onProgress=null}_enqueue(t){if(!this._done){if(this._requests.length>0){this._requests.shift().resolve({value:t,done:!1})}else this._queuedChunks.push(t);this._loaded+=t.byteLength}}get headersReady(){return this._headersReady}get filename(){return this._filename}get isRangeSupported(){return this._stream._isRangeSupported}get isStreamingSupported(){return this._stream._isStreamingSupported}get contentLength(){return this._stream._contentLength}async read(){if(this._queuedChunks.length>0){return{value:this._queuedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}progressiveDone(){this._done||(this._done=!0)}}class PDFDataTransportStreamRangeReader{constructor(t,e,i){this._stream=t;this._begin=e;this._end=i;this._queuedChunk=null;this._requests=[];this._done=!1;this.onProgress=null}_enqueue(t){if(!this._done){if(0===this._requests.length)this._queuedChunk=t;else{this._requests.shift().resolve({value:t,done:!1});for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}this._done=!0;this._stream._removeRangeReader(this)}}get isStreamingSupported(){return!1}async read(){if(this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._stream._removeRangeReader(this)}}function createHeaders(t,e){const i=new Headers;if(!t||!e||"object"!=typeof e)return i;for(const t in e){const s=e[t];void 0!==s&&i.append(t,s)}return i}function getResponseOrigin(t){return URL.parse(t)?.origin??null}function validateRangeRequestCapabilities({responseHeaders:t,isHttp:e,rangeChunkSize:i,disableRange:s}){const n={allowRangeRequests:!1,suggestedLength:void 0},a=parseInt(t.get("Content-Length"),10);if(!Number.isInteger(a))return n;n.suggestedLength=a;if(a<=2*i)return n;if(s||!e)return n;if("bytes"!==t.get("Accept-Ranges"))return n;if("identity"!==(t.get("Content-Encoding")||"identity"))return n;n.allowRangeRequests=!0;return n}function extractFilenameFromHeader(t){const e=t.get("Content-Disposition");if(e){let t=function getFilenameFromContentDispositionHeader(t){let e=!0,i=toParamRegExp("filename\\*","i").exec(t);if(i){i=i[1];let t=rfc2616unquote(i);t=unescape(t);t=rfc5987decode(t);t=rfc2047decode(t);return fixupEncoding(t)}i=function rfc2231getparam(t){const e=[];let i;const s=toParamRegExp("filename\\*((?!0\\d)\\d+)(\\*?)","ig");for(;null!==(i=s.exec(t));){let[,t,s,n]=i;t=parseInt(t,10);if(t in e){if(0===t)break}else e[t]=[s,n]}const n=[];for(let t=0;t{t._responseOrigin=getResponseOrigin(e.url);if(!validateResponseStatus(e.status))throw createResponseError(e.status,s);this._reader=e.body.getReader();this._headersCapability.resolve();const i=e.headers,{allowRangeRequests:n,suggestedLength:a}=validateRangeRequestCapabilities({responseHeaders:i,isHttp:t.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});this._isRangeSupported=n;this._contentLength=a||this._contentLength;this._filename=extractFilenameFromHeader(i);!this._isStreamingSupported&&this._isRangeSupported&&this.cancel(new AbortException("Streaming is disabled."))})).catch(this._headersCapability.reject);this.onProgress=null}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._headersCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class PDFFetchStreamRangeReader{constructor(t,e,i){this._stream=t;this._reader=null;this._loaded=0;const s=t.source;this._withCredentials=s.withCredentials||!1;this._readCapability=Promise.withResolvers();this._isStreamingSupported=!s.disableStream;this._abortController=new AbortController;const n=new Headers(t.headers);n.append("Range",`bytes=${e}-${i-1}`);const a=s.url;fetch(a,createFetchOptions(n,this._withCredentials,this._abortController)).then((e=>{const i=getResponseOrigin(e.url);if(i!==t._responseOrigin)throw new Error(`Expected range response-origin "${i}" to match "${t._responseOrigin}".`);if(!validateResponseStatus(e.status))throw createResponseError(e.status,a);this._readCapability.resolve();this._reader=e.body.getReader()})).catch(this._readCapability.reject);this.onProgress=null}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded});return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class NetworkManager{_responseOrigin=null;constructor({url:t,httpHeaders:e,withCredentials:i}){this.url=t;this.isHttp=/^https?:/i.test(t);this.headers=createHeaders(this.isHttp,e);this.withCredentials=i||!1;this.currXhrId=0;this.pendingRequests=Object.create(null)}request(t){const e=new XMLHttpRequest,i=this.currXhrId++,s=this.pendingRequests[i]={xhr:e};e.open("GET",this.url);e.withCredentials=this.withCredentials;for(const[t,i]of this.headers)e.setRequestHeader(t,i);if(this.isHttp&&"begin"in t&&"end"in t){e.setRequestHeader("Range",`bytes=${t.begin}-${t.end-1}`);s.expectedStatus=206}else s.expectedStatus=200;e.responseType="arraybuffer";assert(t.onError,"Expected `onError` callback to be provided.");e.onerror=()=>{t.onError(e.status)};e.onreadystatechange=this.onStateChange.bind(this,i);e.onprogress=this.onProgress.bind(this,i);s.onHeadersReceived=t.onHeadersReceived;s.onDone=t.onDone;s.onError=t.onError;s.onProgress=t.onProgress;e.send(null);return i}onProgress(t,e){const i=this.pendingRequests[t];i&&i.onProgress?.(e)}onStateChange(t,e){const i=this.pendingRequests[t];if(!i)return;const s=i.xhr;if(s.readyState>=2&&i.onHeadersReceived){i.onHeadersReceived();delete i.onHeadersReceived}if(4!==s.readyState)return;if(!(t in this.pendingRequests))return;delete this.pendingRequests[t];if(0===s.status&&this.isHttp){i.onError(s.status);return}const n=s.status||200;if(!(200===n&&206===i.expectedStatus)&&n!==i.expectedStatus){i.onError(s.status);return}const a=function network_getArrayBuffer(t){const e=t.response;return"string"!=typeof e?e:stringToBytes(e).buffer}(s);if(206===n){const t=s.getResponseHeader("Content-Range"),e=/bytes (\d+)-(\d+)\/(\d+)/.exec(t);if(e)i.onDone({begin:parseInt(e[1],10),chunk:a});else{warn('Missing or invalid "Content-Range" header.');i.onError(0)}}else a?i.onDone({begin:0,chunk:a}):i.onError(s.status)}getRequestXhr(t){return this.pendingRequests[t].xhr}isPendingRequest(t){return t in this.pendingRequests}abortRequest(t){const e=this.pendingRequests[t].xhr;delete this.pendingRequests[t];e.abort()}}class PDFNetworkStream{constructor(t){this._source=t;this._manager=new NetworkManager(t);this._rangeChunkSize=t.rangeChunkSize;this._fullRequestReader=null;this._rangeRequestReaders=[]}_onRangeRequestReaderClosed(t){const e=this._rangeRequestReaders.indexOf(t);e>=0&&this._rangeRequestReaders.splice(e,1)}getFullReader(){assert(!this._fullRequestReader,"PDFNetworkStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNetworkStreamFullRequestReader(this._manager,this._source);return this._fullRequestReader}getRangeReader(t,e){const i=new PDFNetworkStreamRangeRequestReader(this._manager,t,e);i.onClosed=this._onRangeRequestReaderClosed.bind(this);this._rangeRequestReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}}class PDFNetworkStreamFullRequestReader{constructor(t,e){this._manager=t;this._url=e.url;this._fullRequestId=t.request({onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)});this._headersCapability=Promise.withResolvers();this._disableRange=e.disableRange||!1;this._contentLength=e.length;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!1;this._isRangeSupported=!1;this._cachedChunks=[];this._requests=[];this._done=!1;this._storedError=void 0;this._filename=null;this.onProgress=null}_onHeadersReceived(){const t=this._fullRequestId,e=this._manager.getRequestXhr(t);this._manager._responseOrigin=getResponseOrigin(e.responseURL);const i=e.getAllResponseHeaders(),s=new Headers(i?i.trimStart().replace(/[^\S ]+$/,"").split(/[\r\n]+/).map((t=>{const[e,...i]=t.split(": ");return[e,i.join(": ")]})):[]),{allowRangeRequests:n,suggestedLength:a}=validateRangeRequestCapabilities({responseHeaders:s,isHttp:this._manager.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});n&&(this._isRangeSupported=!0);this._contentLength=a||this._contentLength;this._filename=extractFilenameFromHeader(s);this._isRangeSupported&&this._manager.abortRequest(t);this._headersCapability.resolve()}_onDone(t){if(t)if(this._requests.length>0){this._requests.shift().resolve({value:t.chunk,done:!1})}else this._cachedChunks.push(t.chunk);this._done=!0;if(!(this._cachedChunks.length>0)){for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}}_onError(t){this._storedError=createResponseError(t,this._url);this._headersCapability.reject(this._storedError);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._cachedChunks.length=0}_onProgress(t){this.onProgress?.({loaded:t.loaded,total:t.lengthComputable?t.total:this._contentLength})}get filename(){return this._filename}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}get contentLength(){return this._contentLength}get headersReady(){return this._headersCapability.promise}async read(){await this._headersCapability.promise;if(this._storedError)throw this._storedError;if(this._cachedChunks.length>0){return{value:this._cachedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;this._headersCapability.reject(t);for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._fullRequestId)&&this._manager.abortRequest(this._fullRequestId);this._fullRequestReader=null}}class PDFNetworkStreamRangeRequestReader{constructor(t,e,i){this._manager=t;this._url=t.url;this._requestId=t.request({begin:e,end:i,onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)});this._requests=[];this._queuedChunk=null;this._done=!1;this._storedError=void 0;this.onProgress=null;this.onClosed=null}_onHeadersReceived(){const t=getResponseOrigin(this._manager.getRequestXhr(this._requestId)?.responseURL);if(t!==this._manager._responseOrigin){this._storedError=new Error(`Expected range response-origin "${t}" to match "${this._manager._responseOrigin}".`);this._onError(0)}}_close(){this.onClosed?.(this)}_onDone(t){const e=t.chunk;if(this._requests.length>0){this._requests.shift().resolve({value:e,done:!1})}else this._queuedChunk=e;this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._close()}_onError(t){this._storedError??=createResponseError(t,this._url);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._queuedChunk=null}_onProgress(t){this.isStreamingSupported||this.onProgress?.({loaded:t.loaded})}get isStreamingSupported(){return!1}async read(){if(this._storedError)throw this._storedError;if(null!==this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._requestId)&&this._manager.abortRequest(this._requestId);this._close()}}const bt=/^[a-z][a-z0-9\-+.]+:/i;class PDFNodeStream{constructor(t){this.source=t;this.url=function parseUrlOrPath(t){if(bt.test(t))return new URL(t);const e=process.getBuiltinModule("url");return new URL(e.pathToFileURL(t))}(t.url);assert("file:"===this.url.protocol,"PDFNodeStream only supports file:// URLs.");this._fullRequestReader=null;this._rangeRequestReaders=[]}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}getFullReader(){assert(!this._fullRequestReader,"PDFNodeStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNodeStreamFsFullReader(this);return this._fullRequestReader}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new PDFNodeStreamFsRangeReader(this,t,e);this._rangeRequestReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}}class PDFNodeStreamFsFullReader{constructor(t){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;const e=t.source;this._contentLength=e.length;this._loaded=0;this._filename=null;this._disableRange=e.disableRange||!1;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!e.disableStream;this._isRangeSupported=!e.disableRange;this._readableStream=null;this._readCapability=Promise.withResolvers();this._headersCapability=Promise.withResolvers();const i=process.getBuiltinModule("fs");i.promises.lstat(this._url).then((t=>{this._contentLength=t.size;this._setReadableStream(i.createReadStream(this._url));this._headersCapability.resolve()}),(t=>{"ENOENT"===t.code&&(t=createResponseError(0,this._url.href));this._storedError=t;this._headersCapability.reject(t)}))}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=Promise.withResolvers();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));!this._isStreamingSupported&&this._isRangeSupported&&this._error(new AbortException("streaming is disabled"));this._storedError&&this._readableStream.destroy(this._storedError)}}class PDFNodeStreamFsRangeReader{constructor(t,e,i){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;this._loaded=0;this._readableStream=null;this._readCapability=Promise.withResolvers();const s=t.source;this._isStreamingSupported=!s.disableStream;const n=process.getBuiltinModule("fs");this._setReadableStream(n.createReadStream(this._url,{start:e,end:i-1}))}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=Promise.withResolvers();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));this._storedError&&this._readableStream.destroy(this._storedError)}}class TextLayer{#ji=Promise.withResolvers();#mt=null;#qi=!1;#Xi=!!globalThis.FontInspector?.enabled;#Ki=null;#Yi=null;#Qi=0;#Ji=0;#Zi=null;#ts=null;#es=0;#is=0;#ss=Object.create(null);#ns=[];#as=null;#rs=[];#os=new WeakMap;#ls=null;static#hs=new Map;static#ds=new Map;static#cs=new WeakMap;static#us=null;static#ps=new Set;constructor({textContentSource:t,container:e,viewport:i}){if(t instanceof ReadableStream)this.#as=t;else{if("object"!=typeof t)throw new Error('No "textContentSource" parameter specified.');this.#as=new ReadableStream({start(e){e.enqueue(t);e.close()}})}this.#mt=this.#ts=e;this.#is=i.scale*OutputScale.pixelRatio;this.#es=i.rotation;this.#Yi={div:null,properties:null,ctx:null};const{pageWidth:s,pageHeight:n,pageX:a,pageY:r}=i.rawDims;this.#ls=[1,0,0,-1,-a,r+n];this.#Ji=s;this.#Qi=n;TextLayer.#gs();setLayerDimensions(e,i);this.#ji.promise.finally((()=>{TextLayer.#ps.delete(this);this.#Yi=null;this.#ss=null})).catch((()=>{}))}static get fontFamilyMap(){const{isWindows:t,isFirefox:e}=util_FeatureTest.platform;return shadow(this,"fontFamilyMap",new Map([["sans-serif",(t&&e?"Calibri, ":"")+"sans-serif"],["monospace",(t&&e?"Lucida Console, ":"")+"monospace"]]))}render(){const pump=()=>{this.#Zi.read().then((({value:t,done:e})=>{if(e)this.#ji.resolve();else{this.#Ki??=t.lang;Object.assign(this.#ss,t.styles);this.#fs(t.items);pump()}}),this.#ji.reject)};this.#Zi=this.#as.getReader();TextLayer.#ps.add(this);pump();return this.#ji.promise}update({viewport:t,onBefore:e=null}){const i=t.scale*OutputScale.pixelRatio,s=t.rotation;if(s!==this.#es){e?.();this.#es=s;setLayerDimensions(this.#ts,{rotation:s})}if(i!==this.#is){e?.();this.#is=i;const t={div:null,properties:null,ctx:TextLayer.#ms(this.#Ki)};for(const e of this.#rs){t.properties=this.#os.get(e);t.div=e;this.#bs(t)}}}cancel(){const t=new AbortException("TextLayer task cancelled.");this.#Zi?.cancel(t).catch((()=>{}));this.#Zi=null;this.#ji.reject(t)}get textDivs(){return this.#rs}get textContentItemsStr(){return this.#ns}#fs(t){if(this.#qi)return;this.#Yi.ctx??=TextLayer.#ms(this.#Ki);const e=this.#rs,i=this.#ns;for(const s of t){if(e.length>1e5){warn("Ignoring additional textDivs for performance reasons.");this.#qi=!0;return}if(void 0!==s.str){i.push(s.str);this.#As(s)}else if("beginMarkedContentProps"===s.type||"beginMarkedContent"===s.type){const t=this.#mt;this.#mt=document.createElement("span");this.#mt.classList.add("markedContent");null!==s.id&&this.#mt.setAttribute("id",`${s.id}`);t.append(this.#mt)}else"endMarkedContent"===s.type&&(this.#mt=this.#mt.parentNode)}}#As(t){const e=document.createElement("span"),i={angle:0,canvasWidth:0,hasText:""!==t.str,hasEOL:t.hasEOL,fontSize:0};this.#rs.push(e);const s=Util.transform(this.#ls,t.transform);let n=Math.atan2(s[1],s[0]);const a=this.#ss[t.fontName];a.vertical&&(n+=Math.PI/2);let r=this.#Xi&&a.fontSubstitution||a.fontFamily;r=TextLayer.fontFamilyMap.get(r)||r;const o=Math.hypot(s[2],s[3]),l=o*TextLayer.#ws(r,a,this.#Ki);let h,d;if(0===n){h=s[4];d=s[5]-l}else{h=s[4]+l*Math.sin(n);d=s[5]-l*Math.cos(n)}const c="calc(var(--total-scale-factor) *",u=e.style;if(this.#mt===this.#ts){u.left=`${(100*h/this.#Ji).toFixed(2)}%`;u.top=`${(100*d/this.#Qi).toFixed(2)}%`}else{u.left=`${c}${h.toFixed(2)}px)`;u.top=`${c}${d.toFixed(2)}px)`}u.fontSize=`${c}${(TextLayer.#us*o).toFixed(2)}px)`;u.fontFamily=r;i.fontSize=o;e.setAttribute("role","presentation");e.textContent=t.str;e.dir=t.dir;this.#Xi&&(e.dataset.fontName=a.fontSubstitutionLoadedName||t.fontName);0!==n&&(i.angle=n*(180/Math.PI));let p=!1;if(t.str.length>1)p=!0;else if(" "!==t.str&&t.transform[0]!==t.transform[3]){const e=Math.abs(t.transform[0]),i=Math.abs(t.transform[3]);e!==i&&Math.max(e,i)/Math.min(e,i)>1.5&&(p=!0)}p&&(i.canvasWidth=a.vertical?t.height:t.width);this.#os.set(e,i);this.#Yi.div=e;this.#Yi.properties=i;this.#bs(this.#Yi);i.hasText&&this.#mt.append(e);if(i.hasEOL){const t=document.createElement("br");t.setAttribute("role","presentation");this.#mt.append(t)}}#bs(t){const{div:e,properties:i,ctx:s}=t,{style:n}=e;let a="";TextLayer.#us>1&&(a=`scale(${1/TextLayer.#us})`);if(0!==i.canvasWidth&&i.hasText){const{fontFamily:t}=n,{canvasWidth:r,fontSize:o}=i;TextLayer.#ys(s,o*this.#is,t);const{width:l}=s.measureText(e.textContent);l>0&&(a=`scaleX(${r*this.#is/l}) ${a}`)}0!==i.angle&&(a=`rotate(${i.angle}deg) ${a}`);a.length>0&&(n.transform=a)}static cleanup(){if(!(this.#ps.size>0)){this.#hs.clear();for(const{canvas:t}of this.#ds.values())t.remove();this.#ds.clear()}}static#ms(t=null){let e=this.#ds.get(t||="");if(!e){const i=document.createElement("canvas");i.className="hiddenCanvasElement";i.lang=t;document.body.append(i);e=i.getContext("2d",{alpha:!1,willReadFrequently:!0});this.#ds.set(t,e);this.#cs.set(e,{size:0,family:""})}return e}static#ys(t,e,i){const s=this.#cs.get(t);if(e!==s.size||i!==s.family){t.font=`${e}px ${i}`;s.size=e;s.family=i}}static#gs(){if(null!==this.#us)return;const t=document.createElement("div");t.style.opacity=0;t.style.lineHeight=1;t.style.fontSize="1px";t.style.position="absolute";t.textContent="X";document.body.append(t);this.#us=t.getBoundingClientRect().height;t.remove()}static#ws(t,e,i){const s=this.#hs.get(t);if(s)return s;const n=this.#ms(i);n.canvas.width=n.canvas.height=30;this.#ys(n,30,t);const a=n.measureText(""),r=a.fontBoundingBoxAscent,o=Math.abs(a.fontBoundingBoxDescent);n.canvas.width=n.canvas.height=0;let l=.8;if(r)l=r/(r+o);else{util_FeatureTest.platform.isFirefox&&warn("Enable the `dom.textMetrics.fontBoundingBox.enabled` preference in `about:config` to improve TextLayer rendering.");e.ascent?l=e.ascent:e.descent&&(l=1+e.descent)}this.#hs.set(t,l);return l}}class XfaText{static textContent(t){const e=[],i={items:e,styles:Object.create(null)};!function walk(t){if(!t)return;let i=null;const s=t.name;if("#text"===s)i=t.value;else{if(!XfaText.shouldBuildText(s))return;t?.attributes?.textContent?i=t.attributes.textContent:t.value&&(i=t.value)}null!==i&&e.push({str:i});if(t.children)for(const e of t.children)walk(e)}(t);return i}static shouldBuildText(t){return!("textarea"===t||"input"===t||"option"===t||"select"===t)}}function getDocument(e={}){"string"==typeof e||e instanceof URL?e={url:e}:(e instanceof ArrayBuffer||ArrayBuffer.isView(e))&&(e={data:e});const i=new PDFDocumentLoadingTask,{docId:s}=i,n=e.url?function getUrlProp(e){if(e instanceof URL)return e.href;if("string"==typeof e){if(t)return e;const i=URL.parse(e,window.location);if(i)return i.href}throw new Error("Invalid PDF url data: either string or URL-object is expected in the url property.")}(e.url):null,a=e.data?function getDataProp(e){if(t&&"undefined"!=typeof Buffer&&e instanceof Buffer)throw new Error("Please provide binary data as `Uint8Array`, rather than `Buffer`.");if(e instanceof Uint8Array&&e.byteLength===e.buffer.byteLength)return e;if("string"==typeof e)return stringToBytes(e);if(e instanceof ArrayBuffer||ArrayBuffer.isView(e)||"object"==typeof e&&!isNaN(e?.length))return new Uint8Array(e);throw new Error("Invalid PDF binary data: either TypedArray, string, or array-like object is expected in the data property.")}(e.data):null,r=e.httpHeaders||null,o=!0===e.withCredentials,l=e.password??null,h=e.range instanceof PDFDataRangeTransport?e.range:null,d=Number.isInteger(e.rangeChunkSize)&&e.rangeChunkSize>0?e.rangeChunkSize:65536;let c=e.worker instanceof PDFWorker?e.worker:null;const u=e.verbosity,p="string"!=typeof e.docBaseUrl||isDataScheme(e.docBaseUrl)?null:e.docBaseUrl,g=getFactoryUrlProp(e.cMapUrl),f=!1!==e.cMapPacked,m=e.CMapReaderFactory||(t?NodeCMapReaderFactory:DOMCMapReaderFactory),b=getFactoryUrlProp(e.iccUrl),A=getFactoryUrlProp(e.standardFontDataUrl),w=e.StandardFontDataFactory||(t?NodeStandardFontDataFactory:DOMStandardFontDataFactory),y=getFactoryUrlProp(e.wasmUrl),v=e.WasmFactory||(t?NodeWasmFactory:DOMWasmFactory),_=!0!==e.stopAtErrors,x=Number.isInteger(e.maxImageSize)&&e.maxImageSize>-1?e.maxImageSize:-1,E=!1!==e.isEvalSupported,S="boolean"==typeof e.isOffscreenCanvasSupported?e.isOffscreenCanvasSupported:!t,C="boolean"==typeof e.isImageDecoderSupported?e.isImageDecoderSupported:!t&&(util_FeatureTest.platform.isFirefox||!globalThis.chrome),T=Number.isInteger(e.canvasMaxAreaInBytes)?e.canvasMaxAreaInBytes:-1,M="boolean"==typeof e.disableFontFace?e.disableFontFace:t,D=!0===e.fontExtraProperties,P=!0===e.enableXfa,k=e.ownerDocument||globalThis.document,I=!0===e.disableRange,R=!0===e.disableStream,F=!0===e.disableAutoFetch,L=!0===e.pdfBug,O=e.CanvasFactory||(t?NodeCanvasFactory:DOMCanvasFactory),N=e.FilterFactory||(t?NodeFilterFactory:DOMFilterFactory),B=!0===e.enableHWA,H=!1!==e.useWasm,U=h?h.length:e.length??NaN,z="boolean"==typeof e.useSystemFonts?e.useSystemFonts:!t&&!M,G="boolean"==typeof e.useWorkerFetch?e.useWorkerFetch:!!(m===DOMCMapReaderFactory&&w===DOMStandardFontDataFactory&&v===DOMWasmFactory&&g&&A&&y&&isValidFetchUrl(g,document.baseURI)&&isValidFetchUrl(A,document.baseURI)&&isValidFetchUrl(y,document.baseURI));setVerbosityLevel(u);const $={canvasFactory:new O({ownerDocument:k,enableHWA:B}),filterFactory:new N({docId:s,ownerDocument:k}),cMapReaderFactory:G?null:new m({baseUrl:g,isCompressed:f}),standardFontDataFactory:G?null:new w({baseUrl:A}),wasmFactory:G?null:new v({baseUrl:y})};if(!c){const t={verbosity:u,port:GlobalWorkerOptions.workerPort};c=t.port?PDFWorker.fromPort(t):new PDFWorker(t);i._worker=c}const W={docId:s,apiVersion:"5.2.133",data:a,password:l,disableAutoFetch:F,rangeChunkSize:d,length:U,docBaseUrl:p,enableXfa:P,evaluatorOptions:{maxImageSize:x,disableFontFace:M,ignoreErrors:_,isEvalSupported:E,isOffscreenCanvasSupported:S,isImageDecoderSupported:C,canvasMaxAreaInBytes:T,fontExtraProperties:D,useSystemFonts:z,useWasm:H,useWorkerFetch:G,cMapUrl:g,iccUrl:b,standardFontDataUrl:A,wasmUrl:y}},V={ownerDocument:k,pdfBug:L,styleElement:null,loadingParams:{disableAutoFetch:F,enableXfa:P}};c.promise.then((function(){if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const e=c.messageHandler.sendWithPromise("GetDocRequest",W,a?[a.buffer]:null);let l;if(h)l=new PDFDataTransportStream(h,{disableRange:I,disableStream:R});else if(!a){if(!n)throw new Error("getDocument - no `url` parameter provided.");let e;if(t)if(isValidFetchUrl(n)){if("undefined"==typeof fetch||"undefined"==typeof Response||!("body"in Response.prototype))throw new Error("getDocument - the Fetch API was disabled in Node.js, see `--no-experimental-fetch`.");e=PDFFetchStream}else e=PDFNodeStream;else e=isValidFetchUrl(n)?PDFFetchStream:PDFNetworkStream;l=new e({url:n,length:U,httpHeaders:r,withCredentials:o,rangeChunkSize:d,disableRange:I,disableStream:R})}return e.then((t=>{if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const e=new MessageHandler(s,t,c.port),n=new WorkerTransport(e,i,l,V,$);i._transport=n;e.send("Ready",null)}))})).catch(i._capability.reject);return i}function getFactoryUrlProp(t){if("string"!=typeof t)return null;if(t.endsWith("/"))return t;throw new Error(`Invalid factory url: "${t}" must include trailing slash.`)}const isRefProxy=t=>"object"==typeof t&&Number.isInteger(t?.num)&&t.num>=0&&Number.isInteger(t?.gen)&&t.gen>=0,At=function _isValidExplicitDest(t,e,i){if(!Array.isArray(i)||i.length<2)return!1;const[s,n,...a]=i;if(!t(s)&&!Number.isInteger(s))return!1;if(!e(n))return!1;const r=a.length;let o=!0;switch(n.name){case"XYZ":if(r<2||r>3)return!1;break;case"Fit":case"FitB":return 0===r;case"FitH":case"FitBH":case"FitV":case"FitBV":if(r>1)return!1;break;case"FitR":if(4!==r)return!1;o=!1;break;default:return!1}for(const t of a)if(!("number"==typeof t||o&&null===t))return!1;return!0}.bind(null,isRefProxy,(t=>"object"==typeof t&&"string"==typeof t?.name));class PDFDocumentLoadingTask{static#mi=0;_capability=Promise.withResolvers();_transport=null;_worker=null;docId="d"+PDFDocumentLoadingTask.#mi++;destroyed=!1;onPassword=null;onProgress=null;get promise(){return this._capability.promise}async destroy(){this.destroyed=!0;try{this._worker?.port&&(this._worker._pendingDestroy=!0);await(this._transport?.destroy())}catch(t){this._worker?.port&&delete this._worker._pendingDestroy;throw t}this._transport=null;this._worker?.destroy();this._worker=null}async getData(){return this._transport.getData()}}class PDFDataRangeTransport{constructor(t,e,i=!1,s=null){this.length=t;this.initialData=e;this.progressiveDone=i;this.contentDispositionFilename=s;this._rangeListeners=[];this._progressListeners=[];this._progressiveReadListeners=[];this._progressiveDoneListeners=[];this._readyCapability=Promise.withResolvers()}addRangeListener(t){this._rangeListeners.push(t)}addProgressListener(t){this._progressListeners.push(t)}addProgressiveReadListener(t){this._progressiveReadListeners.push(t)}addProgressiveDoneListener(t){this._progressiveDoneListeners.push(t)}onDataRange(t,e){for(const i of this._rangeListeners)i(t,e)}onDataProgress(t,e){this._readyCapability.promise.then((()=>{for(const i of this._progressListeners)i(t,e)}))}onDataProgressiveRead(t){this._readyCapability.promise.then((()=>{for(const e of this._progressiveReadListeners)e(t)}))}onDataProgressiveDone(){this._readyCapability.promise.then((()=>{for(const t of this._progressiveDoneListeners)t()}))}transportReady(){this._readyCapability.resolve()}requestDataRange(t,e){unreachable("Abstract method PDFDataRangeTransport.requestDataRange")}abort(){}}class PDFDocumentProxy{constructor(t,e){this._pdfInfo=t;this._transport=e}get annotationStorage(){return this._transport.annotationStorage}get canvasFactory(){return this._transport.canvasFactory}get filterFactory(){return this._transport.filterFactory}get numPages(){return this._pdfInfo.numPages}get fingerprints(){return this._pdfInfo.fingerprints}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}get allXfaHtml(){return this._transport._htmlForXfa}getPage(t){return this._transport.getPage(t)}getPageIndex(t){return this._transport.getPageIndex(t)}getDestinations(){return this._transport.getDestinations()}getDestination(t){return this._transport.getDestination(t)}getPageLabels(){return this._transport.getPageLabels()}getPageLayout(){return this._transport.getPageLayout()}getPageMode(){return this._transport.getPageMode()}getViewerPreferences(){return this._transport.getViewerPreferences()}getOpenAction(){return this._transport.getOpenAction()}getAttachments(){return this._transport.getAttachments()}getJSActions(){return this._transport.getDocJSActions()}getOutline(){return this._transport.getOutline()}getOptionalContentConfig({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getOptionalContentConfig(e)}getPermissions(){return this._transport.getPermissions()}getMetadata(){return this._transport.getMetadata()}getMarkInfo(){return this._transport.getMarkInfo()}getData(){return this._transport.getData()}saveDocument(){return this._transport.saveDocument()}getDownloadInfo(){return this._transport.downloadInfoCapability.promise}cleanup(t=!1){return this._transport.startCleanup(t||this.isPureXfa)}destroy(){return this.loadingTask.destroy()}cachedPageNumber(t){return this._transport.cachedPageNumber(t)}get loadingParams(){return this._transport.loadingParams}get loadingTask(){return this._transport.loadingTask}getFieldObjects(){return this._transport.getFieldObjects()}hasJSActions(){return this._transport.hasJSActions()}getCalculationOrderIds(){return this._transport.getCalculationOrderIds()}}class PDFPageProxy{#vs=!1;constructor(t,e,i,s=!1){this._pageIndex=t;this._pageInfo=e;this._transport=i;this._stats=s?new StatTimer:null;this._pdfBug=s;this.commonObjs=i.commonObjs;this.objs=new PDFObjects;this._intentStates=new Map;this.destroyed=!1}get pageNumber(){return this._pageIndex+1}get rotate(){return this._pageInfo.rotate}get ref(){return this._pageInfo.ref}get userUnit(){return this._pageInfo.userUnit}get view(){return this._pageInfo.view}getViewport({scale:t,rotation:e=this.rotate,offsetX:i=0,offsetY:s=0,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.view,userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}getAnnotations({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getAnnotations(this._pageIndex,e)}getJSActions(){return this._transport.getPageJSActions(this._pageIndex)}get filterFactory(){return this._transport.filterFactory}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}async getXfa(){return this._transport._htmlForXfa?.children[this._pageIndex]||null}render({canvasContext:t,viewport:e,intent:i="display",annotationMode:s=c.ENABLE,transform:n=null,background:r=null,optionalContentConfigPromise:o=null,annotationCanvasMap:l=null,pageColors:h=null,printAnnotationStorage:d=null,isEditing:u=!1}){this._stats?.time("Overall");const p=this._transport.getRenderingIntent(i,s,d,u),{renderingIntent:g,cacheKey:f}=p;this.#vs=!1;o||=this._transport.getOptionalContentConfig(g);let m=this._intentStates.get(f);if(!m){m=Object.create(null);this._intentStates.set(f,m)}if(m.streamReaderCancelTimeout){clearTimeout(m.streamReaderCancelTimeout);m.streamReaderCancelTimeout=null}const b=!!(g&a);if(!m.displayReadyCapability){m.displayReadyCapability=Promise.withResolvers();m.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(p)}const complete=t=>{m.renderTasks.delete(A);b&&(this.#vs=!0);this.#_s();if(t){A.capability.reject(t);this._abortOperatorList({intentState:m,reason:t instanceof Error?t:new Error(t)})}else A.capability.resolve();if(this._stats){this._stats.timeEnd("Rendering");this._stats.timeEnd("Overall");globalThis.Stats?.enabled&&globalThis.Stats.add(this.pageNumber,this._stats)}},A=new InternalRenderTask({callback:complete,params:{canvasContext:t,viewport:e,transform:n,background:r},objs:this.objs,commonObjs:this.commonObjs,annotationCanvasMap:l,operatorList:m.operatorList,pageIndex:this._pageIndex,canvasFactory:this._transport.canvasFactory,filterFactory:this._transport.filterFactory,useRequestAnimationFrame:!b,pdfBug:this._pdfBug,pageColors:h});(m.renderTasks||=new Set).add(A);const w=A.task;Promise.all([m.displayReadyCapability.promise,o]).then((([t,e])=>{if(this.destroyed)complete();else{this._stats?.time("Rendering");if(!(e.renderingIntent&g))throw new Error("Must use the same `intent`-argument when calling the `PDFPageProxy.render` and `PDFDocumentProxy.getOptionalContentConfig` methods.");A.initializeGraphics({transparency:t,optionalContentConfig:e});A.operatorListChanged()}})).catch(complete);return w}getOperatorList({intent:t="display",annotationMode:e=c.ENABLE,printAnnotationStorage:i=null,isEditing:s=!1}={}){const n=this._transport.getRenderingIntent(t,e,i,s,!0);let a,r=this._intentStates.get(n.cacheKey);if(!r){r=Object.create(null);this._intentStates.set(n.cacheKey,r)}if(!r.opListReadCapability){a=Object.create(null);a.operatorListChanged=function operatorListChanged(){if(r.operatorList.lastChunk){r.opListReadCapability.resolve(r.operatorList);r.renderTasks.delete(a)}};r.opListReadCapability=Promise.withResolvers();(r.renderTasks||=new Set).add(a);r.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(n)}return r.opListReadCapability.promise}streamTextContent({includeMarkedContent:t=!1,disableNormalization:e=!1}={}){return this._transport.messageHandler.sendWithStream("GetTextContent",{pageIndex:this._pageIndex,includeMarkedContent:!0===t,disableNormalization:!0===e},{highWaterMark:100,size:t=>t.items.length})}getTextContent(t={}){if(this._transport._htmlForXfa)return this.getXfa().then((t=>XfaText.textContent(t)));const e=this.streamTextContent(t);return new Promise((function(t,i){const s=e.getReader(),n={items:[],styles:Object.create(null),lang:null};!function pump(){s.read().then((function({value:e,done:i}){if(i)t(n);else{n.lang??=e.lang;Object.assign(n.styles,e.styles);n.items.push(...e.items);pump()}}),i)}()}))}getStructTree(){return this._transport.getStructTree(this._pageIndex)}_destroy(){this.destroyed=!0;const t=[];for(const e of this._intentStates.values()){this._abortOperatorList({intentState:e,reason:new Error("Page was destroyed."),force:!0});if(!e.opListReadCapability)for(const i of e.renderTasks){t.push(i.completed);i.cancel()}}this.objs.clear();this.#vs=!1;return Promise.all(t)}cleanup(t=!1){this.#vs=!0;const e=this.#_s();t&&e&&(this._stats&&=new StatTimer);return e}#_s(){if(!this.#vs||this.destroyed)return!1;for(const{renderTasks:t,operatorList:e}of this._intentStates.values())if(t.size>0||!e.lastChunk)return!1;this._intentStates.clear();this.objs.clear();this.#vs=!1;return!0}_startRenderPage(t,e){const i=this._intentStates.get(e);if(i){this._stats?.timeEnd("Page Request");i.displayReadyCapability?.resolve(t)}}_renderPageChunk(t,e){for(let i=0,s=t.length;i{r.read().then((({value:t,done:e})=>{if(e)o.streamReader=null;else if(!this._transport.destroyed){this._renderPageChunk(t,o);pump()}}),(t=>{o.streamReader=null;if(!this._transport.destroyed){if(o.operatorList){o.operatorList.lastChunk=!0;for(const t of o.renderTasks)t.operatorListChanged();this.#_s()}if(o.displayReadyCapability)o.displayReadyCapability.reject(t);else{if(!o.opListReadCapability)throw t;o.opListReadCapability.reject(t)}}}))};pump()}_abortOperatorList({intentState:t,reason:e,force:i=!1}){if(t.streamReader){if(t.streamReaderCancelTimeout){clearTimeout(t.streamReaderCancelTimeout);t.streamReaderCancelTimeout=null}if(!i){if(t.renderTasks.size>0)return;if(e instanceof RenderingCancelledException){let i=100;e.extraDelay>0&&e.extraDelay<1e3&&(i+=e.extraDelay);t.streamReaderCancelTimeout=setTimeout((()=>{t.streamReaderCancelTimeout=null;this._abortOperatorList({intentState:t,reason:e,force:!0})}),i);return}}t.streamReader.cancel(new AbortException(e.message)).catch((()=>{}));t.streamReader=null;if(!this._transport.destroyed){for(const[e,i]of this._intentStates)if(i===t){this._intentStates.delete(e);break}this.cleanup()}}}get stats(){return this._stats}}class LoopbackPort{#xs=new Map;#Es=Promise.resolve();postMessage(t,e){const i={data:structuredClone(t,e?{transfer:e}:null)};this.#Es.then((()=>{for(const[t]of this.#xs)t.call(this,i)}))}addEventListener(t,e,i=null){let s=null;if(i?.signal instanceof AbortSignal){const{signal:n}=i;if(n.aborted){warn("LoopbackPort - cannot use an `aborted` signal.");return}const onAbort=()=>this.removeEventListener(t,e);s=()=>n.removeEventListener("abort",onAbort);n.addEventListener("abort",onAbort)}this.#xs.set(e,s)}removeEventListener(t,e){const i=this.#xs.get(e);i?.();this.#xs.delete(e)}terminate(){for(const[,t]of this.#xs)t?.();this.#xs.clear()}}class PDFWorker{static#Ss=0;static#Cs=!1;static#Ts;static{if(t){this.#Cs=!0;GlobalWorkerOptions.workerSrc||="./pdf.worker.mjs"}this._isSameOrigin=(t,e)=>{const i=URL.parse(t);if(!i?.origin||"null"===i.origin)return!1;const s=new URL(e,i);return i.origin===s.origin};this._createCDNWrapper=t=>{const e=`await import("${t}");`;return URL.createObjectURL(new Blob([e],{type:"text/javascript"}))}}constructor({name:t=null,port:e=null,verbosity:i=getVerbosityLevel()}={}){this.name=t;this.destroyed=!1;this.verbosity=i;this._readyCapability=Promise.withResolvers();this._port=null;this._webWorker=null;this._messageHandler=null;if(e){if(PDFWorker.#Ts?.has(e))throw new Error("Cannot use more than one PDFWorker per port.");(PDFWorker.#Ts||=new WeakMap).set(e,this);this._initializeFromPort(e)}else this._initialize()}get promise(){return this._readyCapability.promise}#Ms(){this._readyCapability.resolve();this._messageHandler.send("configure",{verbosity:this.verbosity})}get port(){return this._port}get messageHandler(){return this._messageHandler}_initializeFromPort(t){this._port=t;this._messageHandler=new MessageHandler("main","worker",t);this._messageHandler.on("ready",(function(){}));this.#Ms()}_initialize(){if(PDFWorker.#Cs||PDFWorker.#Ds){this._setupFakeWorker();return}let{workerSrc:t}=PDFWorker;try{PDFWorker._isSameOrigin(window.location,t)||(t=PDFWorker._createCDNWrapper(new URL(t,window.location).href));const e=new Worker(t,{type:"module"}),i=new MessageHandler("main","worker",e),terminateEarly=()=>{s.abort();i.destroy();e.terminate();this.destroyed?this._readyCapability.reject(new Error("Worker was destroyed")):this._setupFakeWorker()},s=new AbortController;e.addEventListener("error",(()=>{this._webWorker||terminateEarly()}),{signal:s.signal});i.on("test",(t=>{s.abort();if(!this.destroyed&&t){this._messageHandler=i;this._port=e;this._webWorker=e;this.#Ms()}else terminateEarly()}));i.on("ready",(t=>{s.abort();if(this.destroyed)terminateEarly();else try{sendTest()}catch{this._setupFakeWorker()}}));const sendTest=()=>{const t=new Uint8Array;i.send("test",t,[t.buffer])};sendTest();return}catch{info("The worker has been disabled.")}this._setupFakeWorker()}_setupFakeWorker(){if(!PDFWorker.#Cs){warn("Setting up fake worker.");PDFWorker.#Cs=!0}PDFWorker._setupFakeWorkerGlobal.then((t=>{if(this.destroyed){this._readyCapability.reject(new Error("Worker was destroyed"));return}const e=new LoopbackPort;this._port=e;const i="fake"+PDFWorker.#Ss++,s=new MessageHandler(i+"_worker",i,e);t.setup(s,e);this._messageHandler=new MessageHandler(i,i+"_worker",e);this.#Ms()})).catch((t=>{this._readyCapability.reject(new Error(`Setting up fake worker failed: "${t.message}".`))}))}destroy(){this.destroyed=!0;this._webWorker?.terminate();this._webWorker=null;PDFWorker.#Ts?.delete(this._port);this._port=null;this._messageHandler?.destroy();this._messageHandler=null}static fromPort(t){if(!t?.port)throw new Error("PDFWorker.fromPort - invalid method signature.");const e=this.#Ts?.get(t.port);if(e){if(e._pendingDestroy)throw new Error("PDFWorker.fromPort - the worker is being destroyed.\nPlease remember to await `PDFDocumentLoadingTask.destroy()`-calls.");return e}return new PDFWorker(t)}static get workerSrc(){if(GlobalWorkerOptions.workerSrc)return GlobalWorkerOptions.workerSrc;throw new Error('No "GlobalWorkerOptions.workerSrc" specified.')}static get#Ds(){try{return globalThis.pdfjsWorker?.WorkerMessageHandler||null}catch{return null}}static get _setupFakeWorkerGlobal(){return shadow(this,"_setupFakeWorkerGlobal",(async()=>{if(this.#Ds)return this.#Ds;return(await import(this.workerSrc)).WorkerMessageHandler})())}}class WorkerTransport{#Ps=new Map;#ks=new Map;#Is=new Map;#Rs=new Map;#Fs=null;constructor(t,e,i,s,n){this.messageHandler=t;this.loadingTask=e;this.commonObjs=new PDFObjects;this.fontLoader=new FontLoader({ownerDocument:s.ownerDocument,styleElement:s.styleElement});this.loadingParams=s.loadingParams;this._params=s;this.canvasFactory=n.canvasFactory;this.filterFactory=n.filterFactory;this.cMapReaderFactory=n.cMapReaderFactory;this.standardFontDataFactory=n.standardFontDataFactory;this.wasmFactory=n.wasmFactory;this.destroyed=!1;this.destroyCapability=null;this._networkStream=i;this._fullReader=null;this._lastProgress=null;this.downloadInfoCapability=Promise.withResolvers();this.setupMessageHandler()}#Ls(t,e=null){const i=this.#Ps.get(t);if(i)return i;const s=this.messageHandler.sendWithPromise(t,e);this.#Ps.set(t,s);return s}get annotationStorage(){return shadow(this,"annotationStorage",new AnnotationStorage)}getRenderingIntent(t,e=c.ENABLE,i=null,u=!1,p=!1){let g=n,f=j;switch(t){case"any":g=s;break;case"display":break;case"print":g=a;break;default:warn(`getRenderingIntent - invalid intent: ${t}`)}const m=g&a&&i instanceof PrintAnnotationStorage?i:this.annotationStorage;switch(e){case c.DISABLE:g+=l;break;case c.ENABLE:break;case c.ENABLE_FORMS:g+=r;break;case c.ENABLE_STORAGE:g+=o;f=m.serializable;break;default:warn(`getRenderingIntent - invalid annotationMode: ${e}`)}u&&(g+=h);p&&(g+=d);const{ids:b,hash:A}=m.modifiedIds;return{renderingIntent:g,cacheKey:[g,f.hash,A].join("_"),annotationStorageSerializable:f,modifiedIds:b}}destroy(){if(this.destroyCapability)return this.destroyCapability.promise;this.destroyed=!0;this.destroyCapability=Promise.withResolvers();this.#Fs?.reject(new Error("Worker was destroyed during onPassword callback"));const t=[];for(const e of this.#ks.values())t.push(e._destroy());this.#ks.clear();this.#Is.clear();this.#Rs.clear();this.hasOwnProperty("annotationStorage")&&this.annotationStorage.resetModified();const e=this.messageHandler.sendWithPromise("Terminate",null);t.push(e);Promise.all(t).then((()=>{this.commonObjs.clear();this.fontLoader.clear();this.#Ps.clear();this.filterFactory.destroy();TextLayer.cleanup();this._networkStream?.cancelAllRequests(new AbortException("Worker was terminated."));this.messageHandler?.destroy();this.messageHandler=null;this.destroyCapability.resolve()}),this.destroyCapability.reject);return this.destroyCapability.promise}setupMessageHandler(){const{messageHandler:t,loadingTask:e}=this;t.on("GetReader",((t,e)=>{assert(this._networkStream,"GetReader - no `IPDFStream` instance available.");this._fullReader=this._networkStream.getFullReader();this._fullReader.onProgress=t=>{this._lastProgress={loaded:t.loaded,total:t.total}};e.onPull=()=>{this._fullReader.read().then((function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{this._fullReader.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}));t.on("ReaderHeadersReady",(async t=>{await this._fullReader.headersReady;const{isStreamingSupported:i,isRangeSupported:s,contentLength:n}=this._fullReader;if(!i||!s){this._lastProgress&&e.onProgress?.(this._lastProgress);this._fullReader.onProgress=t=>{e.onProgress?.({loaded:t.loaded,total:t.total})}}return{isStreamingSupported:i,isRangeSupported:s,contentLength:n}}));t.on("GetRangeReader",((t,e)=>{assert(this._networkStream,"GetRangeReader - no `IPDFStream` instance available.");const i=this._networkStream.getRangeReader(t.begin,t.end);if(i){e.onPull=()=>{i.read().then((function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetRangeReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{i.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}else e.close()}));t.on("GetDoc",(({pdfInfo:t})=>{this._numPages=t.numPages;this._htmlForXfa=t.htmlForXfa;delete t.htmlForXfa;e._capability.resolve(new PDFDocumentProxy(t,this))}));t.on("DocException",(t=>{e._capability.reject(wrapReason(t))}));t.on("PasswordRequest",(t=>{this.#Fs=Promise.withResolvers();try{if(!e.onPassword)throw wrapReason(t);const updatePassword=t=>{t instanceof Error?this.#Fs.reject(t):this.#Fs.resolve({password:t})};e.onPassword(updatePassword,t.code)}catch(t){this.#Fs.reject(t)}return this.#Fs.promise}));t.on("DataLoaded",(t=>{e.onProgress?.({loaded:t.length,total:t.length});this.downloadInfoCapability.resolve(t)}));t.on("StartRenderPage",(t=>{if(this.destroyed)return;this.#ks.get(t.pageIndex)._startRenderPage(t.transparency,t.cacheKey)}));t.on("commonobj",(([e,i,s])=>{if(this.destroyed)return null;if(this.commonObjs.has(e))return null;switch(i){case"Font":if("error"in s){const t=s.error;warn(`Error during font loading: ${t}`);this.commonObjs.resolve(e,t);break}const n=this._params.pdfBug&&globalThis.FontInspector?.enabled?(t,e)=>globalThis.FontInspector.fontAdded(t,e):null,a=new FontFaceObject(s,n);this.fontLoader.bind(a).catch((()=>t.sendWithPromise("FontFallback",{id:e}))).finally((()=>{!a.fontExtraProperties&&a.data&&(a.data=null);this.commonObjs.resolve(e,a)}));break;case"CopyLocalImage":const{imageRef:r}=s;assert(r,"The imageRef must be defined.");for(const t of this.#ks.values())for(const[,i]of t.objs)if(i?.ref===r){if(!i.dataLen)return null;this.commonObjs.resolve(e,structuredClone(i));return i.dataLen}break;case"FontPath":case"Image":case"Pattern":this.commonObjs.resolve(e,s);break;default:throw new Error(`Got unknown common object type ${i}`)}return null}));t.on("obj",(([t,e,i,s])=>{if(this.destroyed)return;const n=this.#ks.get(e);if(!n.objs.has(t))if(0!==n._intentStates.size)switch(i){case"Image":case"Pattern":n.objs.resolve(t,s);break;default:throw new Error(`Got unknown object type ${i}`)}else s?.bitmap?.close()}));t.on("DocProgress",(t=>{this.destroyed||e.onProgress?.({loaded:t.loaded,total:t.total})}));t.on("FetchBinaryData",(async t=>{if(this.destroyed)throw new Error("Worker was destroyed.");const e=this[t.type];if(!e)throw new Error(`${t.type} not initialized, see the \`useWorkerFetch\` parameter.`);return e.fetch(t)}))}getData(){return this.messageHandler.sendWithPromise("GetData",null)}saveDocument(){this.annotationStorage.size<=0&&warn("saveDocument called while `annotationStorage` is empty, please use the getData-method instead.");const{map:t,transfer:e}=this.annotationStorage.serializable;return this.messageHandler.sendWithPromise("SaveDocument",{isPureXfa:!!this._htmlForXfa,numPages:this._numPages,annotationStorage:t,filename:this._fullReader?.filename??null},e).finally((()=>{this.annotationStorage.resetModified()}))}getPage(t){if(!Number.isInteger(t)||t<=0||t>this._numPages)return Promise.reject(new Error("Invalid page request."));const e=t-1,i=this.#Is.get(e);if(i)return i;const s=this.messageHandler.sendWithPromise("GetPage",{pageIndex:e}).then((i=>{if(this.destroyed)throw new Error("Transport destroyed");i.refStr&&this.#Rs.set(i.refStr,t);const s=new PDFPageProxy(e,i,this,this._params.pdfBug);this.#ks.set(e,s);return s}));this.#Is.set(e,s);return s}getPageIndex(t){return isRefProxy(t)?this.messageHandler.sendWithPromise("GetPageIndex",{num:t.num,gen:t.gen}):Promise.reject(new Error("Invalid pageIndex request."))}getAnnotations(t,e){return this.messageHandler.sendWithPromise("GetAnnotations",{pageIndex:t,intent:e})}getFieldObjects(){return this.#Ls("GetFieldObjects")}hasJSActions(){return this.#Ls("HasJSActions")}getCalculationOrderIds(){return this.messageHandler.sendWithPromise("GetCalculationOrderIds",null)}getDestinations(){return this.messageHandler.sendWithPromise("GetDestinations",null)}getDestination(t){return"string"!=typeof t?Promise.reject(new Error("Invalid destination request.")):this.messageHandler.sendWithPromise("GetDestination",{id:t})}getPageLabels(){return this.messageHandler.sendWithPromise("GetPageLabels",null)}getPageLayout(){return this.messageHandler.sendWithPromise("GetPageLayout",null)}getPageMode(){return this.messageHandler.sendWithPromise("GetPageMode",null)}getViewerPreferences(){return this.messageHandler.sendWithPromise("GetViewerPreferences",null)}getOpenAction(){return this.messageHandler.sendWithPromise("GetOpenAction",null)}getAttachments(){return this.messageHandler.sendWithPromise("GetAttachments",null)}getDocJSActions(){return this.#Ls("GetDocJSActions")}getPageJSActions(t){return this.messageHandler.sendWithPromise("GetPageJSActions",{pageIndex:t})}getStructTree(t){return this.messageHandler.sendWithPromise("GetStructTree",{pageIndex:t})}getOutline(){return this.messageHandler.sendWithPromise("GetOutline",null)}getOptionalContentConfig(t){return this.#Ls("GetOptionalContentConfig").then((e=>new OptionalContentConfig(e,t)))}getPermissions(){return this.messageHandler.sendWithPromise("GetPermissions",null)}getMetadata(){const t="GetMetadata",e=this.#Ps.get(t);if(e)return e;const i=this.messageHandler.sendWithPromise(t,null).then((t=>({info:t[0],metadata:t[1]?new Metadata(t[1]):null,contentDispositionFilename:this._fullReader?.filename??null,contentLength:this._fullReader?.contentLength??null})));this.#Ps.set(t,i);return i}getMarkInfo(){return this.messageHandler.sendWithPromise("GetMarkInfo",null)}async startCleanup(t=!1){if(!this.destroyed){await this.messageHandler.sendWithPromise("Cleanup",null);for(const t of this.#ks.values()){if(!t.cleanup())throw new Error(`startCleanup: Page ${t.pageNumber} is currently rendering.`)}this.commonObjs.clear();t||this.fontLoader.clear();this.#Ps.clear();this.filterFactory.destroy(!0);TextLayer.cleanup()}}cachedPageNumber(t){if(!isRefProxy(t))return null;const e=0===t.gen?`${t.num}R`:`${t.num}R${t.gen}`;return this.#Rs.get(e)??null}}const wt=Symbol("INITIAL_DATA");class PDFObjects{#Os=Object.create(null);#Ns(t){return this.#Os[t]||={...Promise.withResolvers(),data:wt}}get(t,e=null){if(e){const i=this.#Ns(t);i.promise.then((()=>e(i.data)));return null}const i=this.#Os[t];if(!i||i.data===wt)throw new Error(`Requesting object that isn't resolved yet ${t}.`);return i.data}has(t){const e=this.#Os[t];return!!e&&e.data!==wt}delete(t){const e=this.#Os[t];if(!e||e.data===wt)return!1;delete this.#Os[t];return!0}resolve(t,e=null){const i=this.#Ns(t);i.data=e;i.resolve()}clear(){for(const t in this.#Os){const{data:e}=this.#Os[t];e?.bitmap?.close()}this.#Os=Object.create(null)}*[Symbol.iterator](){for(const t in this.#Os){const{data:e}=this.#Os[t];e!==wt&&(yield[t,e])}}}class RenderTask{#Bs=null;onContinue=null;onError=null;constructor(t){this.#Bs=t}get promise(){return this.#Bs.capability.promise}cancel(t=0){this.#Bs.cancel(null,t)}get separateAnnots(){const{separateAnnots:t}=this.#Bs.operatorList;if(!t)return!1;const{annotationCanvasMap:e}=this.#Bs;return t.form||t.canvas&&e?.size>0}}class InternalRenderTask{#Hs=null;static#Us=new WeakSet;constructor({callback:t,params:e,objs:i,commonObjs:s,annotationCanvasMap:n,operatorList:a,pageIndex:r,canvasFactory:o,filterFactory:l,useRequestAnimationFrame:h=!1,pdfBug:d=!1,pageColors:c=null}){this.callback=t;this.params=e;this.objs=i;this.commonObjs=s;this.annotationCanvasMap=n;this.operatorListIdx=null;this.operatorList=a;this._pageIndex=r;this.canvasFactory=o;this.filterFactory=l;this._pdfBug=d;this.pageColors=c;this.running=!1;this.graphicsReadyCallback=null;this.graphicsReady=!1;this._useRequestAnimationFrame=!0===h&&"undefined"!=typeof window;this.cancelled=!1;this.capability=Promise.withResolvers();this.task=new RenderTask(this);this._cancelBound=this.cancel.bind(this);this._continueBound=this._continue.bind(this);this._scheduleNextBound=this._scheduleNext.bind(this);this._nextBound=this._next.bind(this);this._canvas=e.canvasContext.canvas}get completed(){return this.capability.promise.catch((function(){}))}initializeGraphics({transparency:t=!1,optionalContentConfig:e}){if(this.cancelled)return;if(this._canvas){if(InternalRenderTask.#Us.has(this._canvas))throw new Error("Cannot use the same canvas during multiple render() operations. Use different canvas or ensure previous operations were cancelled or completed.");InternalRenderTask.#Us.add(this._canvas)}if(this._pdfBug&&globalThis.StepperManager?.enabled){this.stepper=globalThis.StepperManager.create(this._pageIndex);this.stepper.init(this.operatorList);this.stepper.nextBreakPoint=this.stepper.getNextBreakPoint()}const{canvasContext:i,viewport:s,transform:n,background:a}=this.params;this.gfx=new CanvasGraphics(i,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:e},this.annotationCanvasMap,this.pageColors);this.gfx.beginDrawing({transform:n,viewport:s,transparency:t,background:a});this.operatorListIdx=0;this.graphicsReady=!0;this.graphicsReadyCallback?.()}cancel(t=null,e=0){this.running=!1;this.cancelled=!0;this.gfx?.endDrawing();if(this.#Hs){window.cancelAnimationFrame(this.#Hs);this.#Hs=null}InternalRenderTask.#Us.delete(this._canvas);t||=new RenderingCancelledException(`Rendering cancelled, page ${this._pageIndex+1}`,e);this.callback(t);this.task.onError?.(t)}operatorListChanged(){if(this.graphicsReady){this.stepper?.updateOperatorList(this.operatorList);this.running||this._continue()}else this.graphicsReadyCallback||=this._continueBound}_continue(){this.running=!0;this.cancelled||(this.task.onContinue?this.task.onContinue(this._scheduleNextBound):this._scheduleNext())}_scheduleNext(){this._useRequestAnimationFrame?this.#Hs=window.requestAnimationFrame((()=>{this.#Hs=null;this._nextBound().catch(this._cancelBound)})):Promise.resolve().then(this._nextBound).catch(this._cancelBound)}async _next(){if(!this.cancelled){this.operatorListIdx=this.gfx.executeOperatorList(this.operatorList,this.operatorListIdx,this._continueBound,this.stepper);if(this.operatorListIdx===this.operatorList.argsArray.length){this.running=!1;if(this.operatorList.lastChunk){this.gfx.endDrawing();InternalRenderTask.#Us.delete(this._canvas);this.callback()}}}}}const yt="5.2.133",vt="4f7761353";function makeColorComp(t){return Math.floor(255*Math.max(0,Math.min(1,t))).toString(16).padStart(2,"0")}function scaleAndClamp(t){return Math.max(0,Math.min(255,255*t))}class ColorConverters{static CMYK_G([t,e,i,s]){return["G",1-Math.min(1,.3*t+.59*i+.11*e+s)]}static G_CMYK([t]){return["CMYK",0,0,0,1-t]}static G_RGB([t]){return["RGB",t,t,t]}static G_rgb([t]){return[t=scaleAndClamp(t),t,t]}static G_HTML([t]){const e=makeColorComp(t);return`#${e}${e}${e}`}static RGB_G([t,e,i]){return["G",.3*t+.59*e+.11*i]}static RGB_rgb(t){return t.map(scaleAndClamp)}static RGB_HTML(t){return`#${t.map(makeColorComp).join("")}`}static T_HTML(){return"#00000000"}static T_rgb(){return[null]}static CMYK_RGB([t,e,i,s]){return["RGB",1-Math.min(1,t+s),1-Math.min(1,i+s),1-Math.min(1,e+s)]}static CMYK_rgb([t,e,i,s]){return[scaleAndClamp(1-Math.min(1,t+s)),scaleAndClamp(1-Math.min(1,i+s)),scaleAndClamp(1-Math.min(1,e+s))]}static CMYK_HTML(t){const e=this.CMYK_RGB(t).slice(1);return this.RGB_HTML(e)}static RGB_CMYK([t,e,i]){const s=1-t,n=1-e,a=1-i;return["CMYK",s,n,a,Math.min(s,n,a)]}}class BaseSVGFactory{create(t,e,i=!1){if(t<=0||e<=0)throw new Error("Invalid SVG dimensions");const s=this._createSVG("svg:svg");s.setAttribute("version","1.1");if(!i){s.setAttribute("width",`${t}px`);s.setAttribute("height",`${e}px`)}s.setAttribute("preserveAspectRatio","none");s.setAttribute("viewBox",`0 0 ${t} ${e}`);return s}createElement(t){if("string"!=typeof t)throw new Error("Invalid SVG element type");return this._createSVG(t)}_createSVG(t){unreachable("Abstract method `_createSVG` called.")}}class DOMSVGFactory extends BaseSVGFactory{_createSVG(t){return document.createElementNS(z,t)}}class XfaLayer{static setupStorage(t,e,i,s,n){const a=s.getValue(e,{value:null});switch(i.name){case"textarea":null!==a.value&&(t.textContent=a.value);if("print"===n)break;t.addEventListener("input",(t=>{s.setValue(e,{value:t.target.value})}));break;case"input":if("radio"===i.attributes.type||"checkbox"===i.attributes.type){a.value===i.attributes.xfaOn?t.setAttribute("checked",!0):a.value===i.attributes.xfaOff&&t.removeAttribute("checked");if("print"===n)break;t.addEventListener("change",(t=>{s.setValue(e,{value:t.target.checked?t.target.getAttribute("xfaOn"):t.target.getAttribute("xfaOff")})}))}else{null!==a.value&&t.setAttribute("value",a.value);if("print"===n)break;t.addEventListener("input",(t=>{s.setValue(e,{value:t.target.value})}))}break;case"select":if(null!==a.value){t.setAttribute("value",a.value);for(const t of i.children)t.attributes.value===a.value?t.attributes.selected=!0:t.attributes.hasOwnProperty("selected")&&delete t.attributes.selected}t.addEventListener("input",(t=>{const i=t.target.options,n=-1===i.selectedIndex?"":i[i.selectedIndex].value;s.setValue(e,{value:n})}))}}static setAttributes({html:t,element:e,storage:i=null,intent:s,linkService:n}){const{attributes:a}=e,r=t instanceof HTMLAnchorElement;"radio"===a.type&&(a.name=`${a.name}-${s}`);for(const[e,i]of Object.entries(a))if(null!=i)switch(e){case"class":i.length&&t.setAttribute(e,i.join(" "));break;case"dataId":break;case"id":t.setAttribute("data-element-id",i);break;case"style":Object.assign(t.style,i);break;case"textContent":t.textContent=i;break;default:(!r||"href"!==e&&"newWindow"!==e)&&t.setAttribute(e,i)}r&&n.addLinkAttributes(t,a.href,a.newWindow);i&&a.dataId&&this.setupStorage(t,a.dataId,e,i)}static render(t){const e=t.annotationStorage,i=t.linkService,s=t.xfaHtml,n=t.intent||"display",a=document.createElement(s.name);s.attributes&&this.setAttributes({html:a,element:s,intent:n,linkService:i});const r="richText"!==n,o=t.div;o.append(a);if(t.viewport){const e=`matrix(${t.viewport.transform.join(",")})`;o.style.transform=e}r&&o.setAttribute("class","xfaLayer xfaFont");const l=[];if(0===s.children.length){if(s.value){const t=document.createTextNode(s.value);a.append(t);r&&XfaText.shouldBuildText(s.name)&&l.push(t)}return{textDivs:l}}const h=[[s,-1,a]];for(;h.length>0;){const[t,s,a]=h.at(-1);if(s+1===t.children.length){h.pop();continue}const o=t.children[++h.at(-1)[1]];if(null===o)continue;const{name:d}=o;if("#text"===d){const t=document.createTextNode(o.value);l.push(t);a.append(t);continue}const c=o?.attributes?.xmlns?document.createElementNS(o.attributes.xmlns,d):document.createElement(d);a.append(c);o.attributes&&this.setAttributes({html:c,element:o,storage:e,intent:n,linkService:i});if(o.children?.length>0)h.push([o,-1,c]);else if(o.value){const t=document.createTextNode(o.value);r&&XfaText.shouldBuildText(d)&&l.push(t);c.append(t)}}for(const t of o.querySelectorAll(".xfaNonInteractive input, .xfaNonInteractive textarea"))t.setAttribute("readOnly",!0);return{textDivs:l}}static update(t){const e=`matrix(${t.viewport.transform.join(",")})`;t.div.style.transform=e;t.div.hidden=!1}}const _t=1e3,xt=new WeakSet;class AnnotationElementFactory{static create(t){switch(t.data.annotationType){case _.LINK:return new LinkAnnotationElement(t);case _.TEXT:return new TextAnnotationElement(t);case _.WIDGET:switch(t.data.fieldType){case"Tx":return new TextWidgetAnnotationElement(t);case"Btn":return t.data.radioButton?new RadioButtonWidgetAnnotationElement(t):t.data.checkBox?new CheckboxWidgetAnnotationElement(t):new PushButtonWidgetAnnotationElement(t);case"Ch":return new ChoiceWidgetAnnotationElement(t);case"Sig":return new SignatureWidgetAnnotationElement(t)}return new WidgetAnnotationElement(t);case _.POPUP:return new PopupAnnotationElement(t);case _.FREETEXT:return new FreeTextAnnotationElement(t);case _.LINE:return new LineAnnotationElement(t);case _.SQUARE:return new SquareAnnotationElement(t);case _.CIRCLE:return new CircleAnnotationElement(t);case _.POLYLINE:return new PolylineAnnotationElement(t);case _.CARET:return new CaretAnnotationElement(t);case _.INK:return new InkAnnotationElement(t);case _.POLYGON:return new PolygonAnnotationElement(t);case _.HIGHLIGHT:return new HighlightAnnotationElement(t);case _.UNDERLINE:return new UnderlineAnnotationElement(t);case _.SQUIGGLY:return new SquigglyAnnotationElement(t);case _.STRIKEOUT:return new StrikeOutAnnotationElement(t);case _.STAMP:return new StampAnnotationElement(t);case _.FILEATTACHMENT:return new FileAttachmentAnnotationElement(t);default:return new AnnotationElement(t)}}}class AnnotationElement{#zs=null;#Gs=!1;#$s=null;constructor(t,{isRenderable:e=!1,ignoreBorder:i=!1,createQuadrilaterals:s=!1}={}){this.isRenderable=e;this.data=t.data;this.layer=t.layer;this.linkService=t.linkService;this.downloadManager=t.downloadManager;this.imageResourcesPath=t.imageResourcesPath;this.renderForms=t.renderForms;this.svgFactory=t.svgFactory;this.annotationStorage=t.annotationStorage;this.enableScripting=t.enableScripting;this.hasJSActions=t.hasJSActions;this._fieldObjects=t.fieldObjects;this.parent=t.parent;e&&(this.container=this._createContainer(i));s&&this._createQuadrilaterals()}static _hasPopupData({titleObj:t,contentsObj:e,richText:i}){return!!(t?.str||e?.str||i?.str)}get _isEditable(){return this.data.isEditable}get hasPopupData(){return AnnotationElement._hasPopupData(this.data)}updateEdited(t){if(!this.container)return;this.#zs||={rect:this.data.rect.slice(0)};const{rect:e}=t;e&&this.#Ws(e);this.#$s?.popup.updateEdited(t)}resetEdited(){if(this.#zs){this.#Ws(this.#zs.rect);this.#$s?.popup.resetEdited();this.#zs=null}}#Ws(t){const{container:{style:e},data:{rect:i,rotation:s},parent:{viewport:{rawDims:{pageWidth:n,pageHeight:a,pageX:r,pageY:o}}}}=this;i?.splice(0,4,...t);e.left=100*(t[0]-r)/n+"%";e.top=100*(a-t[3]+o)/a+"%";if(0===s){e.width=100*(t[2]-t[0])/n+"%";e.height=100*(t[3]-t[1])/a+"%"}else this.setRotation(s)}_createContainer(t){const{data:e,parent:{page:i,viewport:s}}=this,n=document.createElement("section");n.setAttribute("data-annotation-id",e.id);this instanceof WidgetAnnotationElement||(n.tabIndex=_t);const{style:a}=n;a.zIndex=this.parent.zIndex++;e.alternativeText&&(n.title=e.alternativeText);e.noRotate&&n.classList.add("norotate");if(!e.rect||this instanceof PopupAnnotationElement){const{rotation:t}=e;e.hasOwnCanvas||0===t||this.setRotation(t,n);return n}const{width:r,height:o}=this;if(!t&&e.borderStyle.width>0){a.borderWidth=`${e.borderStyle.width}px`;const t=e.borderStyle.horizontalCornerRadius,i=e.borderStyle.verticalCornerRadius;if(t>0||i>0){const e=`calc(${t}px * var(--total-scale-factor)) / calc(${i}px * var(--total-scale-factor))`;a.borderRadius=e}else if(this instanceof RadioButtonWidgetAnnotationElement){const t=`calc(${r}px * var(--total-scale-factor)) / calc(${o}px * var(--total-scale-factor))`;a.borderRadius=t}switch(e.borderStyle.style){case x:a.borderStyle="solid";break;case E:a.borderStyle="dashed";break;case S:warn("Unimplemented border style: beveled");break;case C:warn("Unimplemented border style: inset");break;case T:a.borderBottomStyle="solid"}const s=e.borderColor||null;if(s){this.#Gs=!0;a.borderColor=Util.makeHexColor(0|s[0],0|s[1],0|s[2])}else a.borderWidth=0}const l=Util.normalizeRect([e.rect[0],i.view[3]-e.rect[1]+i.view[1],e.rect[2],i.view[3]-e.rect[3]+i.view[1]]),{pageWidth:h,pageHeight:d,pageX:c,pageY:u}=s.rawDims;a.left=100*(l[0]-c)/h+"%";a.top=100*(l[1]-u)/d+"%";const{rotation:p}=e;if(e.hasOwnCanvas||0===p){a.width=100*r/h+"%";a.height=100*o/d+"%"}else this.setRotation(p,n);return n}setRotation(t,e=this.container){if(!this.data.rect)return;const{pageWidth:i,pageHeight:s}=this.parent.viewport.rawDims;let{width:n,height:a}=this;t%180!=0&&([n,a]=[a,n]);e.style.width=100*n/i+"%";e.style.height=100*a/s+"%";e.setAttribute("data-main-rotation",(360-t)%360)}get _commonActions(){const setColor=(t,e,i)=>{const s=i.detail[t],n=s[0],a=s.slice(1);i.target.style[e]=ColorConverters[`${n}_HTML`](a);this.annotationStorage.setValue(this.data.id,{[e]:ColorConverters[`${n}_rgb`](a)})};return shadow(this,"_commonActions",{display:t=>{const{display:e}=t.detail,i=e%2==1;this.container.style.visibility=i?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noView:i,noPrint:1===e||2===e})},print:t=>{this.annotationStorage.setValue(this.data.id,{noPrint:!t.detail.print})},hidden:t=>{const{hidden:e}=t.detail;this.container.style.visibility=e?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noPrint:e,noView:e})},focus:t=>{setTimeout((()=>t.target.focus({preventScroll:!1})),0)},userName:t=>{t.target.title=t.detail.userName},readonly:t=>{t.target.disabled=t.detail.readonly},required:t=>{this._setRequired(t.target,t.detail.required)},bgColor:t=>{setColor("bgColor","backgroundColor",t)},fillColor:t=>{setColor("fillColor","backgroundColor",t)},fgColor:t=>{setColor("fgColor","color",t)},textColor:t=>{setColor("textColor","color",t)},borderColor:t=>{setColor("borderColor","borderColor",t)},strokeColor:t=>{setColor("strokeColor","borderColor",t)},rotation:t=>{const e=t.detail.rotation;this.setRotation(e);this.annotationStorage.setValue(this.data.id,{rotation:e})}})}_dispatchEventFromSandbox(t,e){const i=this._commonActions;for(const s of Object.keys(e.detail)){const n=t[s]||i[s];n?.(e)}}_setDefaultPropertiesFromJS(t){if(!this.enableScripting)return;const e=this.annotationStorage.getRawValue(this.data.id);if(!e)return;const i=this._commonActions;for(const[s,n]of Object.entries(e)){const a=i[s];if(a){a({detail:{[s]:n},target:t});delete e[s]}}}_createQuadrilaterals(){if(!this.container)return;const{quadPoints:t}=this.data;if(!t)return;const[e,i,s,n]=this.data.rect.map((t=>Math.fround(t)));if(8===t.length){const[a,r,o,l]=t.subarray(2,6);if(s===a&&n===r&&e===o&&i===l)return}const{style:a}=this.container;let r;if(this.#Gs){const{borderColor:t,borderWidth:e}=a;a.borderWidth=0;r=["url('data:image/svg+xml;utf8,",'',``];this.container.classList.add("hasBorder")}const o=s-e,l=n-i,{svgFactory:h}=this,d=h.createElement("svg");d.classList.add("quadrilateralsContainer");d.setAttribute("width",0);d.setAttribute("height",0);const c=h.createElement("defs");d.append(c);const u=h.createElement("clipPath"),p=`clippath_${this.data.id}`;u.setAttribute("id",p);u.setAttribute("clipPathUnits","objectBoundingBox");c.append(u);for(let i=2,s=t.length;i`)}if(this.#Gs){r.push("')");a.backgroundImage=r.join("")}this.container.append(d);this.container.style.clipPath=`url(#${p})`}_createPopup(){const{data:t}=this,e=this.#$s=new PopupAnnotationElement({data:{color:t.color,titleObj:t.titleObj,modificationDate:t.modificationDate,contentsObj:t.contentsObj,richText:t.richText,parentRect:t.rect,borderStyle:0,id:`popup_${t.id}`,rotation:t.rotation},parent:this.parent,elements:[this]});this.parent.div.append(e.render())}render(){unreachable("Abstract method `AnnotationElement.render` called")}_getElementsByName(t,e=null){const i=[];if(this._fieldObjects){const s=this._fieldObjects[t];if(s)for(const{page:t,id:n,exportValues:a}of s){if(-1===t)continue;if(n===e)continue;const s="string"==typeof a?a:null,r=document.querySelector(`[data-element-id="${n}"]`);!r||xt.has(r)?i.push({id:n,exportValue:s,domElement:r}):warn(`_getElementsByName - element not allowed: ${n}`)}return i}for(const s of document.getElementsByName(t)){const{exportValue:t}=s,n=s.getAttribute("data-element-id");n!==e&&(xt.has(s)&&i.push({id:n,exportValue:t,domElement:s}))}return i}show(){this.container&&(this.container.hidden=!1);this.popup?.maybeShow()}hide(){this.container&&(this.container.hidden=!0);this.popup?.forceHide()}getElementsToTriggerPopup(){return this.container}addHighlightArea(){const t=this.getElementsToTriggerPopup();if(Array.isArray(t))for(const e of t)e.classList.add("highlightArea");else t.classList.add("highlightArea")}_editOnDoubleClick(){if(!this._isEditable)return;const{annotationEditorType:t,data:{id:e}}=this;this.container.addEventListener("dblclick",(()=>{this.linkService.eventBus?.dispatch("switchannotationeditormode",{source:this,mode:t,editId:e})}))}get width(){return this.data.rect[2]-this.data.rect[0]}get height(){return this.data.rect[3]-this.data.rect[1]}}class LinkAnnotationElement extends AnnotationElement{constructor(t,e=null){super(t,{isRenderable:!0,ignoreBorder:!!e?.ignoreBorder,createQuadrilaterals:!0});this.isTooltipOnly=t.data.isTooltipOnly}render(){const{data:t,linkService:e}=this,i=document.createElement("a");i.setAttribute("data-element-id",t.id);let s=!1;if(t.url){e.addLinkAttributes(i,t.url,t.newWindow);s=!0}else if(t.action){this._bindNamedAction(i,t.action);s=!0}else if(t.attachment){this.#Vs(i,t.attachment,t.attachmentDest);s=!0}else if(t.setOCGState){this.#js(i,t.setOCGState);s=!0}else if(t.dest){this._bindLink(i,t.dest);s=!0}else{if(t.actions&&(t.actions.Action||t.actions["Mouse Up"]||t.actions["Mouse Down"])&&this.enableScripting&&this.hasJSActions){this._bindJSAction(i,t);s=!0}if(t.resetForm){this._bindResetFormAction(i,t.resetForm);s=!0}else if(this.isTooltipOnly&&!s){this._bindLink(i,"");s=!0}}this.container.classList.add("linkAnnotation");s&&this.container.append(i);return this.container}#qs(){this.container.setAttribute("data-internal-link","")}_bindLink(t,e){t.href=this.linkService.getDestinationHash(e);t.onclick=()=>{e&&this.linkService.goToDestination(e);return!1};(e||""===e)&&this.#qs()}_bindNamedAction(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeNamedAction(e);return!1};this.#qs()}#Vs(t,e,i=null){t.href=this.linkService.getAnchorUrl("");e.description&&(t.title=e.description);t.onclick=()=>{this.downloadManager?.openOrDownloadData(e.content,e.filename,i);return!1};this.#qs()}#js(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeSetOCGState(e);return!1};this.#qs()}_bindJSAction(t,e){t.href=this.linkService.getAnchorUrl("");const i=new Map([["Action","onclick"],["Mouse Up","onmouseup"],["Mouse Down","onmousedown"]]);for(const s of Object.keys(e.actions)){const n=i.get(s);n&&(t[n]=()=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e.id,name:s}});return!1})}t.onclick||(t.onclick=()=>!1);this.#qs()}_bindResetFormAction(t,e){const i=t.onclick;i||(t.href=this.linkService.getAnchorUrl(""));this.#qs();if(this._fieldObjects)t.onclick=()=>{i?.();const{fields:t,refs:s,include:n}=e,a=[];if(0!==t.length||0!==s.length){const e=new Set(s);for(const i of t){const t=this._fieldObjects[i]||[];for(const{id:i}of t)e.add(i)}for(const t of Object.values(this._fieldObjects))for(const i of t)e.has(i.id)===n&&a.push(i)}else for(const t of Object.values(this._fieldObjects))a.push(...t);const r=this.annotationStorage,o=[];for(const t of a){const{id:e}=t;o.push(e);switch(t.type){case"text":{const i=t.defaultValue||"";r.setValue(e,{value:i});break}case"checkbox":case"radiobutton":{const i=t.defaultValue===t.exportValues;r.setValue(e,{value:i});break}case"combobox":case"listbox":{const i=t.defaultValue||"";r.setValue(e,{value:i});break}default:continue}const i=document.querySelector(`[data-element-id="${e}"]`);i&&(xt.has(i)?i.dispatchEvent(new Event("resetform")):warn(`_bindResetFormAction - element not allowed: ${e}`))}this.enableScripting&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:"app",ids:o,name:"ResetForm"}});return!1};else{warn('_bindResetFormAction - "resetForm" action not supported, ensure that the `fieldObjects` parameter is provided.');i||(t.onclick=()=>!1)}}}class TextAnnotationElement extends AnnotationElement{constructor(t){super(t,{isRenderable:!0})}render(){this.container.classList.add("textAnnotation");const t=document.createElement("img");t.src=this.imageResourcesPath+"annotation-"+this.data.name.toLowerCase()+".svg";t.setAttribute("data-l10n-id","pdfjs-text-annotation-type");t.setAttribute("data-l10n-args",JSON.stringify({type:this.data.name}));!this.data.popupRef&&this.hasPopupData&&this._createPopup();this.container.append(t);return this.container}}class WidgetAnnotationElement extends AnnotationElement{render(){return this.container}showElementAndHideCanvas(t){if(this.data.hasOwnCanvas){"CANVAS"===t.previousSibling?.nodeName&&(t.previousSibling.hidden=!0);t.hidden=!1}}_getKeyModifier(t){return util_FeatureTest.platform.isMac?t.metaKey:t.ctrlKey}_setEventListener(t,e,i,s,n){i.includes("mouse")?t.addEventListener(i,(t=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t),shift:t.shiftKey,modifier:this._getKeyModifier(t)}})})):t.addEventListener(i,(t=>{if("blur"===i){if(!e.focused||!t.relatedTarget)return;e.focused=!1}else if("focus"===i){if(e.focused)return;e.focused=!0}n&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t)}})}))}_setEventListeners(t,e,i,s){for(const[n,a]of i)if("Action"===a||this.data.actions?.[a]){"Focus"!==a&&"Blur"!==a||(e||={focused:!1});this._setEventListener(t,e,n,a,s);"Focus"!==a||this.data.actions?.Blur?"Blur"!==a||this.data.actions?.Focus||this._setEventListener(t,e,"focus","Focus",null):this._setEventListener(t,e,"blur","Blur",null)}}_setBackgroundColor(t){const e=this.data.backgroundColor||null;t.style.backgroundColor=null===e?"transparent":Util.makeHexColor(e[0],e[1],e[2])}_setTextStyle(t){const e=["left","center","right"],{fontColor:s}=this.data.defaultAppearanceData,n=this.data.defaultAppearanceData.fontSize||9,a=t.style;let r;const roundToOneDecimal=t=>Math.round(10*t)/10;if(this.data.multiLine){const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2),e=t/(Math.round(t/(i*n))||1);r=Math.min(n,roundToOneDecimal(e/i))}else{const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2);r=Math.min(n,roundToOneDecimal(t/i))}a.fontSize=`calc(${r}px * var(--total-scale-factor))`;a.color=Util.makeHexColor(s[0],s[1],s[2]);null!==this.data.textAlignment&&(a.textAlign=e[this.data.textAlignment])}_setRequired(t,e){e?t.setAttribute("required",!0):t.removeAttribute("required");t.setAttribute("aria-required",e)}}class TextWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms||t.data.hasOwnCanvas||!t.data.hasAppearance&&!!t.data.fieldValue})}setPropertyOnSiblings(t,e,i,s){const n=this.annotationStorage;for(const a of this._getElementsByName(t.name,t.id)){a.domElement&&(a.domElement[e]=i);n.setValue(a.id,{[s]:i})}}render(){const t=this.annotationStorage,e=this.data.id;this.container.classList.add("textWidgetAnnotation");let i=null;if(this.renderForms){const s=t.getValue(e,{value:this.data.fieldValue});let n=s.value||"";const a=t.getValue(e,{charLimit:this.data.maxLen}).charLimit;a&&n.length>a&&(n=n.slice(0,a));let r=s.formattedValue||this.data.textContent?.join("\n")||null;r&&this.data.comb&&(r=r.replaceAll(/\s+/g,""));const o={userValue:n,formattedValue:r,lastCommittedValue:null,commitKey:1,focused:!1};if(this.data.multiLine){i=document.createElement("textarea");i.textContent=r??n;this.data.doNotScroll&&(i.style.overflowY="hidden")}else{i=document.createElement("input");i.type=this.data.password?"password":"text";i.setAttribute("value",r??n);this.data.doNotScroll&&(i.style.overflowX="hidden")}this.data.hasOwnCanvas&&(i.hidden=!0);xt.add(i);i.setAttribute("data-element-id",e);i.disabled=this.data.readOnly;i.name=this.data.fieldName;i.tabIndex=_t;this._setRequired(i,this.data.required);a&&(i.maxLength=a);i.addEventListener("input",(s=>{t.setValue(e,{value:s.target.value});this.setPropertyOnSiblings(i,"value",s.target.value,"value");o.formattedValue=null}));i.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue??"";i.value=o.userValue=e;o.formattedValue=null}));let blurListener=t=>{const{formattedValue:e}=o;null!=e&&(t.target.value=e);t.target.scrollLeft=0};if(this.enableScripting&&this.hasJSActions){i.addEventListener("focus",(t=>{if(o.focused)return;const{target:e}=t;o.userValue&&(e.value=o.userValue);o.lastCommittedValue=e.value;o.commitKey=1;this.data.actions?.Focus||(o.focused=!0)}));i.addEventListener("updatefromsandbox",(i=>{this.showElementAndHideCanvas(i.target);const s={value(i){o.userValue=i.detail.value??"";t.setValue(e,{value:o.userValue.toString()});i.target.value=o.userValue},formattedValue(i){const{formattedValue:s}=i.detail;o.formattedValue=s;null!=s&&i.target!==document.activeElement&&(i.target.value=s);t.setValue(e,{formattedValue:s})},selRange(t){t.target.setSelectionRange(...t.detail.selRange)},charLimit:i=>{const{charLimit:s}=i.detail,{target:n}=i;if(0===s){n.removeAttribute("maxLength");return}n.setAttribute("maxLength",s);let a=o.userValue;if(a&&!(a.length<=s)){a=a.slice(0,s);n.value=o.userValue=a;t.setValue(e,{value:a});this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:a,willCommit:!0,commitKey:1,selStart:n.selectionStart,selEnd:n.selectionEnd}})}}};this._dispatchEventFromSandbox(s,i)}));i.addEventListener("keydown",(t=>{o.commitKey=1;let i=-1;"Escape"===t.key?i=0:"Enter"!==t.key||this.data.multiLine?"Tab"===t.key&&(o.commitKey=3):i=2;if(-1===i)return;const{value:s}=t.target;if(o.lastCommittedValue!==s){o.lastCommittedValue=s;o.userValue=s;this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:s,willCommit:!0,commitKey:i,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}})}}));const s=blurListener;blurListener=null;i.addEventListener("blur",(t=>{if(!o.focused||!t.relatedTarget)return;this.data.actions?.Blur||(o.focused=!1);const{value:i}=t.target;o.userValue=i;o.lastCommittedValue!==i&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:i,willCommit:!0,commitKey:o.commitKey,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}});s(t)}));this.data.actions?.Keystroke&&i.addEventListener("beforeinput",(t=>{o.lastCommittedValue=null;const{data:i,target:s}=t,{value:n,selectionStart:a,selectionEnd:r}=s;let l=a,h=r;switch(t.inputType){case"deleteWordBackward":{const t=n.substring(0,a).match(/\w*[^\w]*$/);t&&(l-=t[0].length);break}case"deleteWordForward":{const t=n.substring(a).match(/^[^\w]*\w*/);t&&(h+=t[0].length);break}case"deleteContentBackward":a===r&&(l-=1);break;case"deleteContentForward":a===r&&(h+=1)}t.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:n,change:i||"",willCommit:!1,selStart:l,selEnd:h}})}));this._setEventListeners(i,o,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.value))}blurListener&&i.addEventListener("blur",blurListener);if(this.data.comb){const t=(this.data.rect[2]-this.data.rect[0])/a;i.classList.add("comb");i.style.letterSpacing=`calc(${t}px * var(--total-scale-factor) - 1ch)`}}else{i=document.createElement("div");i.textContent=this.data.fieldValue;i.style.verticalAlign="middle";i.style.display="table-cell";this.data.hasOwnCanvas&&(i.hidden=!0)}this._setTextStyle(i);this._setBackgroundColor(i);this._setDefaultPropertiesFromJS(i);this.container.append(i);return this.container}}class SignatureWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:!!t.data.hasOwnCanvas})}}class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.exportValue===e.fieldValue}).value;if("string"==typeof s){s="Off"!==s;t.setValue(i,{value:s})}this.container.classList.add("buttonWidgetAnnotation","checkBox");const n=document.createElement("input");xt.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="checkbox";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.setAttribute("exportValue",e.exportValue);n.tabIndex=_t;n.addEventListener("change",(s=>{const{name:n,checked:a}=s.target;for(const s of this._getElementsByName(n,i)){const i=a&&s.exportValue===e.exportValue;s.domElement&&(s.domElement.checked=i);t.setValue(s.id,{value:i})}t.setValue(i,{value:a})}));n.addEventListener("resetform",(t=>{const i=e.defaultFieldValue||"Off";t.target.checked=i===e.exportValue}));if(this.enableScripting&&this.hasJSActions){n.addEventListener("updatefromsandbox",(e=>{const s={value(e){e.target.checked="Off"!==e.detail.value;t.setValue(i,{value:e.target.checked})}};this._dispatchEventFromSandbox(s,e)}));this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("buttonWidgetAnnotation","radioButton");const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.fieldValue===e.buttonValue}).value;if("string"==typeof s){s=s!==e.buttonValue;t.setValue(i,{value:s})}if(s)for(const s of this._getElementsByName(e.fieldName,i))t.setValue(s.id,{value:!1});const n=document.createElement("input");xt.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="radio";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.tabIndex=_t;n.addEventListener("change",(e=>{const{name:s,checked:n}=e.target;for(const e of this._getElementsByName(s,i))t.setValue(e.id,{value:!1});t.setValue(i,{value:n})}));n.addEventListener("resetform",(t=>{const i=e.defaultFieldValue;t.target.checked=null!=i&&i===e.buttonValue}));if(this.enableScripting&&this.hasJSActions){const s=e.buttonValue;n.addEventListener("updatefromsandbox",(e=>{const n={value:e=>{const n=s===e.detail.value;for(const s of this._getElementsByName(e.target.name)){const e=n&&s.id===i;s.domElement&&(s.domElement.checked=e);t.setValue(s.id,{value:e})}}};this._dispatchEventFromSandbox(n,e)}));this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class PushButtonWidgetAnnotationElement extends LinkAnnotationElement{constructor(t){super(t,{ignoreBorder:t.data.hasAppearance})}render(){const t=super.render();t.classList.add("buttonWidgetAnnotation","pushButton");const e=t.lastChild;if(this.enableScripting&&this.hasJSActions&&e){this._setDefaultPropertiesFromJS(e);e.addEventListener("updatefromsandbox",(t=>{this._dispatchEventFromSandbox({},t)}))}return t}}class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("choiceWidgetAnnotation");const t=this.annotationStorage,e=this.data.id,i=t.getValue(e,{value:this.data.fieldValue}),s=document.createElement("select");xt.add(s);s.setAttribute("data-element-id",e);s.disabled=this.data.readOnly;this._setRequired(s,this.data.required);s.name=this.data.fieldName;s.tabIndex=_t;let n=this.data.combo&&this.data.options.length>0;if(!this.data.combo){s.size=this.data.options.length;this.data.multiSelect&&(s.multiple=!0)}s.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue;for(const t of s.options)t.selected=t.value===e}));for(const t of this.data.options){const e=document.createElement("option");e.textContent=t.displayValue;e.value=t.exportValue;if(i.value.includes(t.exportValue)){e.setAttribute("selected",!0);n=!1}s.append(e)}let a=null;if(n){const t=document.createElement("option");t.value=" ";t.setAttribute("hidden",!0);t.setAttribute("selected",!0);s.prepend(t);a=()=>{t.remove();s.removeEventListener("input",a);a=null};s.addEventListener("input",a)}const getValue=t=>{const e=t?"value":"textContent",{options:i,multiple:n}=s;return n?Array.prototype.filter.call(i,(t=>t.selected)).map((t=>t[e])):-1===i.selectedIndex?null:i[i.selectedIndex][e]};let r=getValue(!1);const getItems=t=>{const e=t.target.options;return Array.prototype.map.call(e,(t=>({displayValue:t.textContent,exportValue:t.value})))};if(this.enableScripting&&this.hasJSActions){s.addEventListener("updatefromsandbox",(i=>{const n={value(i){a?.();const n=i.detail.value,o=new Set(Array.isArray(n)?n:[n]);for(const t of s.options)t.selected=o.has(t.value);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},multipleSelection(t){s.multiple=!0},remove(i){const n=s.options,a=i.detail.remove;n[a].selected=!1;s.remove(a);if(n.length>0){-1===Array.prototype.findIndex.call(n,(t=>t.selected))&&(n[0].selected=!0)}t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},clear(i){for(;0!==s.length;)s.remove(0);t.setValue(e,{value:null,items:[]});r=getValue(!1)},insert(i){const{index:n,displayValue:a,exportValue:o}=i.detail.insert,l=s.children[n],h=document.createElement("option");h.textContent=a;h.value=o;l?l.before(h):s.append(h);t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},items(i){const{items:n}=i.detail;for(;0!==s.length;)s.remove(0);for(const t of n){const{displayValue:e,exportValue:i}=t,n=document.createElement("option");n.textContent=e;n.value=i;s.append(n)}s.options.length>0&&(s.options[0].selected=!0);t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},indices(i){const s=new Set(i.detail.indices);for(const t of i.target.options)t.selected=s.has(t.index);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},editable(t){t.target.disabled=!t.detail.editable}};this._dispatchEventFromSandbox(n,i)}));s.addEventListener("input",(i=>{const s=getValue(!0),n=getValue(!1);t.setValue(e,{value:s});i.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:r,change:n,changeEx:s,willCommit:!1,commitKey:1,keyDown:!1}})}));this._setEventListeners(s,null,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"],["input","Action"],["input","Validate"]],(t=>t.target.value))}else s.addEventListener("input",(function(i){t.setValue(e,{value:getValue(!0)})}));this.data.combo&&this._setTextStyle(s);this._setBackgroundColor(s);this._setDefaultPropertiesFromJS(s);this.container.append(s);return this.container}}class PopupAnnotationElement extends AnnotationElement{constructor(t){const{data:e,elements:i}=t;super(t,{isRenderable:AnnotationElement._hasPopupData(e)});this.elements=i;this.popup=null}render(){this.container.classList.add("popupAnnotation");const t=this.popup=new PopupElement({container:this.container,color:this.data.color,titleObj:this.data.titleObj,modificationDate:this.data.modificationDate,contentsObj:this.data.contentsObj,richText:this.data.richText,rect:this.data.rect,parentRect:this.data.parentRect||null,parent:this.parent,elements:this.elements,open:this.data.open}),e=[];for(const i of this.elements){i.popup=t;i.container.ariaHasPopup="dialog";e.push(i.data.id);i.addHighlightArea()}this.container.setAttribute("aria-controls",e.map((t=>`${U}${t}`)).join(","));return this.container}}class PopupElement{#Xs=this.#Ks.bind(this);#Ys=this.#Qs.bind(this);#Js=this.#Zs.bind(this);#tn=this.#en.bind(this);#in=null;#mt=null;#sn=null;#nn=null;#an=null;#rn=null;#on=null;#ln=!1;#hn=null;#T=null;#dn=null;#cn=null;#un=null;#zs=null;#pn=!1;constructor({container:t,color:e,elements:i,titleObj:s,modificationDate:n,contentsObj:a,richText:r,parent:o,rect:l,parentRect:h,open:d}){this.#mt=t;this.#un=s;this.#sn=a;this.#cn=r;this.#rn=o;this.#in=e;this.#dn=l;this.#on=h;this.#an=i;this.#nn=PDFDateString.toDateObject(n);this.trigger=i.flatMap((t=>t.getElementsToTriggerPopup()));for(const t of this.trigger){t.addEventListener("click",this.#tn);t.addEventListener("mouseenter",this.#Js);t.addEventListener("mouseleave",this.#Ys);t.classList.add("popupTriggerArea")}for(const t of i)t.container?.addEventListener("keydown",this.#Xs);this.#mt.hidden=!0;d&&this.#en()}render(){if(this.#hn)return;const t=this.#hn=document.createElement("div");t.className="popup";if(this.#in){const e=t.style.outlineColor=Util.makeHexColor(...this.#in);t.style.backgroundColor=`color-mix(in srgb, ${e} 30%, white)`}const e=document.createElement("span");e.className="header";const i=document.createElement("h1");e.append(i);({dir:i.dir,str:i.textContent}=this.#un);t.append(e);if(this.#nn){const t=document.createElement("span");t.classList.add("popupDate");t.setAttribute("data-l10n-id","pdfjs-annotation-date-time-string");t.setAttribute("data-l10n-args",JSON.stringify({dateObj:this.#nn.valueOf()}));e.append(t)}const s=this.#gn;if(s){XfaLayer.render({xfaHtml:s,intent:"richText",div:t});t.lastChild.classList.add("richText","popupContent")}else{const e=this._formatContents(this.#sn);t.append(e)}this.#mt.append(t)}get#gn(){const t=this.#cn,e=this.#sn;return!t?.str||e?.str&&e.str!==t.str?null:this.#cn.html||null}get#fn(){return this.#gn?.attributes?.style?.fontSize||0}get#mn(){return this.#gn?.attributes?.style?.color||null}#bn(t){const e=[],i={str:t,html:{name:"div",attributes:{dir:"auto"},children:[{name:"p",children:e}]}},s={style:{color:this.#mn,fontSize:this.#fn?`calc(${this.#fn}px * var(--total-scale-factor))`:""}};for(const i of t.split("\n"))e.push({name:"span",value:i,attributes:s});return i}_formatContents({str:t,dir:e}){const i=document.createElement("p");i.classList.add("popupContent");i.dir=e;const s=t.split(/(?:\r\n?|\n)/);for(let t=0,e=s.length;t=0&&n.setAttribute("stroke-width",e||1);if(i)for(let t=0,e=this.#En.length;t{"Enter"===t.key&&(s?t.metaKey:t.ctrlKey)&&this.#Tn()}));!e.popupRef&&this.hasPopupData?this._createPopup():i.classList.add("popupTriggerArea");t.append(i);return t}getElementsToTriggerPopup(){return this.#Cn}addHighlightArea(){this.container.classList.add("highlightArea")}#Tn(){this.downloadManager?.openOrDownloadData(this.content,this.filename)}}class AnnotationLayer{#Mn=null;#Dn=null;#Pn=new Map;#kn=null;constructor({div:t,accessibilityManager:e,annotationCanvasMap:i,annotationEditorUIManager:s,page:n,viewport:a,structTreeLayer:r}){this.div=t;this.#Mn=e;this.#Dn=i;this.#kn=r||null;this.page=n;this.viewport=a;this.zIndex=0;this._annotationEditorUIManager=s}hasEditableAnnotations(){return this.#Pn.size>0}async#In(t,e){const i=t.firstChild||t,s=i.id=`${U}${e}`,n=await(this.#kn?.getAriaAttributes(s));if(n)for(const[t,e]of n)i.setAttribute(t,e);this.div.append(t);this.#Mn?.moveElementInDOM(this.div,t,i,!1)}async render(t){const{annotations:e}=t,i=this.div;setLayerDimensions(i,this.viewport);const s=new Map,n={data:null,layer:i,linkService:t.linkService,downloadManager:t.downloadManager,imageResourcesPath:t.imageResourcesPath||"",renderForms:!1!==t.renderForms,svgFactory:new DOMSVGFactory,annotationStorage:t.annotationStorage||new AnnotationStorage,enableScripting:!0===t.enableScripting,hasJSActions:t.hasJSActions,fieldObjects:t.fieldObjects,parent:this,elements:null};for(const t of e){if(t.noHTML)continue;const e=t.annotationType===_.POPUP;if(e){const e=s.get(t.id);if(!e)continue;n.elements=e}else if(t.rect[2]===t.rect[0]||t.rect[3]===t.rect[1])continue;n.data=t;const i=AnnotationElementFactory.create(n);if(!i.isRenderable)continue;if(!e&&t.popupRef){const e=s.get(t.popupRef);e?e.push(i):s.set(t.popupRef,[i])}const a=i.render();t.hidden&&(a.style.visibility="hidden");await this.#In(a,t.id);if(i._isEditable){this.#Pn.set(i.data.id,i);this._annotationEditorUIManager?.renderAnnotationElement(i)}}this.#Rn()}async addLinkAnnotations(t,e){const i={data:null,layer:this.div,linkService:e,svgFactory:new DOMSVGFactory,parent:this};for(const e of t){e.borderStyle||=AnnotationLayer._defaultBorderStyle;i.data=e;const t=AnnotationElementFactory.create(i);if(!t.isRenderable)continue;const s=t.render();await this.#In(s,e.id)}}update({viewport:t}){const e=this.div;this.viewport=t;setLayerDimensions(e,{rotation:t.rotation});this.#Rn();e.hidden=!1}#Rn(){if(!this.#Dn)return;const t=this.div;for(const[e,i]of this.#Dn){const s=t.querySelector(`[data-annotation-id="${e}"]`);if(!s)continue;i.className="annotationContent";const{firstChild:n}=s;n?"CANVAS"===n.nodeName?n.replaceWith(i):n.classList.contains("annotationContent")?n.after(i):n.before(i):s.append(i);const a=this.#Pn.get(e);if(a)if(a._hasNoCanvas){this._annotationEditorUIManager?.setMissingCanvas(e,s.id,i);a._hasNoCanvas=!1}else a.canvas=i}this.#Dn.clear()}getEditableAnnotations(){return Array.from(this.#Pn.values())}getEditableAnnotation(t){return this.#Pn.get(t)}static get _defaultBorderStyle(){return shadow(this,"_defaultBorderStyle",Object.freeze({width:1,rawWidth:1,style:x,dashArray:[3],horizontalCornerRadius:0,verticalCornerRadius:0}))}}const Et=/\r\n?|\n/g;class FreeTextEditor extends AnnotationEditor{#in;#Fn="";#Ln=`${this.id}-editor`;#On=null;#fn;static _freeTextDefaultContent="";static _internalPadding=0;static _defaultColor=null;static _defaultFontSize=10;static get _keyboardManager(){const t=FreeTextEditor.prototype,arrowChecker=t=>t.isEmpty(),e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+s","mac+meta+s","ctrl+p","mac+meta+p"],t.commitOrRemove,{bubbles:!0}],[["ctrl+Enter","mac+meta+Enter","Escape","mac+Escape"],t.commitOrRemove],[["ArrowLeft","mac+ArrowLeft"],t._translateEmpty,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t._translateEmpty,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t._translateEmpty,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t._translateEmpty,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t._translateEmpty,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t._translateEmpty,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t._translateEmpty,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t._translateEmpty,{args:[0,i],checker:arrowChecker}]]))}static _type="freetext";static _editorType=u.FREETEXT;constructor(t){super({...t,name:"freeTextEditor"});this.#in=t.color||FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor;this.#fn=t.fontSize||FreeTextEditor._defaultFontSize}static initialize(t,e){AnnotationEditor.initialize(t,e);const i=getComputedStyle(document.documentElement);this._internalPadding=parseFloat(i.getPropertyValue("--freetext-padding"))}static updateDefaultParams(t,e){switch(t){case p.FREETEXT_SIZE:FreeTextEditor._defaultFontSize=e;break;case p.FREETEXT_COLOR:FreeTextEditor._defaultColor=e}}updateParams(t,e){switch(t){case p.FREETEXT_SIZE:this.#Nn(e);break;case p.FREETEXT_COLOR:this.#Bn(e)}}static get defaultPropertiesToUpdate(){return[[p.FREETEXT_SIZE,FreeTextEditor._defaultFontSize],[p.FREETEXT_COLOR,FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor]]}get propertiesToUpdate(){return[[p.FREETEXT_SIZE,this.#fn],[p.FREETEXT_COLOR,this.#in]]}#Nn(t){const setFontsize=t=>{this.editorDiv.style.fontSize=`calc(${t}px * var(--total-scale-factor))`;this.translate(0,-(t-this.#fn)*this.parentScale);this.#fn=t;this.#Hn()},e=this.#fn;this.addCommands({cmd:setFontsize.bind(this,t),undo:setFontsize.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.FREETEXT_SIZE,overwriteIfSameType:!0,keepUndo:!0})}#Bn(t){const setColor=t=>{this.#in=this.editorDiv.style.color=t},e=this.#in;this.addCommands({cmd:setColor.bind(this,t),undo:setColor.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.FREETEXT_COLOR,overwriteIfSameType:!0,keepUndo:!0})}_translateEmpty(t,e){this._uiManager.translateSelectedEditors(t,e,!0)}getInitialTranslation(){const t=this.parentScale;return[-FreeTextEditor._internalPadding*t,-(FreeTextEditor._internalPadding+this.#fn)*t]}rebuild(){if(this.parent){super.rebuild();null!==this.div&&(this.isAttachedToDOM||this.parent.add(this))}}enableEditMode(){if(this.isInEditMode())return;this.parent.setEditingState(!1);this.parent.updateToolbar(u.FREETEXT);super.enableEditMode();this.overlayDiv.classList.remove("enabled");this.editorDiv.contentEditable=!0;this._isDraggable=!1;this.div.removeAttribute("aria-activedescendant");this.#On=new AbortController;const t=this._uiManager.combinedSignal(this.#On);this.editorDiv.addEventListener("keydown",this.editorDivKeydown.bind(this),{signal:t});this.editorDiv.addEventListener("focus",this.editorDivFocus.bind(this),{signal:t});this.editorDiv.addEventListener("blur",this.editorDivBlur.bind(this),{signal:t});this.editorDiv.addEventListener("input",this.editorDivInput.bind(this),{signal:t});this.editorDiv.addEventListener("paste",this.editorDivPaste.bind(this),{signal:t})}disableEditMode(){if(this.isInEditMode()){this.parent.setEditingState(!0);super.disableEditMode();this.overlayDiv.classList.add("enabled");this.editorDiv.contentEditable=!1;this.div.setAttribute("aria-activedescendant",this.#Ln);this._isDraggable=!0;this.#On?.abort();this.#On=null;this.div.focus({preventScroll:!0});this.isEditing=!1;this.parent.div.classList.add("freetextEditing")}}focusin(t){if(this._focusEventsAllowed){super.focusin(t);t.target!==this.editorDiv&&this.editorDiv.focus()}}onceAdded(t){if(!this.width){this.enableEditMode();t&&this.editorDiv.focus();this._initialOptions?.isCentered&&this.center();this._initialOptions=null}}isEmpty(){return!this.editorDiv||""===this.editorDiv.innerText.trim()}remove(){this.isEditing=!1;if(this.parent){this.parent.setEditingState(!0);this.parent.div.classList.add("freetextEditing")}super.remove()}#Un(){const t=[];this.editorDiv.normalize();let e=null;for(const i of this.editorDiv.childNodes)if(e?.nodeType!==Node.TEXT_NODE||"BR"!==i.nodeName){t.push(FreeTextEditor.#zn(i));e=i}return t.join("\n")}#Hn(){const[t,e]=this.parentDimensions;let i;if(this.isAttachedToDOM)i=this.div.getBoundingClientRect();else{const{currentLayer:t,div:e}=this,s=e.style.display,n=e.classList.contains("hidden");e.classList.remove("hidden");e.style.display="hidden";t.div.append(this.div);i=e.getBoundingClientRect();e.remove();e.style.display=s;e.classList.toggle("hidden",n)}if(this.rotation%180==this.parentRotation%180){this.width=i.width/t;this.height=i.height/e}else{this.width=i.height/t;this.height=i.width/e}this.fixAndSetPosition()}commit(){if(!this.isInEditMode())return;super.commit();this.disableEditMode();const t=this.#Fn,e=this.#Fn=this.#Un().trimEnd();if(t===e)return;const setText=t=>{this.#Fn=t;if(t){this.#Gn();this._uiManager.rebuild(this);this.#Hn()}else this.remove()};this.addCommands({cmd:()=>{setText(e)},undo:()=>{setText(t)},mustExec:!1});this.#Hn()}shouldGetKeyboardEvents(){return this.isInEditMode()}enterInEditMode(){this.enableEditMode();this.editorDiv.focus()}dblclick(t){this.enterInEditMode()}keydown(t){if(t.target===this.div&&"Enter"===t.key){this.enterInEditMode();t.preventDefault()}}editorDivKeydown(t){FreeTextEditor._keyboardManager.exec(this,t)}editorDivFocus(t){this.isEditing=!0}editorDivBlur(t){this.isEditing=!1}editorDivInput(t){this.parent.div.classList.toggle("freetextEditing",this.isEmpty())}disableEditing(){this.editorDiv.setAttribute("role","comment");this.editorDiv.removeAttribute("aria-multiline")}enableEditing(){this.editorDiv.setAttribute("role","textbox");this.editorDiv.setAttribute("aria-multiline",!0)}render(){if(this.div)return this.div;let t,e;if(this._isCopy||this.annotationElementId){t=this.x;e=this.y}super.render();this.editorDiv=document.createElement("div");this.editorDiv.className="internal";this.editorDiv.setAttribute("id",this.#Ln);this.editorDiv.setAttribute("data-l10n-id","pdfjs-free-text2");this.editorDiv.setAttribute("data-l10n-attrs","default-content");this.enableEditing();this.editorDiv.contentEditable=!0;const{style:i}=this.editorDiv;i.fontSize=`calc(${this.#fn}px * var(--total-scale-factor))`;i.color=this.#in;this.div.append(this.editorDiv);this.overlayDiv=document.createElement("div");this.overlayDiv.classList.add("overlay","enabled");this.div.append(this.overlayDiv);bindEvents(this,this.div,["dblclick","keydown"]);if(this._isCopy||this.annotationElementId){const[i,s]=this.parentDimensions;if(this.annotationElementId){const{position:n}=this._initialData;let[a,r]=this.getInitialTranslation();[a,r]=this.pageTranslationToScreen(a,r);const[o,l]=this.pageDimensions,[h,d]=this.pageTranslation;let c,u;switch(this.rotation){case 0:c=t+(n[0]-h)/o;u=e+this.height-(n[1]-d)/l;break;case 90:c=t+(n[0]-h)/o;u=e-(n[1]-d)/l;[a,r]=[r,-a];break;case 180:c=t-this.width+(n[0]-h)/o;u=e-(n[1]-d)/l;[a,r]=[-a,-r];break;case 270:c=t+(n[0]-h-this.height*l)/o;u=e+(n[1]-d-this.width*o)/l;[a,r]=[-r,a]}this.setAt(c*i,u*s,a,r)}else this._moveAfterPaste(t,e);this.#Gn();this._isDraggable=!0;this.editorDiv.contentEditable=!1}else{this._isDraggable=!1;this.editorDiv.contentEditable=!0}return this.div}static#zn(t){return(t.nodeType===Node.TEXT_NODE?t.nodeValue:t.innerText).replaceAll(Et,"")}editorDivPaste(t){const e=t.clipboardData||window.clipboardData,{types:i}=e;if(1===i.length&&"text/plain"===i[0])return;t.preventDefault();const s=FreeTextEditor.#$n(e.getData("text")||"").replaceAll(Et,"\n");if(!s)return;const n=window.getSelection();if(!n.rangeCount)return;this.editorDiv.normalize();n.deleteFromDocument();const a=n.getRangeAt(0);if(!s.includes("\n")){a.insertNode(document.createTextNode(s));this.editorDiv.normalize();n.collapseToStart();return}const{startContainer:r,startOffset:o}=a,l=[],h=[];if(r.nodeType===Node.TEXT_NODE){const t=r.parentElement;h.push(r.nodeValue.slice(o).replaceAll(Et,""));if(t!==this.editorDiv){let e=l;for(const i of this.editorDiv.childNodes)i!==t?e.push(FreeTextEditor.#zn(i)):e=h}l.push(r.nodeValue.slice(0,o).replaceAll(Et,""))}else if(r===this.editorDiv){let t=l,e=0;for(const i of this.editorDiv.childNodes){e++===o&&(t=h);t.push(FreeTextEditor.#zn(i))}}this.#Fn=`${l.join("\n")}${s}${h.join("\n")}`;this.#Gn();const d=new Range;let c=Math.sumPrecise(l.map((t=>t.length)));for(const{firstChild:t}of this.editorDiv.childNodes)if(t.nodeType===Node.TEXT_NODE){const e=t.nodeValue.length;if(c<=e){d.setStart(t,c);d.setEnd(t,c);break}c-=e}n.removeAllRanges();n.addRange(d)}#Gn(){this.editorDiv.replaceChildren();if(this.#Fn)for(const t of this.#Fn.split("\n")){const e=document.createElement("div");e.append(t?document.createTextNode(t):document.createElement("br"));this.editorDiv.append(e)}}#Wn(){return this.#Fn.replaceAll(" "," ")}static#$n(t){return t.replaceAll(" "," ")}get contentDiv(){return this.editorDiv}static async deserialize(t,e,i){let s=null;if(t instanceof FreeTextAnnotationElement){const{data:{defaultAppearanceData:{fontSize:e,fontColor:i},rect:n,rotation:a,id:r,popupRef:o},textContent:l,textPosition:h,parent:{page:{pageNumber:d}}}=t;if(!l||0===l.length)return null;s=t={annotationType:u.FREETEXT,color:Array.from(i),fontSize:e,value:l.join("\n"),position:h,pageIndex:d-1,rect:n.slice(0),rotation:a,id:r,deleted:!1,popupRef:o}}const n=await super.deserialize(t,e,i);n.#fn=t.fontSize;n.#in=Util.makeHexColor(...t.color);n.#Fn=FreeTextEditor.#$n(t.value);n.annotationElementId=t.id||null;n._initialData=s;return n}serialize(t=!1){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const e=FreeTextEditor._internalPadding*this.parentScale,i=this.getRect(e,e),s=AnnotationEditor._colorManager.convert(this.isAttachedToDOM?getComputedStyle(this.editorDiv).color:this.#in),n={annotationType:u.FREETEXT,color:s,fontSize:this.#fn,value:this.#Wn(),pageIndex:this.pageIndex,rect:i,rotation:this.rotation,structTreeParentId:this._structTreeParentId};if(t){n.isCopy=!0;return n}if(this.annotationElementId&&!this.#Vn(n))return null;n.id=this.annotationElementId;return n}#Vn(t){const{value:e,fontSize:i,color:s,pageIndex:n}=this._initialData;return this._hasBeenMoved||t.value!==e||t.fontSize!==i||t.color.some(((t,e)=>t!==s[e]))||t.pageIndex!==n}renderAnnotationElement(t){const e=super.renderAnnotationElement(t);if(this.deleted)return e;const{style:i}=e;i.fontSize=`calc(${this.#fn}px * var(--total-scale-factor))`;i.color=this.#in;e.replaceChildren();for(const t of this.#Fn.split("\n")){const i=document.createElement("div");i.append(t?document.createTextNode(t):document.createElement("br"));e.append(i)}const s=FreeTextEditor._internalPadding*this.parentScale;t.updateEdited({rect:this.getRect(s,s),popupContent:this.#Fn});return e}resetAnnotationElement(t){super.resetAnnotationElement(t);t.resetEdited()}}class Outline{static PRECISION=1e-4;toSVGPath(){unreachable("Abstract method `toSVGPath` must be implemented.")}get box(){unreachable("Abstract getter `box` must be implemented.")}serialize(t,e){unreachable("Abstract method `serialize` must be implemented.")}static _rescale(t,e,i,s,n,a){a||=new Float32Array(t.length);for(let r=0,o=t.length;r=6;t-=6)isNaN(e[t])?i.push(`L${e[t+4]} ${e[t+5]}`):i.push(`C${e[t]} ${e[t+1]} ${e[t+2]} ${e[t+3]} ${e[t+4]} ${e[t+5]}`);this.#ca(i);return i.join(" ")}#ha(){const[t,e,i,s]=this.#jn,[n,a,r,o]=this.#la();return`M${(this.#Qn[2]-t)/i} ${(this.#Qn[3]-e)/s} L${(this.#Qn[4]-t)/i} ${(this.#Qn[5]-e)/s} L${n} ${a} L${r} ${o} L${(this.#Qn[16]-t)/i} ${(this.#Qn[17]-e)/s} L${(this.#Qn[14]-t)/i} ${(this.#Qn[15]-e)/s} Z`}#ca(t){const e=this.#qn;t.push(`L${e[4]} ${e[5]} Z`)}#da(t){const[e,i,s,n]=this.#jn,a=this.#Qn.subarray(4,6),r=this.#Qn.subarray(16,18),[o,l,h,d]=this.#la();t.push(`L${(a[0]-e)/s} ${(a[1]-i)/n} L${o} ${l} L${h} ${d} L${(r[0]-e)/s} ${(r[1]-i)/n}`)}newFreeDrawOutline(t,e,i,s,n,a){return new FreeDrawOutline(t,e,i,s,n,a)}getOutlines(){const t=this.#Yn,e=this.#qn,i=this.#Qn,[s,n,a,r]=this.#jn,o=new Float32Array((this.#na?.length??0)+2);for(let t=0,e=o.length-2;t=6;t-=6)for(let i=0;i<6;i+=2)if(isNaN(e[t+i])){l[h]=l[h+1]=NaN;h+=2}else{l[h]=e[t+i];l[h+1]=e[t+i+1];h+=2}this.#ga(l,h);return this.newFreeDrawOutline(l,o,this.#jn,this.#ia,this.#Xn,this.#Kn)}#ua(t){const e=this.#Qn,[i,s,n,a]=this.#jn,[r,o,l,h]=this.#la(),d=new Float32Array(36);d.set([NaN,NaN,NaN,NaN,(e[2]-i)/n,(e[3]-s)/a,NaN,NaN,NaN,NaN,(e[4]-i)/n,(e[5]-s)/a,NaN,NaN,NaN,NaN,r,o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,(e[16]-i)/n,(e[17]-s)/a,NaN,NaN,NaN,NaN,(e[14]-i)/n,(e[15]-s)/a],0);return this.newFreeDrawOutline(d,t,this.#jn,this.#ia,this.#Xn,this.#Kn)}#ga(t,e){const i=this.#qn;t.set([NaN,NaN,NaN,NaN,i[4],i[5]],e);return e+6}#pa(t,e){const i=this.#Qn.subarray(4,6),s=this.#Qn.subarray(16,18),[n,a,r,o]=this.#jn,[l,h,d,c]=this.#la();t.set([NaN,NaN,NaN,NaN,(i[0]-n)/r,(i[1]-a)/o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,d,c,NaN,NaN,NaN,NaN,(s[0]-n)/r,(s[1]-a)/o],e);return e+24}}class FreeDrawOutline extends Outline{#jn;#fa=new Float32Array(4);#Xn;#Kn;#na;#ia;#ma;constructor(t,e,i,s,n,a){super();this.#ma=t;this.#na=e;this.#jn=i;this.#ia=s;this.#Xn=n;this.#Kn=a;this.lastPoint=[NaN,NaN];this.#ba(a);const[r,o,l,h]=this.#fa;for(let e=0,i=t.length;et[0]-e[0]||t[1]-e[1]||t[2]-e[2]));const t=[];for(const e of this.#wa)if(e[3]){t.push(...this.#va(e));this.#_a(e)}else{this.#xa(e);t.push(...this.#va(e))}return this.#Ea(t)}#Ea(t){const e=[],i=new Set;for(const i of t){const[t,s,n]=i;e.push([t,s,i],[t,n,i])}e.sort(((t,e)=>t[1]-e[1]||t[0]-e[0]));for(let t=0,s=e.length;t0;){const t=i.values().next().value;let[e,a,r,o,l]=t;i.delete(t);let h=e,d=a;n=[e,r];s.push(n);for(;;){let t;if(i.has(o))t=o;else{if(!i.has(l))break;t=l}i.delete(t);[e,a,r,o,l]=t;if(h!==e){n.push(h,d,e,d===a?a:r);h=e}d=d===a?r:a}n.push(h,d)}return new HighlightOutline(s,this.#jn,this.#Aa)}#Sa(t){const e=this.#ya;let i=0,s=e.length-1;for(;i<=s;){const n=i+s>>1,a=e[n][0];if(a===t)return n;a=0;s--){const[i,n]=this.#ya[s];if(i!==t)break;if(i===t&&n===e){this.#ya.splice(s,1);return}}}#va(t){const[e,i,s]=t,n=[[e,i,s]],a=this.#Sa(s);for(let t=0;t=i)if(o>s)n[t][1]=s;else{if(1===a)return[];n.splice(t,1);t--;a--}else{n[t][2]=i;o>s&&n.push([e,s,o])}}}return n}}class HighlightOutline extends Outline{#jn;#Ca;constructor(t,e,i){super();this.#Ca=t;this.#jn=e;this.lastPoint=i}toSVGPath(){const t=[];for(const e of this.#Ca){let[i,s]=e;t.push(`M${i} ${s}`);for(let n=2;n-1){this.#Ya=!0;this.#er(t);this.#ir()}else if(this.#$a){this.#za=t.anchorNode;this.#Ga=t.anchorOffset;this.#ja=t.focusNode;this.#qa=t.focusOffset;this.#sr();this.#ir();this.rotate(this.rotation)}}get telemetryInitialData(){return{action:"added",type:this.#Ya?"free_highlight":"highlight",color:this._uiManager.highlightColorNames.get(this.color),thickness:this.#sa,methodOfCreation:this.#tr}}get telemetryFinalData(){return{type:"highlight",color:this._uiManager.highlightColorNames.get(this.color)}}static computeTelemetryFinalData(t){return{numberOfColors:t.get("color").size}}#sr(){const t=new HighlightOutliner(this.#$a,.001);this.#Ka=t.getOutlines();[this.x,this.y,this.width,this.height]=this.#Ka.box;const e=new HighlightOutliner(this.#$a,.0025,.001,"ltr"===this._uiManager.direction);this.#Va=e.getOutlines();const{lastPoint:i}=this.#Va;this.#Aa=[(i[0]-this.x)/this.width,(i[1]-this.y)/this.height]}#er({highlightOutlines:t,highlightId:e,clipPathId:i}){this.#Ka=t;this.#Va=t.getNewOutline(this.#sa/2+1.5,.0025);if(e>=0){this.#y=e;this.#Wa=i;this.parent.drawLayer.finalizeDraw(e,{bbox:t.box,path:{d:t.toSVGPath()}});this.#Ja=this.parent.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:!0},bbox:this.#Va.box,path:{d:this.#Va.toSVGPath()}},!0)}else if(this.parent){const e=this.parent.viewport.rotation;this.parent.drawLayer.updateProperties(this.#y,{bbox:HighlightEditor.#nr(this.#Ka.box,(e-this.rotation+360)%360),path:{d:t.toSVGPath()}});this.parent.drawLayer.updateProperties(this.#Ja,{bbox:HighlightEditor.#nr(this.#Va.box,e),path:{d:this.#Va.toSVGPath()}})}const[s,n,a,r]=t.box;switch(this.rotation){case 0:this.x=s;this.y=n;this.width=a;this.height=r;break;case 90:{const[t,e]=this.parentDimensions;this.x=n;this.y=1-s;this.width=a*e/t;this.height=r*t/e;break}case 180:this.x=1-s;this.y=1-n;this.width=a;this.height=r;break;case 270:{const[t,e]=this.parentDimensions;this.x=1-n;this.y=s;this.width=a*e/t;this.height=r*t/e;break}}const{lastPoint:o}=this.#Va;this.#Aa=[(o[0]-s)/a,(o[1]-n)/r]}static initialize(t,e){AnnotationEditor.initialize(t,e);HighlightEditor._defaultColor||=e.highlightColors?.values().next().value||"#fff066"}static updateDefaultParams(t,e){switch(t){case p.HIGHLIGHT_DEFAULT_COLOR:HighlightEditor._defaultColor=e;break;case p.HIGHLIGHT_THICKNESS:HighlightEditor._defaultThickness=e}}translateInPage(t,e){}get toolbarPosition(){return this.#Aa}updateParams(t,e){switch(t){case p.HIGHLIGHT_COLOR:this.#Bn(e);break;case p.HIGHLIGHT_THICKNESS:this.#ar(e)}}static get defaultPropertiesToUpdate(){return[[p.HIGHLIGHT_DEFAULT_COLOR,HighlightEditor._defaultColor],[p.HIGHLIGHT_THICKNESS,HighlightEditor._defaultThickness]]}get propertiesToUpdate(){return[[p.HIGHLIGHT_COLOR,this.color||HighlightEditor._defaultColor],[p.HIGHLIGHT_THICKNESS,this.#sa||HighlightEditor._defaultThickness],[p.HIGHLIGHT_FREE,this.#Ya]]}#Bn(t){const setColorAndOpacity=(t,e)=>{this.color=t;this.#Qa=e;this.parent?.drawLayer.updateProperties(this.#y,{root:{fill:t,"fill-opacity":e}});this.#n?.updateColor(t)},e=this.color,i=this.#Qa;this.addCommands({cmd:setColorAndOpacity.bind(this,t,HighlightEditor._defaultOpacity),undo:setColorAndOpacity.bind(this,e,i),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.HIGHLIGHT_COLOR,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"color_changed",color:this._uiManager.highlightColorNames.get(t)},!0)}#ar(t){const e=this.#sa,setThickness=t=>{this.#sa=t;this.#rr(t)};this.addCommands({cmd:setThickness.bind(this,t),undo:setThickness.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.INK_THICKNESS,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"thickness_changed",thickness:t},!0)}async addEditToolbar(){const t=await super.addEditToolbar();if(!t)return null;if(this._uiManager.highlightColors){this.#n=new ColorPicker({editor:this});t.addColorPicker(this.#n)}return t}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}fixAndSetPosition(){return super.fixAndSetPosition(this.#or())}getBaseTranslation(){return[0,0]}getRect(t,e){return super.getRect(t,e,this.#or())}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);t&&this.div.focus()}remove(){this.#lr();this._reportTelemetry({action:"deleted"});super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#ir();this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t)this.#lr();else if(t){this.#ir(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);this.show(this._isVisible);e&&this.select()}#rr(t){if(!this.#Ya)return;this.#er({highlightOutlines:this.#Ka.getNewOutline(t/2)});this.fixAndSetPosition();const[e,i]=this.parentDimensions;this.setDims(this.width*e,this.height*i)}#lr(){if(null!==this.#y&&this.parent){this.parent.drawLayer.remove(this.#y);this.#y=null;this.parent.drawLayer.remove(this.#Ja);this.#Ja=null}}#ir(t=this.parent){if(null===this.#y){({id:this.#y,clipPathId:this.#Wa}=t.drawLayer.draw({bbox:this.#Ka.box,root:{viewBox:"0 0 1 1",fill:this.color,"fill-opacity":this.#Qa},rootClass:{highlight:!0,free:this.#Ya},path:{d:this.#Ka.toSVGPath()}},!1,!0));this.#Ja=t.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:this.#Ya},bbox:this.#Va.box,path:{d:this.#Va.toSVGPath()}},this.#Ya);this.#Xa&&(this.#Xa.style.clipPath=this.#Wa)}}static#nr([t,e,i,s],n){switch(n){case 90:return[1-e-s,t,s,i];case 180:return[1-t-i,1-e-s,i,s];case 270:return[e,1-t-i,s,i]}return[t,e,i,s]}rotate(t){const{drawLayer:e}=this.parent;let i;if(this.#Ya){t=(t-this.rotation+360)%360;i=HighlightEditor.#nr(this.#Ka.box,t)}else i=HighlightEditor.#nr([this.x,this.y,this.width,this.height],t);e.updateProperties(this.#y,{bbox:i,root:{"data-main-rotation":t}});e.updateProperties(this.#Ja,{bbox:HighlightEditor.#nr(this.#Va.box,t),root:{"data-main-rotation":t}})}render(){if(this.div)return this.div;const t=super.render();if(this.#Za){t.setAttribute("aria-label",this.#Za);t.setAttribute("role","mark")}this.#Ya?t.classList.add("free"):this.div.addEventListener("keydown",this.#hr.bind(this),{signal:this._uiManager._signal});const e=this.#Xa=document.createElement("div");t.append(e);e.setAttribute("aria-hidden","true");e.className="internal";e.style.clipPath=this.#Wa;const[i,s]=this.parentDimensions;this.setDims(this.width*i,this.height*s);bindEvents(this,this.#Xa,["pointerover","pointerleave"]);this.enableEditing();return t}pointerover(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#Ja,{rootClass:{hovered:!0}})}pointerleave(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#Ja,{rootClass:{hovered:!1}})}#hr(t){HighlightEditor._keyboardManager.exec(this,t)}_moveCaret(t){this.parent.unselect(this);switch(t){case 0:case 2:this.#dr(!0);break;case 1:case 3:this.#dr(!1)}}#dr(t){if(!this.#za)return;const e=window.getSelection();t?e.setPosition(this.#za,this.#Ga):e.setPosition(this.#ja,this.#qa)}select(){super.select();this.#Ja&&this.parent?.drawLayer.updateProperties(this.#Ja,{rootClass:{hovered:!1,selected:!0}})}unselect(){super.unselect();if(this.#Ja){this.parent?.drawLayer.updateProperties(this.#Ja,{rootClass:{selected:!1}});this.#Ya||this.#dr(!1)}}get _mustFixPosition(){return!this.#Ya}show(t=this._isVisible){super.show(t);if(this.parent){this.parent.drawLayer.updateProperties(this.#y,{rootClass:{hidden:!t}});this.parent.drawLayer.updateProperties(this.#Ja,{rootClass:{hidden:!t}})}}#or(){return this.#Ya?this.rotation:0}#cr(){if(this.#Ya)return null;const[t,e]=this.pageDimensions,[i,s]=this.pageTranslation,n=this.#$a,a=new Float32Array(8*n.length);let r=0;for(const{x:o,y:l,width:h,height:d}of n){const n=o*t+i,c=(1-l)*e+s;a[r]=a[r+4]=n;a[r+1]=a[r+3]=c;a[r+2]=a[r+6]=n+h*t;a[r+5]=a[r+7]=c-d*e;r+=8}return a}#ur(t){return this.#Ka.serialize(t,this.#or())}static startHighlighting(t,e,{target:i,x:s,y:n}){const{x:a,y:r,width:o,height:l}=i.getBoundingClientRect(),h=new AbortController,d=t.combinedSignal(h),pointerUpCallback=e=>{h.abort();this.#pr(t,e)};window.addEventListener("blur",pointerUpCallback,{signal:d});window.addEventListener("pointerup",pointerUpCallback,{signal:d});window.addEventListener("pointerdown",stopEvent,{capture:!0,passive:!1,signal:d});window.addEventListener("contextmenu",noContextMenu,{signal:d});i.addEventListener("pointermove",this.#gr.bind(this,t),{signal:d});this._freeHighlight=new FreeHighlightOutliner({x:s,y:n},[a,r,o,l],t.scale,this._defaultThickness/2,e,.001);({id:this._freeHighlightId,clipPathId:this._freeHighlightClipId}=t.drawLayer.draw({bbox:[0,0,1,1],root:{viewBox:"0 0 1 1",fill:this._defaultColor,"fill-opacity":this._defaultOpacity},rootClass:{highlight:!0,free:!0},path:{d:this._freeHighlight.toSVGPath()}},!0,!0))}static#gr(t,e){this._freeHighlight.add(e)&&t.drawLayer.updateProperties(this._freeHighlightId,{path:{d:this._freeHighlight.toSVGPath()}})}static#pr(t,e){this._freeHighlight.isEmpty()?t.drawLayer.remove(this._freeHighlightId):t.createAndAddNewEditor(e,!1,{highlightId:this._freeHighlightId,highlightOutlines:this._freeHighlight.getOutlines(),clipPathId:this._freeHighlightClipId,methodOfCreation:"main_toolbar"});this._freeHighlightId=-1;this._freeHighlight=null;this._freeHighlightClipId=""}static async deserialize(t,e,i){let s=null;if(t instanceof HighlightAnnotationElement){const{data:{quadPoints:e,rect:i,rotation:n,id:a,color:r,opacity:o,popupRef:l},parent:{page:{pageNumber:h}}}=t;s=t={annotationType:u.HIGHLIGHT,color:Array.from(r),opacity:o,quadPoints:e,boxes:null,pageIndex:h-1,rect:i.slice(0),rotation:n,id:a,deleted:!1,popupRef:l}}else if(t instanceof InkAnnotationElement){const{data:{inkLists:e,rect:i,rotation:n,id:a,color:r,borderStyle:{rawWidth:o},popupRef:l},parent:{page:{pageNumber:h}}}=t;s=t={annotationType:u.HIGHLIGHT,color:Array.from(r),thickness:o,inkLists:e,boxes:null,pageIndex:h-1,rect:i.slice(0),rotation:n,id:a,deleted:!1,popupRef:l}}const{color:n,quadPoints:a,inkLists:r,opacity:o}=t,l=await super.deserialize(t,e,i);l.color=Util.makeHexColor(...n);l.#Qa=o||1;r&&(l.#sa=t.thickness);l.annotationElementId=t.id||null;l._initialData=s;const[h,d]=l.pageDimensions,[c,p]=l.pageTranslation;if(a){const t=l.#$a=[];for(let e=0;et!==e[i]))}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}static canCreateNewEmptyEditor(){return!1}}class DrawingOptions{#fr=Object.create(null);updateProperty(t,e){this[t]=e;this.updateSVGProperty(t,e)}updateProperties(t){if(t)for(const[e,i]of Object.entries(t))e.startsWith("_")||this.updateProperty(e,i)}updateSVGProperty(t,e){this.#fr[t]=e}toSVGProperties(){const t=this.#fr;this.#fr=Object.create(null);return{root:t}}reset(){this.#fr=Object.create(null)}updateAll(t=this){this.updateProperties(t)}clone(){unreachable("Not implemented")}}class DrawingEditor extends AnnotationEditor{#mr=null;#br;_drawId=null;static _currentDrawId=-1;static _currentParent=null;static#Ar=null;static#wr=null;static#yr=null;static#vr=NaN;static#_r=null;static#xr=null;static#Er=NaN;static _INNER_MARGIN=3;constructor(t){super(t);this.#br=t.mustBeCommitted||!1;this._addOutlines(t)}_addOutlines(t){if(t.drawOutlines){this.#Sr(t);this.#ir()}}#Sr({drawOutlines:t,drawId:e,drawingOptions:i}){this.#mr=t;this._drawingOptions||=i;if(e>=0){this._drawId=e;this.parent.drawLayer.finalizeDraw(e,t.defaultProperties)}else this._drawId=this.#Cr(t,this.parent);this.#Tr(t.box)}#Cr(t,e){const{id:i}=e.drawLayer.draw(DrawingEditor._mergeSVGProperties(this._drawingOptions.toSVGProperties(),t.defaultSVGProperties),!1,!1);return i}static _mergeSVGProperties(t,e){const i=new Set(Object.keys(t));for(const[s,n]of Object.entries(e))i.has(s)?Object.assign(t[s],n):t[s]=n;return t}static getDefaultDrawingOptions(t){unreachable("Not implemented")}static get typesMap(){unreachable("Not implemented")}static get isDrawer(){return!0}static get supportMultipleDrawings(){return!1}static updateDefaultParams(t,e){const i=this.typesMap.get(t);i&&this._defaultDrawingOptions.updateProperty(i,e);if(this._currentParent){DrawingEditor.#Ar.updateProperty(i,e);this._currentParent.drawLayer.updateProperties(this._currentDrawId,this._defaultDrawingOptions.toSVGProperties())}}updateParams(t,e){const i=this.constructor.typesMap.get(t);i&&this._updateProperty(t,i,e)}static get defaultPropertiesToUpdate(){const t=[],e=this._defaultDrawingOptions;for(const[i,s]of this.typesMap)t.push([i,e[s]]);return t}get propertiesToUpdate(){const t=[],{_drawingOptions:e}=this;for(const[i,s]of this.constructor.typesMap)t.push([i,e[s]]);return t}_updateProperty(t,e,i){const s=this._drawingOptions,n=s[e],setter=t=>{s.updateProperty(e,t);const i=this.#mr.updateProperty(e,t);i&&this.#Tr(i);this.parent?.drawLayer.updateProperties(this._drawId,s.toSVGProperties())};this.addCommands({cmd:setter.bind(this,i),undo:setter.bind(this,n),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:t,overwriteIfSameType:!0,keepUndo:!0})}_onResizing(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#mr.getPathResizingSVGProperties(this.#Mr()),{bbox:this.#Dr()}))}_onResized(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#mr.getPathResizedSVGProperties(this.#Mr()),{bbox:this.#Dr()}))}_onTranslating(t,e){this.parent?.drawLayer.updateProperties(this._drawId,{bbox:this.#Dr()})}_onTranslated(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#mr.getPathTranslatedSVGProperties(this.#Mr(),this.parentDimensions),{bbox:this.#Dr()}))}_onStartDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!0}})}_onStopDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!1}})}commit(){super.commit();this.disableEditMode();this.disableEditing()}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}getBaseTranslation(){return[0,0]}get isResizable(){return!0}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);this._isDraggable=!0;if(this.#br){this.#br=!1;this.commit();this.parent.setSelected(this);t&&this.isOnScreen&&this.div.focus()}}remove(){this.#lr();super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#ir();this.#Tr(this.#mr.box);this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t){this._uiManager.removeShouldRescale(this);this.#lr()}else if(t){this._uiManager.addShouldRescale(this);this.#ir(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);e&&this.select()}#lr(){if(null!==this._drawId&&this.parent){this.parent.drawLayer.remove(this._drawId);this._drawId=null;this._drawingOptions.reset()}}#ir(t=this.parent){if(null===this._drawId||this.parent!==t)if(null===this._drawId){this._drawingOptions.updateAll();this._drawId=this.#Cr(this.#mr,t)}else this.parent.drawLayer.updateParent(this._drawId,t.drawLayer)}#Pr([t,e,i,s]){const{parentDimensions:[n,a],rotation:r}=this;switch(r){case 90:return[e,1-t,i*(a/n),s*(n/a)];case 180:return[1-t,1-e,i,s];case 270:return[1-e,t,i*(a/n),s*(n/a)];default:return[t,e,i,s]}}#Mr(){const{x:t,y:e,width:i,height:s,parentDimensions:[n,a],rotation:r}=this;switch(r){case 90:return[1-e,t,i*(n/a),s*(a/n)];case 180:return[1-t,1-e,i,s];case 270:return[e,1-t,i*(n/a),s*(a/n)];default:return[t,e,i,s]}}#Tr(t){[this.x,this.y,this.width,this.height]=this.#Pr(t);if(this.div){this.fixAndSetPosition();const[t,e]=this.parentDimensions;this.setDims(this.width*t,this.height*e)}this._onResized()}#Dr(){const{x:t,y:e,width:i,height:s,rotation:n,parentRotation:a,parentDimensions:[r,o]}=this;switch((4*n+a)/90){case 1:return[1-e-s,t,s,i];case 2:return[1-t-i,1-e-s,i,s];case 3:return[e,1-t-i,s,i];case 4:return[t,e-i*(r/o),s*(o/r),i*(r/o)];case 5:return[1-e,t,i*(r/o),s*(o/r)];case 6:return[1-t-s*(o/r),1-e,s*(o/r),i*(r/o)];case 7:return[e-i*(r/o),1-t-s*(o/r),i*(r/o),s*(o/r)];case 8:return[t-i,e-s,i,s];case 9:return[1-e,t-i,s,i];case 10:return[1-t,1-e,i,s];case 11:return[e-s,1-t,s,i];case 12:return[t-s*(o/r),e,s*(o/r),i*(r/o)];case 13:return[1-e-i*(r/o),t-s*(o/r),i*(r/o),s*(o/r)];case 14:return[1-t,1-e-i*(r/o),s*(o/r),i*(r/o)];case 15:return[e,1-t,i*(r/o),s*(o/r)];default:return[t,e,i,s]}}rotate(){this.parent&&this.parent.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties({bbox:this.#Dr()},this.#mr.updateRotation((this.parentRotation-this.rotation+360)%360)))}onScaleChanging(){this.parent&&this.#Tr(this.#mr.updateParentDimensions(this.parentDimensions,this.parent.scale))}static onScaleChangingWhenDrawing(){}render(){if(this.div)return this.div;let t,e;if(this._isCopy){t=this.x;e=this.y}const i=super.render();i.classList.add("draw");const s=document.createElement("div");i.append(s);s.setAttribute("aria-hidden","true");s.className="internal";const[n,a]=this.parentDimensions;this.setDims(this.width*n,this.height*a);this._uiManager.addShouldRescale(this);this.disableEditing();this._isCopy&&this._moveAfterPaste(t,e);return i}static createDrawerInstance(t,e,i,s,n){unreachable("Not implemented")}static startDrawing(t,e,i,s){const{target:n,offsetX:a,offsetY:r,pointerId:o,pointerType:l}=s;if(DrawingEditor.#_r&&DrawingEditor.#_r!==l)return;const{viewport:{rotation:h}}=t,{width:d,height:c}=n.getBoundingClientRect(),u=DrawingEditor.#wr=new AbortController,p=t.combinedSignal(u);DrawingEditor.#vr||=o;DrawingEditor.#_r??=l;window.addEventListener("pointerup",(t=>{DrawingEditor.#vr===t.pointerId?this._endDraw(t):DrawingEditor.#xr?.delete(t.pointerId)}),{signal:p});window.addEventListener("pointercancel",(t=>{DrawingEditor.#vr===t.pointerId?this._currentParent.endDrawingSession():DrawingEditor.#xr?.delete(t.pointerId)}),{signal:p});window.addEventListener("pointerdown",(t=>{if(DrawingEditor.#_r===t.pointerType){(DrawingEditor.#xr||=new Set).add(t.pointerId);if(DrawingEditor.#Ar.isCancellable()){DrawingEditor.#Ar.removeLastElement();DrawingEditor.#Ar.isEmpty()?this._currentParent.endDrawingSession(!0):this._endDraw(null)}}}),{capture:!0,passive:!1,signal:p});window.addEventListener("contextmenu",noContextMenu,{signal:p});n.addEventListener("pointermove",this._drawMove.bind(this),{signal:p});n.addEventListener("touchmove",(t=>{t.timeStamp===DrawingEditor.#Er&&stopEvent(t)}),{signal:p});t.toggleDrawing();e._editorUndoBar?.hide();if(DrawingEditor.#Ar)t.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#Ar.startNew(a,r,d,c,h));else{e.updateUIForDefaultProperties(this);DrawingEditor.#Ar=this.createDrawerInstance(a,r,d,c,h);DrawingEditor.#yr=this.getDefaultDrawingOptions();this._currentParent=t;({id:this._currentDrawId}=t.drawLayer.draw(this._mergeSVGProperties(DrawingEditor.#yr.toSVGProperties(),DrawingEditor.#Ar.defaultSVGProperties),!0,!1))}}static _drawMove(t){DrawingEditor.#Er=-1;if(!DrawingEditor.#Ar)return;const{offsetX:e,offsetY:i,pointerId:s}=t;if(DrawingEditor.#vr===s)if(DrawingEditor.#xr?.size>=1)this._endDraw(t);else{this._currentParent.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#Ar.add(e,i));DrawingEditor.#Er=t.timeStamp;stopEvent(t)}}static _cleanup(t){if(t){this._currentDrawId=-1;this._currentParent=null;DrawingEditor.#Ar=null;DrawingEditor.#yr=null;DrawingEditor.#_r=null;DrawingEditor.#Er=NaN}if(DrawingEditor.#wr){DrawingEditor.#wr.abort();DrawingEditor.#wr=null;DrawingEditor.#vr=NaN;DrawingEditor.#xr=null}}static _endDraw(t){const e=this._currentParent;if(e){e.toggleDrawing(!0);this._cleanup(!1);t?.target===e.div&&e.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#Ar.end(t.offsetX,t.offsetY));if(this.supportMultipleDrawings){const t=DrawingEditor.#Ar,i=this._currentDrawId,s=t.getLastElement();e.addCommands({cmd:()=>{e.drawLayer.updateProperties(i,t.setLastElement(s))},undo:()=>{e.drawLayer.updateProperties(i,t.removeLastElement())},mustExec:!1,type:p.DRAW_STEP})}else this.endDrawing(!1)}}static endDrawing(t){const e=this._currentParent;if(!e)return null;e.toggleDrawing(!0);e.cleanUndoStack(p.DRAW_STEP);if(!DrawingEditor.#Ar.isEmpty()){const{pageDimensions:[i,s],scale:n}=e,a=e.createAndAddNewEditor({offsetX:0,offsetY:0},!1,{drawId:this._currentDrawId,drawOutlines:DrawingEditor.#Ar.getOutlines(i*n,s*n,n,this._INNER_MARGIN),drawingOptions:DrawingEditor.#yr,mustBeCommitted:!t});this._cleanup(!0);return a}e.drawLayer.remove(this._currentDrawId);this._cleanup(!0);return null}createDrawingOptions(t){}static deserializeDraw(t,e,i,s,n,a){unreachable("Not implemented")}static async deserialize(t,e,i){const{rawDims:{pageWidth:s,pageHeight:n,pageX:a,pageY:r}}=e.viewport,o=this.deserializeDraw(a,r,s,n,this._INNER_MARGIN,t),l=await super.deserialize(t,e,i);l.createDrawingOptions(t);l.#Sr({drawOutlines:o});l.#ir();l.onScaleChanging();l.rotate();return l}serializeDraw(t){const[e,i]=this.pageTranslation,[s,n]=this.pageDimensions;return this.#mr.serialize([e,i,s,n],t)}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}static canCreateNewEmptyEditor(){return!1}}class InkDrawOutliner{#Qn=new Float64Array(6);#wn;#kr;#es;#sa;#na;#Ir="";#Rr=0;#Ca=new InkDrawOutline;#Fr;#Lr;constructor(t,e,i,s,n,a){this.#Fr=i;this.#Lr=s;this.#es=n;this.#sa=a;[t,e]=this.#Or(t,e);const r=this.#wn=[NaN,NaN,NaN,NaN,t,e];this.#na=[t,e];this.#kr=[{line:r,points:this.#na}];this.#Qn.set(r,0)}updateProperty(t,e){"stroke-width"===t&&(this.#sa=e)}#Or(t,e){return Outline._normalizePoint(t,e,this.#Fr,this.#Lr,this.#es)}isEmpty(){return!this.#kr||0===this.#kr.length}isCancellable(){return this.#na.length<=10}add(t,e){[t,e]=this.#Or(t,e);const[i,s,n,a]=this.#Qn.subarray(2,6),r=t-n,o=e-a;if(Math.hypot(this.#Fr*r,this.#Lr*o)<=2)return null;this.#na.push(t,e);if(isNaN(i)){this.#Qn.set([n,a,t,e],2);this.#wn.push(NaN,NaN,NaN,NaN,t,e);return{path:{d:this.toSVGPath()}}}isNaN(this.#Qn[0])&&this.#wn.splice(6,6);this.#Qn.set([i,s,n,a,t,e],0);this.#wn.push(...Outline.createBezierPoints(i,s,n,a,t,e));return{path:{d:this.toSVGPath()}}}end(t,e){const i=this.add(t,e);return i||(2===this.#na.length?{path:{d:this.toSVGPath()}}:null)}startNew(t,e,i,s,n){this.#Fr=i;this.#Lr=s;this.#es=n;[t,e]=this.#Or(t,e);const a=this.#wn=[NaN,NaN,NaN,NaN,t,e];this.#na=[t,e];const r=this.#kr.at(-1);if(r){r.line=new Float32Array(r.line);r.points=new Float32Array(r.points)}this.#kr.push({line:a,points:this.#na});this.#Qn.set(a,0);this.#Rr=0;this.toSVGPath();return null}getLastElement(){return this.#kr.at(-1)}setLastElement(t){if(!this.#kr)return this.#Ca.setLastElement(t);this.#kr.push(t);this.#wn=t.line;this.#na=t.points;this.#Rr=0;return{path:{d:this.toSVGPath()}}}removeLastElement(){if(!this.#kr)return this.#Ca.removeLastElement();this.#kr.pop();this.#Ir="";for(let t=0,e=this.#kr.length;tt??NaN)),d,c,u,p),points:g(r[t].map((t=>t??NaN)),d,c,u,p)});const f=new this.prototype.constructor;f.build(h,i,s,1,o,l,n);return f}#zr(t=this.#sa){const e=this.#Xn+t/2*this.#Br;return this.#es%180==0?[e/this.#Fr,e/this.#Lr]:[e/this.#Lr,e/this.#Fr]}#Ur(){const[t,e,i,s]=this.#fa,[n,a]=this.#zr(0);return[t+n,e+a,i-2*n,s-2*a]}#Hr(){const t=this.#fa=new Float32Array([1/0,1/0,-1/0,-1/0]);for(const{line:e}of this.#kr){if(e.length<=12){for(let i=4,s=e.length;it!==e[i]))||t.thickness!==i||t.opacity!==s||t.pageIndex!==n}renderAnnotationElement(t){const{points:e,rect:i}=this.serializeDraw(!1);t.updateEdited({rect:i,thickness:this._drawingOptions["stroke-width"],points:e});return null}}class ContourDrawOutline extends InkDrawOutline{toSVGPath(){let t=super.toSVGPath();t.endsWith("Z")||(t+="Z");return t}}class SignatureExtractor{static#Gr={maxDim:512,sigmaSFactor:.02,sigmaR:25,kernelSize:16};static#$r(t,e,i,s){s-=e;return 0===(i-=t)?s>0?0:4:1===i?s+6:2-s}static#Wr=new Int32Array([0,1,-1,1,-1,0,-1,-1,0,-1,1,-1,1,0,1,1]);static#Vr(t,e,i,s,n,a,r){const o=this.#$r(i,s,n,a);for(let n=0;n<8;n++){const a=(-n+o-r+16)%8;if(0!==t[(i+this.#Wr[2*a])*e+(s+this.#Wr[2*a+1])])return a}return-1}static#jr(t,e,i,s,n,a,r){const o=this.#$r(i,s,n,a);for(let n=0;n<8;n++){const a=(n+o+r+16)%8;if(0!==t[(i+this.#Wr[2*a])*e+(s+this.#Wr[2*a+1])])return a}return-1}static#qr(t,e,i,s){const n=t.length,a=new Int32Array(n);for(let e=0;e=1&&0===a[s+1])){1!==n&&(r=Math.abs(n));continue}o+=1;d+=1;n>1&&(r=n)}const c=[i,t],u=d===i+1,p={isHole:u,points:c,id:o,parent:0};l.push(p);let g;for(const t of l)if(t.id===r){g=t;break}g?g.isHole?p.parent=u?g.parent:r:p.parent=u?r:g.parent:p.parent=u?r:0;const f=this.#Vr(a,e,t,i,h,d,0);if(-1===f){a[s]=-o;1!==a[s]&&(r=Math.abs(a[s]));continue}let m=this.#Wr[2*f],b=this.#Wr[2*f+1];const A=t+m,w=i+b;h=A;d=w;let y=t,v=i;for(;;){const n=this.#jr(a,e,y,v,h,d,1);m=this.#Wr[2*n];b=this.#Wr[2*n+1];const l=y+m,u=v+b;c.push(u,l);const p=y*e+v;0===a[p+1]?a[p]=-o:1===a[p]&&(a[p]=o);if(l===t&&u===i&&y===A&&v===w){1!==a[s]&&(r=Math.abs(a[s]));break}h=y;d=v;y=l;v=u}}}return l}static#Xr(t,e,i,s){if(i-e<=4){for(let n=e;ny){v=s;y=e}}if(y>(l*w)**2){this.#Xr(t,e,v+2,s);this.#Xr(t,v,i,s)}else s.push(n,a)}static#Kr(t){const e=[],i=t.length;this.#Xr(t,0,i,e);e.push(t[i-2],t[i-1]);return e.length<=4?null:e}static#Yr(t,e,i,s,n,a){const r=new Float32Array(a**2),o=-2*s**2,l=a>>1;for(let t=0;t=i))for(let i=0;i=e)continue;const p=t[u*e+s],f=r[o*a+i]*h[Math.abs(p-d)];c+=p*f;g+=f}}p[u[o]=Math.round(c/g)]++}return[u,p]}static#Qr(t){const e=new Uint32Array(256);for(const i of t)e[i]++;return e}static#Jr(t){const e=t.length,i=new Uint8ClampedArray(e>>2);let s=-1/0,n=1/0;for(let e=0,a=i.length;es&&(s=a);a0!==t));let a=n,r=n;for(e=n;e<256;e++){const n=t[e];if(n>i){if(e-a>s){s=e-a;r=e-1}i=n;a=e}}for(e=r-1;e>=0&&!(t[e]>t[e+1]);e--);return e}static#to(t){const e=t,{width:i,height:s}=t,{maxDim:n}=this.#Gr;let a=i,r=s;if(i>n||s>n){let o=i,l=s,h=Math.log2(Math.max(i,s)/n);const d=Math.floor(h);h=h===d?d-1:d;for(let i=0;in&&(a=Math.ceil(a/2));r>n&&(r=Math.ceil(r/2));const i=new OffscreenCanvas(a,r);i.getContext("2d").drawImage(t,0,0,o,l,0,0,a,r);o=a;l=r;t!==e&&t.close();t=i.transferToImageBitmap()}const c=Math.min(n/a,n/r);a=Math.round(a*c);r=Math.round(r*c)}const o=new OffscreenCanvas(a,r).getContext("2d",{willReadFrequently:!0});o.filter="grayscale(1)";o.drawImage(t,0,0,t.width,t.height,0,0,a,r);const l=o.getImageData(0,0,a,r).data;return[this.#Jr(l),a,r]}static extractContoursFromText(t,{fontFamily:e,fontStyle:i,fontWeight:s},n,a,r,o){let l=new OffscreenCanvas(1,1),h=l.getContext("2d",{alpha:!1});const d=h.font=`${i} ${s} 200px ${e}`,{actualBoundingBoxLeft:c,actualBoundingBoxRight:u,actualBoundingBoxAscent:p,actualBoundingBoxDescent:g,fontBoundingBoxAscent:f,fontBoundingBoxDescent:m,width:b}=h.measureText(t),A=1.5,w=Math.ceil(Math.max(Math.abs(c)+Math.abs(u)||0,b)*A),y=Math.ceil(Math.max(Math.abs(p)+Math.abs(g)||200,Math.abs(f)+Math.abs(m)||200)*A);l=new OffscreenCanvas(w,y);h=l.getContext("2d",{alpha:!0,willReadFrequently:!0});h.font=d;h.filter="grayscale(1)";h.fillStyle="white";h.fillRect(0,0,w,y);h.fillStyle="black";h.fillText(t,.5*w/2,1.5*y/2);const v=this.#Jr(h.getImageData(0,0,w,y).data),_=this.#Qr(v),x=this.#Zr(_),E=this.#qr(v,w,y,x);return this.processDrawnLines({lines:{curves:E,width:w,height:y},pageWidth:n,pageHeight:a,rotation:r,innerMargin:o,mustSmooth:!0,areContours:!0})}static process(t,e,i,s,n){const[a,r,o]=this.#to(t),[l,h]=this.#Yr(a,r,o,Math.hypot(r,o)*this.#Gr.sigmaSFactor,this.#Gr.sigmaR,this.#Gr.kernelSize),d=this.#Zr(h),c=this.#qr(l,r,o,d);return this.processDrawnLines({lines:{curves:c,width:r,height:o},pageWidth:e,pageHeight:i,rotation:s,innerMargin:n,mustSmooth:!0,areContours:!0})}static processDrawnLines({lines:t,pageWidth:e,pageHeight:i,rotation:s,innerMargin:n,mustSmooth:a,areContours:r}){s%180!=0&&([e,i]=[i,e]);const{curves:o,width:l,height:h}=t,d=t.thickness??0,c=[],u=Math.min(e/l,i/h),p=u/e,g=u/i,f=[];for(const{points:t}of o){const e=a?this.#Kr(t):t;if(!e)continue;f.push(e);const i=e.length,s=new Float32Array(i),n=new Float32Array(3*(2===i?2:i-2));c.push({line:n,points:s});if(2===i){s[0]=e[0]*p;s[1]=e[1]*g;n.set([NaN,NaN,NaN,NaN,s[0],s[1]],0);continue}let[r,o,l,h]=e;r*=p;o*=g;l*=p;h*=g;s.set([r,o,l,h],0);n.set([NaN,NaN,NaN,NaN,r,o],0);for(let t=4;t=-128&&o<=127?Int8Array:r>=-32768&&o<=32767?Int16Array:Int32Array;const h=t.length,d=8+3*h,c=new Uint32Array(d);let u=0;c[u++]=d*Uint32Array.BYTES_PER_ELEMENT+(l-2*h)*a.BYTES_PER_ELEMENT;c[u++]=0;c[u++]=s;c[u++]=n;c[u++]=e?0:1;c[u++]=Math.max(0,Math.floor(i??0));c[u++]=h;c[u++]=a.BYTES_PER_ELEMENT;for(const e of t){c[u++]=e.length-2;c[u++]=e[0];c[u++]=e[1]}const p=new CompressionStream("deflate-raw"),g=p.writable.getWriter();await g.ready;g.write(c);const f=a.prototype.constructor;for(const e of t){const t=new f(e.length-2);for(let i=2,s=e.length;i{await n.ready;await n.close()})).catch((()=>{}));let a=null,r=0;for await(const t of i){a||=new Uint8Array(new Uint32Array(t.buffer,0,4)[0]);a.set(t,r);r+=t.length}const o=new Uint32Array(a.buffer,0,a.length>>2),l=o[1];if(0!==l)throw new Error(`Invalid version: ${l}`);const h=o[2],d=o[3],c=0===o[4],u=o[5],p=o[6],g=o[7],f=[],m=(8+3*p)*Uint32Array.BYTES_PER_ELEMENT;let b;switch(g){case Int8Array.BYTES_PER_ELEMENT:b=new Int8Array(a.buffer,m);break;case Int16Array.BYTES_PER_ELEMENT:b=new Int16Array(a.buffer,m);break;case Int32Array.BYTES_PER_ELEMENT:b=new Int32Array(a.buffer,m)}r=0;for(let t=0;t{e?.updateEditSignatureButton(t)}))}getSignaturePreview(){const{newCurves:t,areContours:e,thickness:i,width:s,height:n}=this.#so,a=Math.max(s,n);return{areContours:e,outline:SignatureExtractor.processDrawnLines({lines:{curves:t.map((t=>({points:t}))),thickness:i,width:s,height:n},pageWidth:a,pageHeight:a,rotation:0,innerMargin:0,mustSmooth:!1,areContours:e}).outline}}async addEditToolbar(){const t=await super.addEditToolbar();if(!t)return null;if(this._uiManager.signatureManager&&null!==this.#io){await t.addEditSignatureButton(this._uiManager.signatureManager,this.#no,this.#io);t.show()}return t}addSignature(t,e,i,s){const{x:n,y:a}=this,{outline:r}=this.#so=t;this.#eo=r instanceof ContourDrawOutline;this.#io=i;this.div.setAttribute("data-l10n-args",JSON.stringify({description:i}));let o;if(this.#eo)o=SignatureEditor.getDefaultDrawingOptions();else{o=SignatureEditor._defaultDrawnSignatureOptions.clone();o.updateProperties({"stroke-width":r.thickness})}this._addOutlines({drawOutlines:r,drawingOptions:o});const[l,h]=this.parentDimensions,[,d]=this.pageDimensions;let c=e/d;c=c>=1?.5:c;this.width*=c/this.height;if(this.width>=1){c*=.9/this.width;this.width=.9}this.height=c;this.setDims(l*this.width,h*this.height);this.x=n;this.y=a;this.center();this._onResized();this.onScaleChanging();this.rotate();this._uiManager.addToAnnotationStorage(this);this.setUuid(s);this._reportTelemetry({action:"pdfjs.signature.inserted",data:{hasBeenSaved:!!s,hasDescription:!!i}});this.div.hidden=!1}getFromImage(t){const{rawDims:{pageWidth:e,pageHeight:i},rotation:s}=this.parent.viewport;return SignatureExtractor.process(t,e,i,s,SignatureEditor._INNER_MARGIN)}getFromText(t,e){const{rawDims:{pageWidth:i,pageHeight:s},rotation:n}=this.parent.viewport;return SignatureExtractor.extractContoursFromText(t,e,i,s,n,SignatureEditor._INNER_MARGIN)}getDrawnSignature(t){const{rawDims:{pageWidth:e,pageHeight:i},rotation:s}=this.parent.viewport;return SignatureExtractor.processDrawnLines({lines:t,pageWidth:e,pageHeight:i,rotation:s,innerMargin:SignatureEditor._INNER_MARGIN,mustSmooth:!1,areContours:!1})}createDrawingOptions({areContours:t,thickness:e}){if(t)this._drawingOptions=SignatureEditor.getDefaultDrawingOptions();else{this._drawingOptions=SignatureEditor._defaultDrawnSignatureOptions.clone();this._drawingOptions.updateProperties({"stroke-width":e})}}serialize(t=!1){if(this.isEmpty())return null;const{lines:e,points:i,rect:s}=this.serializeDraw(t),{_drawingOptions:{"stroke-width":n}}=this,a={annotationType:u.SIGNATURE,isSignature:!0,areContours:this.#eo,color:[0,0,0],thickness:this.#eo?0:n,pageIndex:this.pageIndex,rect:s,rotation:this.rotation,structTreeParentId:this._structTreeParentId};if(t){a.paths={lines:e,points:i};a.uuid=this.#no;a.isCopy=!0}else a.lines=e;this.#io&&(a.accessibilityData={type:"Figure",alt:this.#io});return a}static deserializeDraw(t,e,i,s,n,a){return a.areContours?ContourDrawOutline.deserialize(t,e,i,s,n,a):InkDrawOutline.deserialize(t,e,i,s,n,a)}static async deserialize(t,e,i){const s=await super.deserialize(t,e,i);s.#eo=t.areContours;s.#io=t.accessibilityData?.alt||"";s.#no=t.uuid;return s}}class StampEditor extends AnnotationEditor{#ao=null;#ro=null;#oo=null;#lo=null;#ho=null;#do="";#co=null;#uo=!1;#po=null;#go=!1;#fo=!1;static _type="stamp";static _editorType=u.STAMP;constructor(t){super({...t,name:"stampEditor"});this.#lo=t.bitmapUrl;this.#ho=t.bitmapFile;this.defaultL10nId="pdfjs-editor-stamp-editor"}static initialize(t,e){AnnotationEditor.initialize(t,e)}static isHandlingMimeForPasting(t){return G.includes(t)}static paste(t,e){e.pasteEditor(u.STAMP,{bitmapFile:t.getAsFile()})}altTextFinish(){this._uiManager.useNewAltTextFlow&&(this.div.hidden=!1);super.altTextFinish()}get telemetryFinalData(){return{type:"stamp",hasAltText:!!this.altTextData?.altText}}static computeTelemetryFinalData(t){const e=t.get("hasAltText");return{hasAltText:e.get(!0)??0,hasNoAltText:e.get(!1)??0}}#mo(t,e=!1){if(t){this.#ao=t.bitmap;if(!e){this.#ro=t.id;this.#go=t.isSvg}t.file&&(this.#do=t.file.name);this.#bo()}else this.remove()}#Ao(){this.#oo=null;this._uiManager.enableWaiting(!1);if(this.#co)if(this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#ao){this._editToolbar.hide();this._uiManager.editAltText(this,!0)}else{if(!this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#ao){this._reportTelemetry({action:"pdfjs.image.image_added",data:{alt_text_modal:!1,alt_text_type:"empty"}});try{this.mlGuessAltText()}catch{}}this.div.focus()}}async mlGuessAltText(t=null,e=!0){if(this.hasAltTextData())return null;const{mlManager:i}=this._uiManager;if(!i)throw new Error("No ML.");if(!await i.isEnabledFor("altText"))throw new Error("ML isn't enabled for alt text.");const{data:s,width:n,height:a}=t||this.copyCanvas(null,null,!0).imageData,r=await i.guess({name:"altText",request:{data:s,width:n,height:a,channels:s.length/(n*a)}});if(!r)throw new Error("No response from the AI service.");if(r.error)throw new Error("Error from the AI service.");if(r.cancel)return null;if(!r.output)throw new Error("No valid response from the AI service.");const o=r.output;await this.setGuessedAltText(o);e&&!this.hasAltTextData()&&(this.altTextData={alt:o,decorative:!1});return o}#wo(){if(this.#ro){this._uiManager.enableWaiting(!0);this._uiManager.imageManager.getFromId(this.#ro).then((t=>this.#mo(t,!0))).finally((()=>this.#Ao()));return}if(this.#lo){const t=this.#lo;this.#lo=null;this._uiManager.enableWaiting(!0);this.#oo=this._uiManager.imageManager.getFromUrl(t).then((t=>this.#mo(t))).finally((()=>this.#Ao()));return}if(this.#ho){const t=this.#ho;this.#ho=null;this._uiManager.enableWaiting(!0);this.#oo=this._uiManager.imageManager.getFromFile(t).then((t=>this.#mo(t))).finally((()=>this.#Ao()));return}const t=document.createElement("input");t.type="file";t.accept=G.join(",");const e=this._uiManager._signal;this.#oo=new Promise((i=>{t.addEventListener("change",(async()=>{if(t.files&&0!==t.files.length){this._uiManager.enableWaiting(!0);const e=await this._uiManager.imageManager.getFromFile(t.files[0]);this._reportTelemetry({action:"pdfjs.image.image_selected",data:{alt_text_modal:this._uiManager.useNewAltTextFlow}});this.#mo(e)}else this.remove();i()}),{signal:e});t.addEventListener("cancel",(()=>{this.remove();i()}),{signal:e})})).finally((()=>this.#Ao()));t.click()}remove(){if(this.#ro){this.#ao=null;this._uiManager.imageManager.deleteId(this.#ro);this.#co?.remove();this.#co=null;if(this.#po){clearTimeout(this.#po);this.#po=null}}super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#ro&&null===this.#co&&this.#wo();this.isAttachedToDOM||this.parent.add(this)}}else this.#ro&&this.#wo()}onceAdded(t){this._isDraggable=!0;t&&this.div.focus()}isEmpty(){return!(this.#oo||this.#ao||this.#lo||this.#ho||this.#ro||this.#uo)}get isResizable(){return!0}render(){if(this.div)return this.div;let t,e;if(this._isCopy){t=this.x;e=this.y}super.render();this.div.hidden=!0;this.addAltTextButton();this.#uo||(this.#ao?this.#bo():this.#wo());this._isCopy&&this._moveAfterPaste(t,e);this._uiManager.addShouldRescale(this);return this.div}setCanvas(t,e){const{id:i,bitmap:s}=this._uiManager.imageManager.getFromCanvas(t,e);e.remove();if(i&&this._uiManager.imageManager.isValidId(i)){this.#ro=i;s&&(this.#ao=s);this.#uo=!1;this.#bo()}}_onResized(){this.onScaleChanging()}onScaleChanging(){if(!this.parent)return;null!==this.#po&&clearTimeout(this.#po);this.#po=setTimeout((()=>{this.#po=null;this.#yo()}),200)}#bo(){const{div:t}=this;let{width:e,height:i}=this.#ao;const[s,n]=this.pageDimensions,a=.75;if(this.width){e=this.width*s;i=this.height*n}else if(e>a*s||i>a*n){const t=Math.min(a*s/e,a*n/i);e*=t;i*=t}const[r,o]=this.parentDimensions;this.setDims(e*r/s,i*o/n);this._uiManager.enableWaiting(!1);const l=this.#co=document.createElement("canvas");l.setAttribute("role","img");this.addContainer(l);this.width=e/s;this.height=i/n;this._initialOptions?.isCentered?this.center():this.fixAndSetPosition();this._initialOptions=null;this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&!this.annotationElementId||(t.hidden=!1);this.#yo();if(!this.#fo){this.parent.addUndoableEditor(this);this.#fo=!0}this._reportTelemetry({action:"inserted_image"});this.#do&&this.div.setAttribute("aria-description",this.#do)}copyCanvas(t,e,i=!1){t||(t=224);const{width:s,height:n}=this.#ao,a=new OutputScale;let r=this.#ao,o=s,l=n,h=null;if(e){if(s>e||n>e){const t=Math.min(e/s,e/n);o=Math.floor(s*t);l=Math.floor(n*t)}h=document.createElement("canvas");const t=h.width=Math.ceil(o*a.sx),i=h.height=Math.ceil(l*a.sy);this.#go||(r=this.#vo(t,i));const d=h.getContext("2d");d.filter=this._uiManager.hcmFilter;let c="white",u="#cfcfd8";if("none"!==this._uiManager.hcmFilter)u="black";else if(window.matchMedia?.("(prefers-color-scheme: dark)").matches){c="#8f8f9d";u="#42414d"}const p=15,g=p*a.sx,f=p*a.sy,m=new OffscreenCanvas(2*g,2*f),b=m.getContext("2d");b.fillStyle=c;b.fillRect(0,0,2*g,2*f);b.fillStyle=u;b.fillRect(0,0,g,f);b.fillRect(g,f,g,f);d.fillStyle=d.createPattern(m,"repeat");d.fillRect(0,0,t,i);d.drawImage(r,0,0,r.width,r.height,0,0,t,i)}let d=null;if(i){let e,i;if(a.symmetric&&r.widtht||n>t){const a=Math.min(t/s,t/n);e=Math.floor(s*a);i=Math.floor(n*a);this.#go||(r=this.#vo(e,i))}}const o=new OffscreenCanvas(e,i).getContext("2d",{willReadFrequently:!0});o.drawImage(r,0,0,r.width,r.height,0,0,e,i);d={width:e,height:i,data:o.getImageData(0,0,e,i).data}}return{canvas:h,width:o,height:l,imageData:d}}#vo(t,e){const{width:i,height:s}=this.#ao;let n=i,a=s,r=this.#ao;for(;n>2*t||a>2*e;){const i=n,s=a;n>2*t&&(n=n>=16384?Math.floor(n/2)-1:Math.ceil(n/2));a>2*e&&(a=a>=16384?Math.floor(a/2)-1:Math.ceil(a/2));const o=new OffscreenCanvas(n,a);o.getContext("2d").drawImage(r,0,0,i,s,0,0,n,a);r=o.transferToImageBitmap()}return r}#yo(){const[t,e]=this.parentDimensions,{width:i,height:s}=this,n=new OutputScale,a=Math.ceil(i*t*n.sx),r=Math.ceil(s*e*n.sy),o=this.#co;if(!o||o.width===a&&o.height===r)return;o.width=a;o.height=r;const l=this.#go?this.#ao:this.#vo(a,r),h=o.getContext("2d");h.filter=this._uiManager.hcmFilter;h.drawImage(l,0,0,l.width,l.height,0,0,a,r)}#_o(t){if(t){if(this.#go){const t=this._uiManager.imageManager.getSvgUrl(this.#ro);if(t)return t}const t=document.createElement("canvas");({width:t.width,height:t.height}=this.#ao);t.getContext("2d").drawImage(this.#ao,0,0);return t.toDataURL()}if(this.#go){const[t,e]=this.pageDimensions,i=Math.round(this.width*t*PixelsPerInch.PDF_TO_CSS_UNITS),s=Math.round(this.height*e*PixelsPerInch.PDF_TO_CSS_UNITS),n=new OffscreenCanvas(i,s);n.getContext("2d").drawImage(this.#ao,0,0,this.#ao.width,this.#ao.height,0,0,i,s);return n.transferToImageBitmap()}return structuredClone(this.#ao)}static async deserialize(t,e,i){let s=null,n=!1;if(t instanceof StampAnnotationElement){const{data:{rect:a,rotation:r,id:o,structParent:l,popupRef:h},container:d,parent:{page:{pageNumber:c}},canvas:p}=t;let g,f;if(p){delete t.canvas;({id:g,bitmap:f}=i.imageManager.getFromCanvas(d.id,p));p.remove()}else{n=!0;t._hasNoCanvas=!0}const m=(await e._structTree.getAriaAttributes(`${U}${o}`))?.get("aria-label")||"";s=t={annotationType:u.STAMP,bitmapId:g,bitmap:f,pageIndex:c-1,rect:a.slice(0),rotation:r,id:o,deleted:!1,accessibilityData:{decorative:!1,altText:m},isSvg:!1,structParent:l,popupRef:h}}const a=await super.deserialize(t,e,i),{rect:r,bitmap:o,bitmapUrl:l,bitmapId:h,isSvg:d,accessibilityData:c}=t;if(n){i.addMissingCanvas(t.id,a);a.#uo=!0}else if(h&&i.imageManager.isValidId(h)){a.#ro=h;o&&(a.#ao=o)}else a.#lo=l;a.#go=d;const[p,g]=a.pageDimensions;a.width=(r[2]-r[0])/p;a.height=(r[3]-r[1])/g;a.annotationElementId=t.id||null;c&&(a.altTextData=c);a._initialData=s;a.#fo=!!s;return a}serialize(t=!1,e=null){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const i={annotationType:u.STAMP,bitmapId:this.#ro,pageIndex:this.pageIndex,rect:this.getRect(0,0),rotation:this.rotation,isSvg:this.#go,structTreeParentId:this._structTreeParentId};if(t){i.bitmapUrl=this.#_o(!0);i.accessibilityData=this.serializeAltText(!0);i.isCopy=!0;return i}const{decorative:s,altText:n}=this.serializeAltText(!1);!s&&n&&(i.accessibilityData={type:"Figure",alt:n});if(this.annotationElementId){const t=this.#Vn(i);if(t.isSame)return null;t.isSameAltText?delete i.accessibilityData:i.accessibilityData.structParent=this._initialData.structParent??-1}i.id=this.annotationElementId;if(null===e)return i;e.stamps||=new Map;const a=this.#go?(i.rect[2]-i.rect[0])*(i.rect[3]-i.rect[1]):null;if(e.stamps.has(this.#ro)){if(this.#go){const t=e.stamps.get(this.#ro);if(a>t.area){t.area=a;t.serialized.bitmap.close();t.serialized.bitmap=this.#_o(!1)}}}else{e.stamps.set(this.#ro,{area:a,serialized:i});i.bitmap=this.#_o(!1)}return i}#Vn(t){const{pageIndex:e,accessibilityData:{altText:i}}=this._initialData,s=t.pageIndex===e,n=(t.accessibilityData?.alt||"")===i;return{isSame:!this._hasBeenMoved&&!this._hasBeenResized&&s&&n,isSameAltText:n}}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}}class AnnotationEditorLayer{#Mn;#xo=!1;#Eo=null;#So=null;#Co=null;#To=new Map;#Mo=!1;#Do=!1;#Po=!1;#ko=null;#Io=null;#Ro=null;#Fo=null;#m;static _initialized=!1;static#G=new Map([FreeTextEditor,InkEditor,StampEditor,HighlightEditor,SignatureEditor].map((t=>[t._editorType,t])));constructor({uiManager:t,pageIndex:e,div:i,structTreeLayer:s,accessibilityManager:n,annotationLayer:a,drawLayer:r,textLayer:o,viewport:l,l10n:h}){const d=[...AnnotationEditorLayer.#G.values()];if(!AnnotationEditorLayer._initialized){AnnotationEditorLayer._initialized=!0;for(const e of d)e.initialize(h,t)}t.registerEditorTypes(d);this.#m=t;this.pageIndex=e;this.div=i;this.#Mn=n;this.#Eo=a;this.viewport=l;this.#Ro=o;this.drawLayer=r;this._structTree=s;this.#m.addLayer(this)}get isEmpty(){return 0===this.#To.size}get isInvisible(){return this.isEmpty&&this.#m.getMode()===u.NONE}updateToolbar(t){this.#m.updateToolbar(t)}updateMode(t=this.#m.getMode()){this.#Lo();switch(t){case u.NONE:this.disableTextSelection();this.togglePointerEvents(!1);this.toggleAnnotationLayerPointerEvents(!0);this.disableClick();return;case u.INK:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick();break;case u.HIGHLIGHT:this.enableTextSelection();this.togglePointerEvents(!1);this.disableClick();break;default:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick()}this.toggleAnnotationLayerPointerEvents(!1);const{classList:e}=this.div;for(const i of AnnotationEditorLayer.#G.values())e.toggle(`${i._type}Editing`,t===i._editorType);this.div.hidden=!1}hasTextLayer(t){return t===this.#Ro?.div}setEditingState(t){this.#m.setEditingState(t)}addCommands(t){this.#m.addCommands(t)}cleanUndoStack(t){this.#m.cleanUndoStack(t)}toggleDrawing(t=!1){this.div.classList.toggle("drawing",!t)}togglePointerEvents(t=!1){this.div.classList.toggle("disabled",!t)}toggleAnnotationLayerPointerEvents(t=!1){this.#Eo?.div.classList.toggle("disabled",!t)}async enable(){this.#Po=!0;this.div.tabIndex=0;this.togglePointerEvents(!0);const t=new Set;for(const e of this.#To.values()){e.enableEditing();e.show(!0);if(e.annotationElementId){this.#m.removeChangedExistingAnnotation(e);t.add(e.annotationElementId)}}if(!this.#Eo){this.#Po=!1;return}const e=this.#Eo.getEditableAnnotations();for(const i of e){i.hide();if(this.#m.isDeletedAnnotationElement(i.data.id))continue;if(t.has(i.data.id))continue;const e=await this.deserialize(i);if(e){this.addOrRebuild(e);e.enableEditing()}}this.#Po=!1}disable(){this.#Do=!0;this.div.tabIndex=-1;this.togglePointerEvents(!1);const t=new Map,e=new Map;for(const i of this.#To.values()){i.disableEditing();if(i.annotationElementId)if(null===i.serialize()){e.set(i.annotationElementId,i);this.getEditableAnnotation(i.annotationElementId)?.show();i.remove()}else t.set(i.annotationElementId,i)}if(this.#Eo){const i=this.#Eo.getEditableAnnotations();for(const s of i){const{id:i}=s.data;if(this.#m.isDeletedAnnotationElement(i))continue;let n=e.get(i);if(n){n.resetAnnotationElement(s);n.show(!1);s.show()}else{n=t.get(i);if(n){this.#m.addChangedExistingAnnotation(n);n.renderAnnotationElement(s)&&n.show(!1)}s.show()}}}this.#Lo();this.isEmpty&&(this.div.hidden=!0);const{classList:i}=this.div;for(const t of AnnotationEditorLayer.#G.values())i.remove(`${t._type}Editing`);this.disableTextSelection();this.toggleAnnotationLayerPointerEvents(!0);this.#Do=!1}getEditableAnnotation(t){return this.#Eo?.getEditableAnnotation(t)||null}setActiveEditor(t){this.#m.getActive()!==t&&this.#m.setActiveEditor(t)}enableTextSelection(){this.div.tabIndex=-1;if(this.#Ro?.div&&!this.#Fo){this.#Fo=new AbortController;const t=this.#m.combinedSignal(this.#Fo);this.#Ro.div.addEventListener("pointerdown",this.#Oo.bind(this),{signal:t});this.#Ro.div.classList.add("highlighting")}}disableTextSelection(){this.div.tabIndex=0;if(this.#Ro?.div&&this.#Fo){this.#Fo.abort();this.#Fo=null;this.#Ro.div.classList.remove("highlighting")}}#Oo(t){this.#m.unselectAll();const{target:e}=t;if(e===this.#Ro.div||("img"===e.getAttribute("role")||e.classList.contains("endOfContent"))&&this.#Ro.div.contains(e)){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;this.#m.showAllEditors("highlight",!0,!0);this.#Ro.div.classList.add("free");this.toggleDrawing();HighlightEditor.startHighlighting(this,"ltr"===this.#m.direction,{target:this.#Ro.div,x:t.x,y:t.y});this.#Ro.div.addEventListener("pointerup",(()=>{this.#Ro.div.classList.remove("free");this.toggleDrawing(!0)}),{once:!0,signal:this.#m._signal});t.preventDefault()}}enableClick(){if(this.#So)return;this.#So=new AbortController;const t=this.#m.combinedSignal(this.#So);this.div.addEventListener("pointerdown",this.pointerdown.bind(this),{signal:t});const e=this.pointerup.bind(this);this.div.addEventListener("pointerup",e,{signal:t});this.div.addEventListener("pointercancel",e,{signal:t})}disableClick(){this.#So?.abort();this.#So=null}attach(t){this.#To.set(t.id,t);const{annotationElementId:e}=t;e&&this.#m.isDeletedAnnotationElement(e)&&this.#m.removeDeletedAnnotationElement(t)}detach(t){this.#To.delete(t.id);this.#Mn?.removePointerInTextLayer(t.contentDiv);!this.#Do&&t.annotationElementId&&this.#m.addDeletedAnnotationElement(t)}remove(t){this.detach(t);this.#m.removeEditor(t);t.div.remove();t.isAttachedToDOM=!1}changeParent(t){if(t.parent!==this){if(t.parent&&t.annotationElementId){this.#m.addDeletedAnnotationElement(t.annotationElementId);AnnotationEditor.deleteAnnotationElement(t);t.annotationElementId=null}this.attach(t);t.parent?.detach(t);t.setParent(this);if(t.div&&t.isAttachedToDOM){t.div.remove();this.div.append(t.div)}}}add(t){if(t.parent!==this||!t.isAttachedToDOM){this.changeParent(t);this.#m.addEditor(t);this.attach(t);if(!t.isAttachedToDOM){const e=t.render();this.div.append(e);t.isAttachedToDOM=!0}t.fixAndSetPosition();t.onceAdded(!this.#Po);this.#m.addToAnnotationStorage(t);t._reportTelemetry(t.telemetryInitialData)}}moveEditorInDOM(t){if(!t.isAttachedToDOM)return;const{activeElement:e}=document;if(t.div.contains(e)&&!this.#Co){t._focusEventsAllowed=!1;this.#Co=setTimeout((()=>{this.#Co=null;if(t.div.contains(document.activeElement))t._focusEventsAllowed=!0;else{t.div.addEventListener("focusin",(()=>{t._focusEventsAllowed=!0}),{once:!0,signal:this.#m._signal});e.focus()}}),0)}t._structTreeParentId=this.#Mn?.moveElementInDOM(this.div,t.div,t.contentDiv,!0)}addOrRebuild(t){if(t.needsToBeRebuilt()){t.parent||=this;t.rebuild();t.show()}else this.add(t)}addUndoableEditor(t){this.addCommands({cmd:()=>t._uiManager.rebuild(t),undo:()=>{t.remove()},mustExec:!1})}getNextId(){return this.#m.getId()}get#No(){return AnnotationEditorLayer.#G.get(this.#m.getMode())}combinedSignal(t){return this.#m.combinedSignal(t)}#Bo(t){const e=this.#No;return e?new e.prototype.constructor(t):null}canCreateNewEmptyEditor(){return this.#No?.canCreateNewEmptyEditor()}async pasteEditor(t,e){this.#m.updateToolbar(t);await this.#m.updateMode(t);const{offsetX:i,offsetY:s}=this.#Ho(),n=this.getNextId(),a=this.#Bo({parent:this,id:n,x:i,y:s,uiManager:this.#m,isCentered:!0,...e});a&&this.add(a)}async deserialize(t){return await(AnnotationEditorLayer.#G.get(t.annotationType??t.annotationEditorType)?.deserialize(t,this,this.#m))||null}createAndAddNewEditor(t,e,i={}){const s=this.getNextId(),n=this.#Bo({parent:this,id:s,x:t.offsetX,y:t.offsetY,uiManager:this.#m,isCentered:e,...i});n&&this.add(n);return n}#Ho(){const{x:t,y:e,width:i,height:s}=this.div.getBoundingClientRect(),n=Math.max(0,t),a=Math.max(0,e),r=(n+Math.min(window.innerWidth,t+i))/2-t,o=(a+Math.min(window.innerHeight,e+s))/2-e,[l,h]=this.viewport.rotation%180==0?[r,o]:[o,r];return{offsetX:l,offsetY:h}}addNewEditor(t={}){this.createAndAddNewEditor(this.#Ho(),!0,t)}setSelected(t){this.#m.setSelected(t)}toggleSelected(t){this.#m.toggleSelected(t)}unselect(t){this.#m.unselect(t)}pointerup(t){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;if(!this.#Mo)return;this.#Mo=!1;if(this.#No?.isDrawer&&this.#No.supportMultipleDrawings)return;if(!this.#xo){this.#xo=!0;return}const i=this.#m.getMode();i!==u.STAMP&&i!==u.SIGNATURE?this.createAndAddNewEditor(t,!1):this.#m.unselectAll()}pointerdown(t){this.#m.getMode()===u.HIGHLIGHT&&this.enableTextSelection();if(this.#Mo){this.#Mo=!1;return}const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;this.#Mo=!0;if(this.#No?.isDrawer){this.startDrawingSession(t);return}const i=this.#m.getActive();this.#xo=!i||i.isEmpty()}startDrawingSession(t){this.div.focus({preventScroll:!0});if(this.#ko){this.#No.startDrawing(this,this.#m,!1,t);return}this.#m.setCurrentDrawingSession(this);this.#ko=new AbortController;const e=this.#m.combinedSignal(this.#ko);this.div.addEventListener("blur",(({relatedTarget:t})=>{if(t&&!this.div.contains(t)){this.#Io=null;this.commitOrRemove()}}),{signal:e});this.#No.startDrawing(this,this.#m,!1,t)}pause(t){if(t){const{activeElement:t}=document;this.div.contains(t)&&(this.#Io=t)}else this.#Io&&setTimeout((()=>{this.#Io?.focus();this.#Io=null}),0)}endDrawingSession(t=!1){if(!this.#ko)return null;this.#m.setCurrentDrawingSession(null);this.#ko.abort();this.#ko=null;this.#Io=null;return this.#No.endDrawing(t)}findNewParent(t,e,i){const s=this.#m.findParent(e,i);if(null===s||s===this)return!1;s.changeParent(t);return!0}commitOrRemove(){if(this.#ko){this.endDrawingSession();return!0}return!1}onScaleChanging(){this.#ko&&this.#No.onScaleChangingWhenDrawing(this)}destroy(){this.commitOrRemove();if(this.#m.getActive()?.parent===this){this.#m.commitOrRemove();this.#m.setActiveEditor(null)}if(this.#Co){clearTimeout(this.#Co);this.#Co=null}for(const t of this.#To.values()){this.#Mn?.removePointerInTextLayer(t.contentDiv);t.setParent(null);t.isAttachedToDOM=!1;t.div.remove()}this.div=null;this.#To.clear();this.#m.removeLayer(this)}#Lo(){for(const t of this.#To.values())t.isEmpty()&&t.remove()}render({viewport:t}){this.viewport=t;setLayerDimensions(this.div,t);for(const t of this.#m.getEditors(this.pageIndex)){this.add(t);t.rebuild()}this.updateMode()}update({viewport:t}){this.#m.commitOrRemove();this.#Lo();const e=this.viewport.rotation,i=t.rotation;this.viewport=t;setLayerDimensions(this.div,{rotation:i});if(e!==i)for(const t of this.#To.values())t.rotate(i)}get pageDimensions(){const{pageWidth:t,pageHeight:e}=this.viewport.rawDims;return[t,e]}get scale(){return this.#m.viewParameters.realScale}}class DrawLayer{#rn=null;#Uo=new Map;#zo=new Map;static#y=0;constructor({pageIndex:t}){this.pageIndex=t}setParent(t){if(this.#rn){if(this.#rn!==t){if(this.#Uo.size>0)for(const e of this.#Uo.values()){e.remove();t.append(e)}this.#rn=t}}else this.#rn=t}static get _svgFactory(){return shadow(this,"_svgFactory",new DOMSVGFactory)}static#Go(t,[e,i,s,n]){const{style:a}=t;a.top=100*i+"%";a.left=100*e+"%";a.width=100*s+"%";a.height=100*n+"%"}#$o(){const t=DrawLayer._svgFactory.create(1,1,!0);this.#rn.append(t);t.setAttribute("aria-hidden",!0);return t}#Wo(t,e){const i=DrawLayer._svgFactory.createElement("clipPath");t.append(i);const s=`clip_${e}`;i.setAttribute("id",s);i.setAttribute("clipPathUnits","objectBoundingBox");const n=DrawLayer._svgFactory.createElement("use");i.append(n);n.setAttribute("href",`#${e}`);n.classList.add("clip");return s}#Vo(t,e){for(const[i,s]of Object.entries(e))null===s?t.removeAttribute(i):t.setAttribute(i,s)}draw(t,e=!1,i=!1){const s=DrawLayer.#y++,n=this.#$o(),a=DrawLayer._svgFactory.createElement("defs");n.append(a);const r=DrawLayer._svgFactory.createElement("path");a.append(r);const o=`path_p${this.pageIndex}_${s}`;r.setAttribute("id",o);r.setAttribute("vector-effect","non-scaling-stroke");e&&this.#zo.set(s,r);const l=i?this.#Wo(a,o):null,h=DrawLayer._svgFactory.createElement("use");n.append(h);h.setAttribute("href",`#${o}`);this.updateProperties(n,t);this.#Uo.set(s,n);return{id:s,clipPathId:`url(#${l})`}}drawOutline(t,e){const i=DrawLayer.#y++,s=this.#$o(),n=DrawLayer._svgFactory.createElement("defs");s.append(n);const a=DrawLayer._svgFactory.createElement("path");n.append(a);const r=`path_p${this.pageIndex}_${i}`;a.setAttribute("id",r);a.setAttribute("vector-effect","non-scaling-stroke");let o;if(e){const t=DrawLayer._svgFactory.createElement("mask");n.append(t);o=`mask_p${this.pageIndex}_${i}`;t.setAttribute("id",o);t.setAttribute("maskUnits","objectBoundingBox");const e=DrawLayer._svgFactory.createElement("rect");t.append(e);e.setAttribute("width","1");e.setAttribute("height","1");e.setAttribute("fill","white");const s=DrawLayer._svgFactory.createElement("use");t.append(s);s.setAttribute("href",`#${r}`);s.setAttribute("stroke","none");s.setAttribute("fill","black");s.setAttribute("fill-rule","nonzero");s.classList.add("mask")}const l=DrawLayer._svgFactory.createElement("use");s.append(l);l.setAttribute("href",`#${r}`);o&&l.setAttribute("mask",`url(#${o})`);const h=l.cloneNode();s.append(h);l.classList.add("mainOutline");h.classList.add("secondaryOutline");this.updateProperties(s,t);this.#Uo.set(i,s);return i}finalizeDraw(t,e){this.#zo.delete(t);this.updateProperties(t,e)}updateProperties(t,e){if(!e)return;const{root:i,bbox:s,rootClass:n,path:a}=e,r="number"==typeof t?this.#Uo.get(t):t;if(r){i&&this.#Vo(r,i);s&&DrawLayer.#Go(r,s);if(n){const{classList:t}=r;for(const[e,i]of Object.entries(n))t.toggle(e,i)}if(a){const t=r.firstChild.firstChild;this.#Vo(t,a)}}}updateParent(t,e){if(e===this)return;const i=this.#Uo.get(t);if(i){e.#rn.append(i);this.#Uo.delete(t);e.#Uo.set(t,i)}}remove(t){this.#zo.delete(t);if(null!==this.#rn){this.#Uo.get(t).remove();this.#Uo.delete(t)}}destroy(){this.#rn=null;for(const t of this.#Uo.values())t.remove();this.#Uo.clear();this.#zo.clear()}}globalThis.pdfjsTestingUtils={HighlightOutliner};globalThis.pdfjsLib={AbortException,AnnotationEditorLayer,AnnotationEditorParamsType:p,AnnotationEditorType:u,AnnotationEditorUIManager,AnnotationLayer,AnnotationMode:c,AnnotationType:_,build:vt,ColorPicker,createValidAbsoluteUrl,DOMSVGFactory,DrawLayer,FeatureTest:util_FeatureTest,fetchData,getDocument,getFilenameFromUrl,getPdfFilenameFromUrl,getUuid,getXfaPageViewport,GlobalWorkerOptions,ImageKind:v,InvalidPDFException,isDataScheme,isPdfFile,isValidExplicitDest:At,MathClamp,noContextMenu,normalizeUnicode,OPS:D,OutputScale,PasswordResponses:F,PDFDataRangeTransport,PDFDateString,PDFWorker,PermissionFlag:g,PixelsPerInch,RenderingCancelledException,ResponseException,setLayerDimensions,shadow,SignatureExtractor,stopEvent,SupportedImageMimeTypes:G,TextLayer,TouchManager,updateUrlHash,Util,VerbosityLevel:M,version:yt,XfaLayer};export{AbortException,AnnotationEditorLayer,p as AnnotationEditorParamsType,u as AnnotationEditorType,AnnotationEditorUIManager,AnnotationLayer,c as AnnotationMode,_ as AnnotationType,ColorPicker,DOMSVGFactory,DrawLayer,util_FeatureTest as FeatureTest,GlobalWorkerOptions,v as ImageKind,InvalidPDFException,MathClamp,D as OPS,OutputScale,PDFDataRangeTransport,PDFDateString,PDFWorker,F as PasswordResponses,g as PermissionFlag,PixelsPerInch,RenderingCancelledException,ResponseException,SignatureExtractor,G as SupportedImageMimeTypes,TextLayer,TouchManager,Util,M as VerbosityLevel,XfaLayer,vt as build,createValidAbsoluteUrl,fetchData,getDocument,getFilenameFromUrl,getPdfFilenameFromUrl,getUuid,getXfaPageViewport,isDataScheme,isPdfFile,At as isValidExplicitDest,noContextMenu,normalizeUnicode,setLayerDimensions,shadow,stopEvent,updateUrlHash,yt as version}; \ No newline at end of file diff --git a/src/client/public/js/pdf.worker.min.mjs b/src/client/public/js/pdf.worker.min.mjs deleted file mode 100644 index f963616..0000000 --- a/src/client/public/js/pdf.worker.min.mjs +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @licstart The following is the entire license notice for the - * JavaScript code in this page - * - * Copyright 2024 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @licend The above is the entire license notice for the - * JavaScript code in this page - */ -const e=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),t=[.001,0,0,.001,0,0],a=1.35,r=.35,i=.25925925925925924,n=1,s=2,o=4,c=8,l=16,h=64,u=128,d=256,f="pdfjs_internal_editor_",g=3,p=9,m=13,b=15,y=101,w={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},S=0,x=4,k=1,C=2,v=3,F=1,O=2,T=3,M=4,D=5,R=6,N=7,E=8,L=9,_=10,j=11,U=12,X=13,q=14,H=15,W=16,z=17,$=20,G="Group",V="R",K=1,J=2,Y=4,Z=16,Q=32,ee=128,te=512,ae=1,re=2,ie=4096,ne=8192,se=32768,oe=65536,ce=131072,le=1048576,he=2097152,ue=8388608,de=16777216,fe=1,ge=2,pe=3,me=4,be=5,ye={E:"Mouse Enter",X:"Mouse Exit",D:"Mouse Down",U:"Mouse Up",Fo:"Focus",Bl:"Blur",PO:"PageOpen",PC:"PageClose",PV:"PageVisible",PI:"PageInvisible",K:"Keystroke",F:"Format",V:"Validate",C:"Calculate"},we={WC:"WillClose",WS:"WillSave",DS:"DidSave",WP:"WillPrint",DP:"DidPrint"},Se={O:"PageOpen",C:"PageClose"},xe=1,Ae=5,ke=1,Ce=2,ve=3,Fe=4,Ie=5,Oe=6,Te=7,Me=8,De=9,Be=10,Re=11,Ne=12,Ee=13,Pe=14,Le=15,_e=16,je=17,Ue=18,Xe=19,qe=20,He=21,We=22,ze=23,$e=24,Ge=25,Ve=26,Ke=27,Je=28,Ye=29,Ze=30,Qe=31,et=32,tt=33,at=34,rt=35,it=36,nt=37,st=38,ot=39,ct=40,lt=41,ht=42,ut=43,dt=44,ft=45,gt=46,pt=47,mt=48,bt=49,yt=50,wt=51,St=52,xt=53,At=54,kt=55,Ct=56,vt=57,Ft=58,It=59,Ot=60,Tt=61,Mt=62,Dt=63,Bt=64,Rt=65,Nt=66,Et=67,Pt=68,Lt=69,_t=70,jt=71,Ut=72,Xt=73,qt=74,Ht=75,Wt=76,zt=77,$t=80,Gt=81,Vt=83,Kt=84,Jt=85,Yt=86,Zt=87,Qt=88,ea=89,ta=90,aa=91,ra=92,ia=93,na=94,sa=0,oa=1,ca=2,la=3,ha=1,ua=2;let da=xe;function getVerbosityLevel(){return da}function info(e){da>=Ae&&console.log(`Info: ${e}`)}function warn(e){da>=xe&&console.log(`Warning: ${e}`)}function unreachable(e){throw new Error(e)}function assert(e,t){e||unreachable(t)}function createValidAbsoluteUrl(e,t=null,a=null){if(!e)return null;if(a&&"string"==typeof e){if(a.addDefaultProtocol&&e.startsWith("www.")){const t=e.match(/\./g);t?.length>=2&&(e=`http://${e}`)}if(a.tryConvertEncoding)try{e=stringToUTF8String(e)}catch{}}const r=t?URL.parse(e,t):URL.parse(e);return function _isValidProtocol(e){switch(e?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(r)?r:null}function shadow(e,t,a,r=!1){Object.defineProperty(e,t,{value:a,enumerable:!r,configurable:!0,writable:!1});return a}const fa=function BaseExceptionClosure(){function BaseException(e,t){this.message=e;this.name=t}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends fa{constructor(e,t){super(e,"PasswordException");this.code=t}}class UnknownErrorException extends fa{constructor(e,t){super(e,"UnknownErrorException");this.details=t}}class InvalidPDFException extends fa{constructor(e){super(e,"InvalidPDFException")}}class ResponseException extends fa{constructor(e,t,a){super(e,"ResponseException");this.status=t;this.missing=a}}class FormatError extends fa{constructor(e){super(e,"FormatError")}}class AbortException extends fa{constructor(e){super(e,"AbortException")}}function bytesToString(e){"object"==typeof e&&void 0!==e?.length||unreachable("Invalid argument for bytesToString");const t=e.length,a=8192;if(t>24&255,e>>16&255,e>>8&255,255&e)}function objectSize(e){return Object.keys(e).length}class FeatureTest{static get isLittleEndian(){return shadow(this,"isLittleEndian",function isLittleEndian(){const e=new Uint8Array(4);e[0]=1;return 1===new Uint32Array(e.buffer,0,1)[0]}())}static get isEvalSupported(){return shadow(this,"isEvalSupported",function isEvalSupported(){try{new Function("");return!0}catch{return!1}}())}static get isOffscreenCanvasSupported(){return shadow(this,"isOffscreenCanvasSupported","undefined"!=typeof OffscreenCanvas)}static get isImageDecoderSupported(){return shadow(this,"isImageDecoderSupported","undefined"!=typeof ImageDecoder)}static get platform(){if("undefined"!=typeof navigator&&"string"==typeof navigator?.platform&&"string"==typeof navigator?.userAgent){const{platform:e,userAgent:t}=navigator;return shadow(this,"platform",{isAndroid:t.includes("Android"),isLinux:e.includes("Linux"),isMac:e.includes("Mac"),isWindows:e.includes("Win"),isFirefox:t.includes("Firefox")})}return shadow(this,"platform",{isAndroid:!1,isLinux:!1,isMac:!1,isWindows:!1,isFirefox:!1})}static get isCSSRoundSupported(){return shadow(this,"isCSSRoundSupported",globalThis.CSS?.supports?.("width: round(1.5px, 1px)"))}}const ga=Array.from(Array(256).keys(),(e=>e.toString(16).padStart(2,"0")));class Util{static makeHexColor(e,t,a){return`#${ga[e]}${ga[t]}${ga[a]}`}static scaleMinMax(e,t){let a;if(e[0]){if(e[0]<0){a=t[0];t[0]=t[2];t[2]=a}t[0]*=e[0];t[2]*=e[0];if(e[3]<0){a=t[1];t[1]=t[3];t[3]=a}t[1]*=e[3];t[3]*=e[3]}else{a=t[0];t[0]=t[1];t[1]=a;a=t[2];t[2]=t[3];t[3]=a;if(e[1]<0){a=t[1];t[1]=t[3];t[3]=a}t[1]*=e[1];t[3]*=e[1];if(e[2]<0){a=t[0];t[0]=t[2];t[2]=a}t[0]*=e[2];t[2]*=e[2]}t[0]+=e[4];t[1]+=e[5];t[2]+=e[4];t[3]+=e[5]}static transform(e,t){return[e[0]*t[0]+e[2]*t[1],e[1]*t[0]+e[3]*t[1],e[0]*t[2]+e[2]*t[3],e[1]*t[2]+e[3]*t[3],e[0]*t[4]+e[2]*t[5]+e[4],e[1]*t[4]+e[3]*t[5]+e[5]]}static applyTransform(e,t,a=0){const r=e[a],i=e[a+1];e[a]=r*t[0]+i*t[2]+t[4];e[a+1]=r*t[1]+i*t[3]+t[5]}static applyTransformToBezier(e,t,a=0){const r=t[0],i=t[1],n=t[2],s=t[3],o=t[4],c=t[5];for(let t=0;t<6;t+=2){const l=e[a+t],h=e[a+t+1];e[a+t]=l*r+h*n+o;e[a+t+1]=l*i+h*s+c}}static applyInverseTransform(e,t){const a=e[0],r=e[1],i=t[0]*t[3]-t[1]*t[2];e[0]=(a*t[3]-r*t[2]+t[2]*t[5]-t[4]*t[3])/i;e[1]=(-a*t[1]+r*t[0]+t[4]*t[1]-t[5]*t[0])/i}static axialAlignedBoundingBox(e,t,a){const r=t[0],i=t[1],n=t[2],s=t[3],o=t[4],c=t[5],l=e[0],h=e[1],u=e[2],d=e[3];let f=r*l+o,g=f,p=r*u+o,m=p,b=s*h+c,y=b,w=s*d+c,S=w;if(0!==i||0!==n){const e=i*l,t=i*u,a=n*h,r=n*d;f+=a;m+=a;p+=r;g+=r;b+=e;S+=e;w+=t;y+=t}a[0]=Math.min(a[0],f,p,g,m);a[1]=Math.min(a[1],b,w,y,S);a[2]=Math.max(a[2],f,p,g,m);a[3]=Math.max(a[3],b,w,y,S)}static inverseTransform(e){const t=e[0]*e[3]-e[1]*e[2];return[e[3]/t,-e[1]/t,-e[2]/t,e[0]/t,(e[2]*e[5]-e[4]*e[3])/t,(e[4]*e[1]-e[5]*e[0])/t]}static singularValueDecompose2dScale(e,t){const a=e[0],r=e[1],i=e[2],n=e[3],s=a**2+r**2,o=a*i+r*n,c=i**2+n**2,l=(s+c)/2,h=Math.sqrt(l**2-(s*c-o**2));t[0]=Math.sqrt(l+h||1);t[1]=Math.sqrt(l-h||1)}static normalizeRect(e){const t=e.slice(0);if(e[0]>e[2]){t[0]=e[2];t[2]=e[0]}if(e[1]>e[3]){t[1]=e[3];t[3]=e[1]}return t}static intersect(e,t){const a=Math.max(Math.min(e[0],e[2]),Math.min(t[0],t[2])),r=Math.min(Math.max(e[0],e[2]),Math.max(t[0],t[2]));if(a>r)return null;const i=Math.max(Math.min(e[1],e[3]),Math.min(t[1],t[3])),n=Math.min(Math.max(e[1],e[3]),Math.max(t[1],t[3]));return i>n?null:[a,i,r,n]}static pointBoundingBox(e,t,a){a[0]=Math.min(a[0],e);a[1]=Math.min(a[1],t);a[2]=Math.max(a[2],e);a[3]=Math.max(a[3],t)}static rectBoundingBox(e,t,a,r,i){i[0]=Math.min(i[0],e,a);i[1]=Math.min(i[1],t,r);i[2]=Math.max(i[2],e,a);i[3]=Math.max(i[3],t,r)}static#e(e,t,a,r,i,n,s,o,c,l){if(c<=0||c>=1)return;const h=1-c,u=c*c,d=u*c,f=h*(h*(h*e+3*c*t)+3*u*a)+d*r,g=h*(h*(h*i+3*c*n)+3*u*s)+d*o;l[0]=Math.min(l[0],f);l[1]=Math.min(l[1],g);l[2]=Math.max(l[2],f);l[3]=Math.max(l[3],g)}static#t(e,t,a,r,i,n,s,o,c,l,h,u){if(Math.abs(c)<1e-12){Math.abs(l)>=1e-12&&this.#e(e,t,a,r,i,n,s,o,-h/l,u);return}const d=l**2-4*h*c;if(d<0)return;const f=Math.sqrt(d),g=2*c;this.#e(e,t,a,r,i,n,s,o,(-l+f)/g,u);this.#e(e,t,a,r,i,n,s,o,(-l-f)/g,u)}static bezierBoundingBox(e,t,a,r,i,n,s,o,c){c[0]=Math.min(c[0],e,s);c[1]=Math.min(c[1],t,o);c[2]=Math.max(c[2],e,s);c[3]=Math.max(c[3],t,o);this.#t(e,a,i,s,t,r,n,o,3*(3*(a-i)-e+s),6*(e-2*a+i),3*(a-e),c);this.#t(e,a,i,s,t,r,n,o,3*(3*(r-n)-t+o),6*(t-2*r+n),3*(r-t),c)}}const pa=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,728,711,710,729,733,731,730,732,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8226,8224,8225,8230,8212,8211,402,8260,8249,8250,8722,8240,8222,8220,8221,8216,8217,8218,8482,64257,64258,321,338,352,376,381,305,322,339,353,382,0,8364];function stringToPDFString(e){if(e[0]>="ï"){let t;if("þ"===e[0]&&"ÿ"===e[1]){t="utf-16be";e.length%2==1&&(e=e.slice(0,-1))}else if("ÿ"===e[0]&&"þ"===e[1]){t="utf-16le";e.length%2==1&&(e=e.slice(0,-1))}else"ï"===e[0]&&"»"===e[1]&&"¿"===e[2]&&(t="utf-8");if(t)try{const a=new TextDecoder(t,{fatal:!0}),r=stringToBytes(e),i=a.decode(r);return i.includes("")?i.replaceAll(/\x1b[^\x1b]*(?:\x1b|$)/g,""):i}catch(e){warn(`stringToPDFString: "${e}".`)}}const t=[];for(let a=0,r=e.length;aga[e])).join("")}"function"!=typeof Promise.try&&(Promise.try=function(e,...t){return new Promise((a=>{a(e(...t))}))});"function"!=typeof Math.sumPrecise&&(Math.sumPrecise=function(e){return e.reduce(((e,t)=>e+t),0)});const ya=Symbol("CIRCULAR_REF"),wa=Symbol("EOF");let Sa=Object.create(null),xa=Object.create(null),Aa=Object.create(null);class Name{constructor(e){this.name=e}static get(e){return xa[e]||=new Name(e)}}class Cmd{constructor(e){this.cmd=e}static get(e){return Sa[e]||=new Cmd(e)}}const ka=function nonSerializableClosure(){return ka};class Dict{constructor(e=null){this._map=new Map;this.xref=e;this.objId=null;this.suppressEncryption=!1;this.__nonSerializable__=ka}assignXref(e){this.xref=e}get size(){return this._map.size}get(e,t,a){let r=this._map.get(e);if(void 0===r&&void 0!==t){r=this._map.get(t);void 0===r&&void 0!==a&&(r=this._map.get(a))}return r instanceof Ref&&this.xref?this.xref.fetch(r,this.suppressEncryption):r}async getAsync(e,t,a){let r=this._map.get(e);if(void 0===r&&void 0!==t){r=this._map.get(t);void 0===r&&void 0!==a&&(r=this._map.get(a))}return r instanceof Ref&&this.xref?this.xref.fetchAsync(r,this.suppressEncryption):r}getArray(e,t,a){let r=this._map.get(e);if(void 0===r&&void 0!==t){r=this._map.get(t);void 0===r&&void 0!==a&&(r=this._map.get(a))}r instanceof Ref&&this.xref&&(r=this.xref.fetch(r,this.suppressEncryption));if(Array.isArray(r)){r=r.slice();for(let e=0,t=r.length;e{unreachable("Should not call `set` on the empty dictionary.")};return shadow(this,"empty",e)}static merge({xref:e,dictArray:t,mergeSubDicts:a=!1}){const r=new Dict(e),i=new Map;for(const e of t)if(e instanceof Dict)for(const[t,r]of e._map){let e=i.get(t);if(void 0===e){e=[];i.set(t,e)}else if(!(a&&r instanceof Dict))continue;e.push(r)}for(const[t,a]of i){if(1===a.length||!(a[0]instanceof Dict)){r._map.set(t,a[0]);continue}const i=new Dict(e);for(const e of a)for(const[t,a]of e._map)i._map.has(t)||i._map.set(t,a);i.size>0&&r._map.set(t,i)}i.clear();return r.size>0?r:Dict.empty}clone(){const e=new Dict(this.xref);for(const t of this.getKeys())e.set(t,this.getRaw(t));return e}delete(e){delete this._map[e]}}class Ref{constructor(e,t){this.num=e;this.gen=t}toString(){return 0===this.gen?`${this.num}R`:`${this.num}R${this.gen}`}static fromString(e){const t=Aa[e];if(t)return t;const a=/^(\d+)R(\d*)$/.exec(e);return a&&"0"!==a[1]?Aa[e]=new Ref(parseInt(a[1]),a[2]?parseInt(a[2]):0):null}static get(e,t){const a=0===t?`${e}R`:`${e}R${t}`;return Aa[a]||=new Ref(e,t)}}class RefSet{constructor(e=null){this._set=new Set(e?._set)}has(e){return this._set.has(e.toString())}put(e){this._set.add(e.toString())}remove(e){this._set.delete(e.toString())}[Symbol.iterator](){return this._set.values()}clear(){this._set.clear()}}class RefSetCache{constructor(){this._map=new Map}get size(){return this._map.size}get(e){return this._map.get(e.toString())}has(e){return this._map.has(e.toString())}put(e,t){this._map.set(e.toString(),t)}putAlias(e,t){this._map.set(e.toString(),this.get(t))}[Symbol.iterator](){return this._map.values()}clear(){this._map.clear()}*values(){yield*this._map.values()}*items(){for(const[e,t]of this._map)yield[Ref.fromString(e),t]}}function isName(e,t){return e instanceof Name&&(void 0===t||e.name===t)}function isCmd(e,t){return e instanceof Cmd&&(void 0===t||e.cmd===t)}function isDict(e,t){return e instanceof Dict&&(void 0===t||isName(e.get("Type"),t))}function isRefsEqual(e,t){return e.num===t.num&&e.gen===t.gen}class BaseStream{get length(){unreachable("Abstract getter `length` accessed")}get isEmpty(){unreachable("Abstract getter `isEmpty` accessed")}get isDataLoaded(){return shadow(this,"isDataLoaded",!0)}getByte(){unreachable("Abstract method `getByte` called")}getBytes(e){unreachable("Abstract method `getBytes` called")}async getImageData(e,t){return this.getBytes(e,t)}async asyncGetBytes(){unreachable("Abstract method `asyncGetBytes` called")}get isAsync(){return!1}get isAsyncDecoder(){return!1}get canAsyncDecodeImageFromBuffer(){return!1}async getTransferableImage(){return null}peekByte(){const e=this.getByte();-1!==e&&this.pos--;return e}peekBytes(e){const t=this.getBytes(e);this.pos-=t.length;return t}getUint16(){const e=this.getByte(),t=this.getByte();return-1===e||-1===t?-1:(e<<8)+t}getInt32(){return(this.getByte()<<24)+(this.getByte()<<16)+(this.getByte()<<8)+this.getByte()}getByteRange(e,t){unreachable("Abstract method `getByteRange` called")}getString(e){return bytesToString(this.getBytes(e))}skip(e){this.pos+=e||1}reset(){unreachable("Abstract method `reset` called")}moveStart(){unreachable("Abstract method `moveStart` called")}makeSubStream(e,t,a=null){unreachable("Abstract method `makeSubStream` called")}getBaseStreams(){return null}}const Ca=/^[1-9]\.\d$/,va=2**31-1,Fa=[1,0,0,1,0,0],Ia=["ColorSpace","ExtGState","Font","Pattern","Properties","Shading","XObject"],Oa=["ExtGState","Font","Properties","XObject"];function getLookupTableFactory(e){let t;return function(){if(e){t=Object.create(null);e(t);e=null}return t}}class MissingDataException extends fa{constructor(e,t){super(`Missing data [${e}, ${t})`,"MissingDataException");this.begin=e;this.end=t}}class ParserEOFException extends fa{constructor(e){super(e,"ParserEOFException")}}class XRefEntryException extends fa{constructor(e){super(e,"XRefEntryException")}}class XRefParseException extends fa{constructor(e){super(e,"XRefParseException")}}function arrayBuffersToBytes(e){const t=e.length;if(0===t)return new Uint8Array(0);if(1===t)return new Uint8Array(e[0]);let a=0;for(let r=0;r0,"The number should be a positive integer.");const a="M".repeat(e/1e3|0)+Ta[e%1e3/100|0]+Ta[10+(e%100/10|0)]+Ta[20+e%10];return t?a.toLowerCase():a}function log2(e){return e>0?Math.ceil(Math.log2(e)):0}function readInt8(e,t){return e[t]<<24>>24}function readInt16(e,t){return(e[t]<<24|e[t+1]<<16)>>16}function readUint16(e,t){return e[t]<<8|e[t+1]}function readUint32(e,t){return(e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3])>>>0}function isWhiteSpace(e){return 32===e||9===e||13===e||10===e}function isNumberArray(e,t){return Array.isArray(e)?(null===t||e.length===t)&&e.every((e=>"number"==typeof e)):ArrayBuffer.isView(e)&&!(e instanceof BigInt64Array||e instanceof BigUint64Array)&&(null===t||e.length===t)}function lookupMatrix(e,t){return isNumberArray(e,6)?e:t}function lookupRect(e,t){return isNumberArray(e,4)?e:t}function lookupNormalRect(e,t){return isNumberArray(e,4)?Util.normalizeRect(e):t}function parseXFAPath(e){const t=/(.+)\[(\d+)\]$/;return e.split(".").map((e=>{const a=e.match(t);return a?{name:a[1],pos:parseInt(a[2],10)}:{name:e,pos:0}}))}function escapePDFName(e){const t=[];let a=0;for(let r=0,i=e.length;r126||35===i||40===i||41===i||60===i||62===i||91===i||93===i||123===i||125===i||47===i||37===i){a"\n"===e?"\\n":"\r"===e?"\\r":`\\${e}`))}function _collectJS(e,t,a,r){if(!e)return;let i=null;if(e instanceof Ref){if(r.has(e))return;i=e;r.put(i);e=t.fetch(e)}if(Array.isArray(e))for(const i of e)_collectJS(i,t,a,r);else if(e instanceof Dict){if(isName(e.get("S"),"JavaScript")){const t=e.get("JS");let r;t instanceof BaseStream?r=t.getString():"string"==typeof t&&(r=t);r&&=stringToPDFString(r).replaceAll("\0","");r&&a.push(r)}_collectJS(e.getRaw("Next"),t,a,r)}i&&r.remove(i)}function collectActions(e,t,a){const r=Object.create(null),i=getInheritableProperty({dict:t,key:"AA",stopWhenFound:!1});if(i)for(let t=i.length-1;t>=0;t--){const n=i[t];if(n instanceof Dict)for(const t of n.getKeys()){const i=a[t];if(!i)continue;const s=[];_collectJS(n.getRaw(t),e,s,new RefSet);s.length>0&&(r[i]=s)}}if(t.has("A")){const a=[];_collectJS(t.get("A"),e,a,new RefSet);a.length>0&&(r.Action=a)}return objectSize(r)>0?r:null}const Ma={60:"<",62:">",38:"&",34:""",39:"'"};function*codePointIter(e){for(let t=0,a=e.length;t55295&&(a<57344||a>65533)&&t++;yield a}}function encodeToXmlString(e){const t=[];let a=0;for(let r=0,i=e.length;r55295&&(i<57344||i>65533)&&r++;a=r+1}}if(0===t.length)return e;a: ${e}.`);return!1}return!0}function validateCSSFont(e){const t=new Set(["100","200","300","400","500","600","700","800","900","1000","normal","bold","bolder","lighter"]),{fontFamily:a,fontWeight:r,italicAngle:i}=e;if(!validateFontName(a,!0))return!1;const n=r?r.toString():"";e.fontWeight=t.has(n)?n:"400";const s=parseFloat(i);e.italicAngle=isNaN(s)||s<-90||s>90?"14":i.toString();return!0}function recoverJsURL(e){const t=new RegExp("^\\s*("+["app.launchURL","window.open","xfa.host.gotoURL"].join("|").replaceAll(".","\\.")+")\\((?:'|\")([^'\"]*)(?:'|\")(?:,\\s*(\\w+)\\)|\\))","i").exec(e);return t?.[2]?{url:t[2],newWindow:"app.launchURL"===t[1]&&"true"===t[3]}:null}function numberToString(e){if(Number.isInteger(e))return e.toString();const t=Math.round(100*e);return t%100==0?(t/100).toString():t%10==0?e.toFixed(1):e.toFixed(2)}function getNewAnnotationsMap(e){if(!e)return null;const t=new Map;for(const[a,r]of e){if(!a.startsWith(f))continue;let e=t.get(r.pageIndex);if(!e){e=[];t.set(r.pageIndex,e)}e.push(r)}return t.size>0?t:null}function stringToAsciiOrUTF16BE(e){return function isAscii(e){return/^[\x00-\x7F]*$/.test(e)}(e)?e:stringToUTF16String(e,!0)}function stringToUTF16HexString(e){const t=[];for(let a=0,r=e.length;a>8&255],ga[255&r])}return t.join("")}function stringToUTF16String(e,t=!1){const a=[];t&&a.push("þÿ");for(let t=0,r=e.length;t>8&255),String.fromCharCode(255&r))}return a.join("")}function getRotationMatrix(e,t,a){switch(e){case 90:return[0,1,-1,0,t,0];case 180:return[-1,0,0,-1,t,a];case 270:return[0,-1,1,0,0,a];default:throw new Error("Invalid rotation")}}function getSizeInBytes(e){return Math.ceil(Math.ceil(Math.log2(1+e))/8)}class QCMS{static _module=null;static _mustAddAlpha=!1;static _destBuffer=null}let Da;const Ba="undefined"!=typeof TextDecoder?new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}):{decode:()=>{throw Error("TextDecoder not available")}};"undefined"!=typeof TextDecoder&&Ba.decode();let Ra=null;function getUint8ArrayMemory0(){null!==Ra&&0!==Ra.byteLength||(Ra=new Uint8Array(Da.memory.buffer));return Ra}let Na=0;function passArray8ToWasm0(e,t){const a=t(1*e.length,1)>>>0;getUint8ArrayMemory0().set(e,a/1);Na=e.length;return a}const Ea=Object.freeze({RGB8:0,0:"RGB8",RGBA8:1,1:"RGBA8",BGRA8:2,2:"BGRA8",Gray8:3,3:"Gray8",GrayA8:4,4:"GrayA8",CMYK:5,5:"CMYK"}),Pa=Object.freeze({Perceptual:0,0:"Perceptual",RelativeColorimetric:1,1:"RelativeColorimetric",Saturation:2,2:"Saturation",AbsoluteColorimetric:3,3:"AbsoluteColorimetric"});function __wbg_get_imports(){const e={wbg:{}};e.wbg.__wbg_copyresult_b08ee7d273f295dd=function(e,t){!function copy_result(e,t){const{_module:a,_mustAddAlpha:r,_destBuffer:i}=QCMS,n=new Uint8Array(a.memory.buffer,e,t);if(n.length!==i.length)if(r)for(let e=0,t=0,a=n.length;e>>0,t>>>0)};e.wbg.__wbg_copyrgb_d60ce17bb05d9b67=function(e){!function copy_rgb(e){QCMS._destBuffer.set(new Uint8Array(QCMS._module.memory.buffer,e,3))}(e>>>0)};e.wbg.__wbindgen_init_externref_table=function(){const e=Da.__wbindgen_export_0,t=e.grow(4);e.set(0,void 0);e.set(t+0,void 0);e.set(t+1,null);e.set(t+2,!0);e.set(t+3,!1)};e.wbg.__wbindgen_throw=function(e,t){throw new Error(function getStringFromWasm0(e,t){e>>>=0;return Ba.decode(getUint8ArrayMemory0().subarray(e,e+t))}(e,t))};return e}function __wbg_finalize_init(e,t){Da=e.exports;__wbg_init.__wbindgen_wasm_module=t;Ra=null;Da.__wbindgen_start();return Da}async function __wbg_init(e){if(void 0!==Da)return Da;void 0!==e&&(Object.getPrototypeOf(e)===Object.prototype?({module_or_path:e}=e):console.warn("using deprecated parameters for the initialization function; pass a single object instead"));void 0===e&&(e=new URL("qcms_bg.wasm",import.meta.url));const t=__wbg_get_imports();("string"==typeof e||"function"==typeof Request&&e instanceof Request||"function"==typeof URL&&e instanceof URL)&&(e=fetch(e));const{instance:a,module:r}=await async function __wbg_load(e,t){if("function"==typeof Response&&e instanceof Response){if("function"==typeof WebAssembly.instantiateStreaming)try{return await WebAssembly.instantiateStreaming(e,t)}catch(t){if("application/wasm"==e.headers.get("Content-Type"))throw t;console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",t)}const a=await e.arrayBuffer();return await WebAssembly.instantiate(a,t)}{const a=await WebAssembly.instantiate(e,t);return a instanceof WebAssembly.Instance?{instance:a,module:e}:a}}(await e,t);return __wbg_finalize_init(a,r)}class ColorSpace{constructor(e,t){this.name=e;this.numComps=t}getRgb(e,t){const a=new Uint8ClampedArray(3);this.getRgbItem(e,t,a,0);return a}getRgbItem(e,t,a,r){unreachable("Should not call ColorSpace.getRgbItem")}getRgbBuffer(e,t,a,r,i,n,s){unreachable("Should not call ColorSpace.getRgbBuffer")}getOutputLength(e,t){unreachable("Should not call ColorSpace.getOutputLength")}isPassthrough(e){return!1}isDefaultDecode(e,t){return ColorSpace.isDefaultDecode(e,this.numComps)}fillRgb(e,t,a,r,i,n,s,o,c){const l=t*a;let h=null;const u=1<u&&"DeviceGray"!==this.name&&"DeviceRGB"!==this.name){const t=s<=8?new Uint8Array(u):new Uint16Array(u);for(let e=0;e=.99554525?1:MathClamp(1.055*e**(1/2.4)-.055,0,1)}#p(e){return e<0?-this.#p(-e):e>8?((e+16)/116)**3:e*CalRGBCS.#h}#m(e,t,a){if(0===e[0]&&0===e[1]&&0===e[2]){a[0]=t[0];a[1]=t[1];a[2]=t[2];return}const r=this.#p(0),i=(1-r)/(1-this.#p(e[0])),n=1-i,s=(1-r)/(1-this.#p(e[1])),o=1-s,c=(1-r)/(1-this.#p(e[2])),l=1-c;a[0]=t[0]*i+n;a[1]=t[1]*s+o;a[2]=t[2]*c+l}#b(e,t,a){if(1===e[0]&&1===e[2]){a[0]=t[0];a[1]=t[1];a[2]=t[2];return}const r=a;this.#u(CalRGBCS.#r,t,r);const i=CalRGBCS.#o;this.#d(e,r,i);this.#u(CalRGBCS.#i,i,a)}#y(e,t,a){const r=a;this.#u(CalRGBCS.#r,t,r);const i=CalRGBCS.#o;this.#f(e,r,i);this.#u(CalRGBCS.#i,i,a)}#a(e,t,a,r,i){const n=MathClamp(e[t]*i,0,1),s=MathClamp(e[t+1]*i,0,1),o=MathClamp(e[t+2]*i,0,1),c=1===n?1:n**this.GR,l=1===s?1:s**this.GG,h=1===o?1:o**this.GB,u=this.MXA*c+this.MXB*l+this.MXC*h,d=this.MYA*c+this.MYB*l+this.MYC*h,f=this.MZA*c+this.MZB*l+this.MZC*h,g=CalRGBCS.#c;g[0]=u;g[1]=d;g[2]=f;const p=CalRGBCS.#l;this.#b(this.whitePoint,g,p);const m=CalRGBCS.#c;this.#m(this.blackPoint,p,m);const b=CalRGBCS.#l;this.#y(CalRGBCS.#s,m,b);const y=CalRGBCS.#c;this.#u(CalRGBCS.#n,b,y);a[r]=255*this.#g(y[0]);a[r+1]=255*this.#g(y[1]);a[r+2]=255*this.#g(y[2])}getRgbItem(e,t,a,r){this.#a(e,t,a,r,1)}getRgbBuffer(e,t,a,r,i,n,s){const o=1/((1<this.amax||this.bmin>this.bmax){info("Invalid Range, falling back to defaults");this.amin=-100;this.amax=100;this.bmin=-100;this.bmax=100}}#w(e){return e>=6/29?e**3:108/841*(e-4/29)}#S(e,t,a,r){return a+e*(r-a)/t}#a(e,t,a,r,i){let n=e[t],s=e[t+1],o=e[t+2];if(!1!==a){n=this.#S(n,a,0,100);s=this.#S(s,a,this.amin,this.amax);o=this.#S(o,a,this.bmin,this.bmax)}s>this.amax?s=this.amax:sthis.bmax?o=this.bmax:o{!function qcms_drop_transformer(e){Da.qcms_drop_transformer(e)}(e)}));constructor(e,t,a){if(!IccColorSpace.isUsable)throw new Error("No ICC color space support");super(t,a);let r;switch(a){case 1:r=Ea.Gray8;this.#A=(e,t)=>function qcms_convert_one(e,t){Da.qcms_convert_one(e,t)}(this.#x,255*e[t]);break;case 3:r=Ea.RGB8;this.#A=(e,t)=>function qcms_convert_three(e,t,a,r){Da.qcms_convert_three(e,t,a,r)}(this.#x,255*e[t],255*e[t+1],255*e[t+2]);break;case 4:r=Ea.CMYK;this.#A=(e,t)=>function qcms_convert_four(e,t,a,r,i){Da.qcms_convert_four(e,t,a,r,i)}(this.#x,255*e[t],255*e[t+1],255*e[t+2],255*e[t+3]);break;default:throw new Error(`Unsupported number of components: ${a}`)}this.#x=function qcms_transformer_from_memory(e,t,a){const r=passArray8ToWasm0(e,Da.__wbindgen_malloc),i=Na;return Da.qcms_transformer_from_memory(r,i,t,a)>>>0}(e,r,Pa.Perceptual);if(!this.#x)throw new Error("Failed to create ICC color space");IccColorSpace.#v.register(this,this.#x)}getRgbItem(e,t,a,r){QCMS._destBuffer=a.subarray(r,r+3);this.#A(e,t);QCMS._destBuffer=null}getRgbBuffer(e,t,a,r,i,n,s){e=e.subarray(t,t+a*this.numComps);if(8!==n){const t=255/((1<=this.end?-1:this.bytes[this.pos++]}getBytes(e){const t=this.bytes,a=this.pos,r=this.end;if(!e)return t.subarray(a,r);let i=a+e;i>r&&(i=r);this.pos=i;return t.subarray(a,i)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);return this.bytes.subarray(e,t)}reset(){this.pos=this.start}moveStart(){this.start=this.pos}makeSubStream(e,t,a=null){return new Stream(this.bytes.buffer,e,t,a)}}class StringStream extends Stream{constructor(e){super(stringToBytes(e))}}class NullStream extends Stream{constructor(){super(new Uint8Array(0))}}class ChunkedStream extends Stream{constructor(e,t,a){super(new Uint8Array(e),0,e,null);this.chunkSize=t;this._loadedChunks=new Set;this.numChunks=Math.ceil(e/t);this.manager=a;this.progressiveDataLength=0;this.lastSuccessfulEnsureByteChunk=-1}getMissingChunks(){const e=[];for(let t=0,a=this.numChunks;t=this.end?this.numChunks:Math.floor(t/this.chunkSize);for(let e=a;ethis.numChunks)&&t!==this.lastSuccessfulEnsureByteChunk){if(!this._loadedChunks.has(t))throw new MissingDataException(e,e+1);this.lastSuccessfulEnsureByteChunk=t}}ensureRange(e,t){if(e>=t)return;if(t<=this.progressiveDataLength)return;const a=Math.floor(e/this.chunkSize);if(a>this.numChunks)return;const r=Math.min(Math.floor((t-1)/this.chunkSize)+1,this.numChunks);for(let i=a;i=this.end)return-1;e>=this.progressiveDataLength&&this.ensureByte(e);return this.bytes[this.pos++]}getBytes(e){const t=this.bytes,a=this.pos,r=this.end;if(!e){r>this.progressiveDataLength&&this.ensureRange(a,r);return t.subarray(a,r)}let i=a+e;i>r&&(i=r);i>this.progressiveDataLength&&this.ensureRange(a,i);this.pos=i;return t.subarray(a,i)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);t>this.progressiveDataLength&&this.ensureRange(e,t);return this.bytes.subarray(e,t)}makeSubStream(e,t,a=null){t?e+t>this.progressiveDataLength&&this.ensureRange(e,e+t):e>=this.progressiveDataLength&&this.ensureByte(e);function ChunkedStreamSubstream(){}ChunkedStreamSubstream.prototype=Object.create(this);ChunkedStreamSubstream.prototype.getMissingChunks=function(){const e=this.chunkSize,t=Math.floor(this.start/e),a=Math.floor((this.end-1)/e)+1,r=[];for(let e=t;e{const readChunk=({value:n,done:s})=>{try{if(s){const t=arrayBuffersToBytes(r);r=null;e(t);return}i+=n.byteLength;a.isStreamingSupported&&this.onProgress({loaded:i});r.push(n);a.read().then(readChunk,t)}catch(e){t(e)}};a.read().then(readChunk,t)})).then((t=>{this.aborted||this.onReceiveData({chunk:t,begin:e})}))}requestAllChunks(e=!1){if(!e){const e=this.stream.getMissingChunks();this._requestChunks(e)}return this._loadedStreamCapability.promise}_requestChunks(e){const t=this.currRequestId++,a=new Set;this._chunksNeededByRequest.set(t,a);for(const t of e)this.stream.hasChunk(t)||a.add(t);if(0===a.size)return Promise.resolve();const r=Promise.withResolvers();this._promisesByRequest.set(t,r);const i=[];for(const e of a){let a=this._requestsByChunk.get(e);if(!a){a=[];this._requestsByChunk.set(e,a);i.push(e)}a.push(t)}if(i.length>0){const e=this.groupChunks(i);for(const t of e){const e=t.beginChunk*this.chunkSize,a=Math.min(t.endChunk*this.chunkSize,this.length);this.sendRequest(e,a).catch(r.reject)}}return r.promise.catch((e=>{if(!this.aborted)throw e}))}getStream(){return this.stream}requestRange(e,t){t=Math.min(t,this.length);const a=this.getBeginChunk(e),r=this.getEndChunk(t),i=[];for(let e=a;ee-t));return this._requestChunks(t)}groupChunks(e){const t=[];let a=-1,r=-1;for(let i=0,n=e.length;i=0&&r+1!==n){t.push({beginChunk:a,endChunk:r+1});a=n}i+1===e.length&&t.push({beginChunk:a,endChunk:n+1});r=n}return t}onProgress(e){this.msgHandler.send("DocProgress",{loaded:this.stream.numChunksLoaded*this.chunkSize+e.loaded,total:this.length})}onReceiveData(e){const t=e.chunk,a=void 0===e.begin,r=a?this.progressiveDataLength:e.begin,i=r+t.byteLength,n=Math.floor(r/this.chunkSize),s=i0||o.push(a)}}}if(!this.disableAutoFetch&&0===this._requestsByChunk.size){let e;if(1===this.stream.numChunksLoaded){const t=this.stream.numChunks-1;this.stream.hasChunk(t)||(e=t)}else e=this.stream.nextEmptyChunk(s);Number.isInteger(e)&&this._requestChunks([e])}for(const e of o){const t=this._promisesByRequest.get(e);this._promisesByRequest.delete(e);t.resolve()}this.msgHandler.send("DocProgress",{loaded:this.stream.numChunksLoaded*this.chunkSize,total:this.length})}onError(e){this._loadedStreamCapability.reject(e)}getBeginChunk(e){return Math.floor(e/this.chunkSize)}getEndChunk(e){return Math.floor((e-1)/this.chunkSize)+1}abort(e){this.aborted=!0;this.pdfNetworkStream?.cancelAllRequests(e);for(const t of this._promisesByRequest.values())t.reject(e)}}function convertToRGBA(e){switch(e.kind){case k:return convertBlackAndWhiteToRGBA(e);case C:return function convertRGBToRGBA({src:e,srcPos:t=0,dest:a,destPos:r=0,width:i,height:n}){let s=0;const o=i*n*3,c=o>>2,l=new Uint32Array(e.buffer,t,c);if(FeatureTest.isLittleEndian){for(;s>>24|t<<8|4278190080;a[r+2]=t>>>16|i<<16|4278190080;a[r+3]=i>>>8|4278190080}for(let i=4*s,n=t+o;i>>8|255;a[r+2]=t<<16|i>>>16|255;a[r+3]=i<<8|255}for(let i=4*s,n=t+o;i>3,u=7&r,d=e.length;a=new Uint32Array(a.buffer);let f=0;for(let r=0;ra||t>a)return!0;const r=e*t;if(this._hasMaxArea)return r>this.MAX_AREA;if(r(this.MAX_AREA=this.#I**2)}static get MAX_DIM(){return shadow(this,"MAX_DIM",this._guessMax(2048,65537,0,1))}static get MAX_AREA(){this._hasMaxArea=!0;return shadow(this,"MAX_AREA",this._guessMax(this.#I,this.MAX_DIM,128,0)**2)}static set MAX_AREA(e){if(e>=0){this._hasMaxArea=!0;shadow(this,"MAX_AREA",e)}}static setOptions({canvasMaxAreaInBytes:e=-1,isImageDecoderSupported:t=!1}){this._hasMaxArea||(this.MAX_AREA=e>>2);this.#O=t}static _areGoodDims(e,t){try{const a=new OffscreenCanvas(e,t),r=a.getContext("2d");r.fillRect(0,0,1,1);const i=r.getImageData(0,0,1,1).data[3];a.width=a.height=1;return 0!==i}catch{return!1}}static _guessMax(e,t,a,r){for(;e+a+1va){const e=this.#T();if(e)return e}const r=this._encodeBMP();let i,n;if(await ImageResizer.canUseImageDecoder){i=new ImageDecoder({data:r,type:"image/bmp",preferAnimation:!1,transfer:[r.buffer]});n=i.decode().catch((e=>{warn(`BMP image decoding failed: ${e}`);return createImageBitmap(new Blob([this._encodeBMP().buffer],{type:"image/bmp"}))})).finally((()=>{i.close()}))}else n=createImageBitmap(new Blob([r.buffer],{type:"image/bmp"}));const{MAX_AREA:s,MAX_DIM:o}=ImageResizer,c=Math.max(t/o,a/o,Math.sqrt(t*a/s)),l=Math.max(c,2),h=Math.round(10*(c+1.25))/10/l,u=Math.floor(Math.log2(h)),d=new Array(u+2).fill(2);d[0]=l;d.splice(-1,1,h/(1<>s,c=r>>s;let l,h=r;try{l=new Uint8Array(n)}catch{let e=Math.floor(Math.log2(n+1));for(;;)try{l=new Uint8Array(2**e-1);break}catch{e-=1}h=Math.floor((2**e-1)/(4*a));const t=a*h*4;t>s;e>3,s=a+3&-4;if(a!==s){const e=new Uint8Array(s*t);let r=0;for(let n=0,o=t*a;ni&&(r=i)}else{for(;!this.eof;)this.readBlock(t);r=this.bufferLength}this.pos=r;return this.buffer.subarray(a,r)}async getImageData(e,t){if(!this.canAsyncDecodeImageFromBuffer)return this.isAsyncDecoder?this.decodeImage(null,t):this.getBytes(e,t);const a=await this.stream.asyncGetBytes();return this.decodeImage(a,t)}reset(){this.pos=0}makeSubStream(e,t,a=null){if(void 0===t)for(;!this.eof;)this.readBlock();else{const a=e+t;for(;this.bufferLength<=a&&!this.eof;)this.readBlock()}return new Stream(this.buffer,e,t,a)}getBaseStreams(){return this.str?this.str.getBaseStreams():null}}class StreamsSequenceStream extends DecodeStream{constructor(e,t=null){e=e.filter((e=>e instanceof BaseStream));let a=0;for(const t of e)a+=t instanceof DecodeStream?t._rawMinBufferLength:t.length;super(a);this.streams=e;this._onError=t}readBlock(){const e=this.streams;if(0===e.length){this.eof=!0;return}const t=e.shift();let a;try{a=t.getBytes()}catch(e){if(this._onError){this._onError(e,t.dict?.objId);return}throw e}const r=this.bufferLength,i=r+a.length;this.ensureBuffer(i).set(a,r);this.bufferLength=i}getBaseStreams(){const e=[];for(const t of this.streams){const a=t.getBaseStreams();a&&e.push(...a)}return e.length>0?e:null}}class ColorSpaceUtils{static parse({cs:e,xref:t,resources:a=null,pdfFunctionFactory:r,globalColorSpaceCache:i,localColorSpaceCache:n,asyncIfNotCached:s=!1}){const o={xref:t,resources:a,pdfFunctionFactory:r,globalColorSpaceCache:i,localColorSpaceCache:n};let c,l,h;if(e instanceof Ref){l=e;const a=i.getByRef(l)||n.getByRef(l);if(a)return a;e=t.fetch(e)}if(e instanceof Name){c=e.name;const t=n.getByName(c);if(t)return t}try{h=this.#M(e,o)}catch(e){if(s&&!(e instanceof MissingDataException))return Promise.reject(e);throw e}if(c||l){n.set(c,l,h);l&&i.set(null,l,h)}return s?Promise.resolve(h):h}static#D(e,t){const{globalColorSpaceCache:a}=t;let r;if(e instanceof Ref){r=e;const t=a.getByRef(r);if(t)return t}const i=this.#M(e,t);r&&a.set(null,r,i);return i}static#M(e,t){const{xref:a,resources:r,pdfFunctionFactory:i,globalColorSpaceCache:n}=t;if((e=a.fetchIfRef(e))instanceof Name)switch(e.name){case"G":case"DeviceGray":return this.gray;case"RGB":case"DeviceRGB":return this.rgb;case"DeviceRGBA":return this.rgba;case"CMYK":case"DeviceCMYK":return this.cmyk;case"Pattern":return new PatternCS(null);default:if(r instanceof Dict){const a=r.get("ColorSpace");if(a instanceof Dict){const r=a.get(e.name);if(r){if(r instanceof Name)return this.#M(r,t);e=r;break}}}warn(`Unrecognized ColorSpace: ${e.name}`);return this.gray}if(Array.isArray(e)){const r=a.fetchIfRef(e[0]).name;let s,o,c,l,h,u;switch(r){case"G":case"DeviceGray":return this.gray;case"RGB":case"DeviceRGB":return this.rgb;case"CMYK":case"DeviceCMYK":return this.cmyk;case"CalGray":s=a.fetchIfRef(e[1]);l=s.getArray("WhitePoint");h=s.getArray("BlackPoint");u=s.get("Gamma");return new CalGrayCS(l,h,u);case"CalRGB":s=a.fetchIfRef(e[1]);l=s.getArray("WhitePoint");h=s.getArray("BlackPoint");u=s.getArray("Gamma");const d=s.getArray("Matrix");return new CalRGBCS(l,h,u,d);case"ICCBased":const f=e[1]instanceof Ref;if(f){const t=n.getByRef(e[1]);if(t)return t}const g=a.fetchIfRef(e[1]),p=g.dict;o=p.get("N");if(IccColorSpace.isUsable)try{const t=new IccColorSpace(g.getBytes(),"ICCBased",o);f&&n.set(null,e[1],t);return t}catch(t){if(t instanceof MissingDataException)throw t;warn(`ICCBased color space (${e[1]}): "${t}".`)}const m=p.getRaw("Alternate");if(m){const e=this.#D(m,t);if(e.numComps===o)return e;warn("ICCBased color space: Ignoring incorrect /Alternate entry.")}if(1===o)return this.gray;if(3===o)return this.rgb;if(4===o)return this.cmyk;break;case"Pattern":c=e[1]||null;c&&(c=this.#D(c,t));return new PatternCS(c);case"I":case"Indexed":c=this.#D(e[1],t);const b=MathClamp(a.fetchIfRef(e[2]),0,255),y=a.fetchIfRef(e[3]);return new IndexedCS(c,b,y);case"Separation":case"DeviceN":const w=a.fetchIfRef(e[1]);o=Array.isArray(w)?w.length:1;c=this.#D(e[2],t);const S=i.create(e[3]);return new AlternateCS(o,c,S);case"Lab":s=a.fetchIfRef(e[1]);l=s.getArray("WhitePoint");h=s.getArray("BlackPoint");const x=s.getArray("Range");return new LabCS(l,h,x);default:warn(`Unimplemented ColorSpace object: ${r}`);return this.gray}}warn(`Unrecognized ColorSpace object: ${e}`);return this.gray}static get gray(){return shadow(this,"gray",new DeviceGrayCS)}static get rgb(){return shadow(this,"rgb",new DeviceRgbCS)}static get rgba(){return shadow(this,"rgba",new DeviceRgbaCS)}static get cmyk(){if(CmykICCBasedCS.isUsable)try{return shadow(this,"cmyk",new CmykICCBasedCS)}catch{warn("CMYK fallback: DeviceCMYK")}return shadow(this,"cmyk",new DeviceCmykCS)}}class JpegError extends fa{constructor(e){super(e,"JpegError")}}class DNLMarkerError extends fa{constructor(e,t){super(e,"DNLMarkerError");this.scanLines=t}}class EOIMarkerError extends fa{constructor(e){super(e,"EOIMarkerError")}}const _a=new Uint8Array([0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63]),ja=4017,Ua=799,Xa=3406,qa=2276,Ha=1567,Wa=3784,za=5793,$a=2896;function buildHuffmanTable(e,t){let a,r,i=0,n=16;for(;n>0&&!e[n-1];)n--;const s=[{children:[],index:0}];let o,c=s[0];for(a=0;a0;)c=s.pop();c.index++;s.push(c);for(;s.length<=a;){s.push(o={children:[],index:0});c.children[c.index]=o.children;c=o}i++}if(a+10){g--;return f>>g&1}f=e[t++];if(255===f){const r=e[t++];if(r){if(220===r&&l){const r=readUint16(e,t+=2);t+=2;if(r>0&&r!==a.scanLines)throw new DNLMarkerError("Found DNL marker (0xFFDC) while parsing scan data",r)}else if(217===r){if(l){const e=y*(8===a.precision?8:0);if(e>0&&Math.round(a.scanLines/e)>=5)throw new DNLMarkerError("Found EOI marker (0xFFD9) while parsing scan data, possibly caused by incorrect `scanLines` parameter",e)}throw new EOIMarkerError("Found EOI marker (0xFFD9) while parsing scan data")}throw new JpegError(`unexpected marker ${(f<<8|r).toString(16)}`)}}g=7;return f>>>7}function decodeHuffman(e){let t=e;for(;;){t=t[readBit()];switch(typeof t){case"number":return t;case"object":continue}throw new JpegError("invalid huffman sequence")}}function receive(e){let t=0;for(;e>0;){t=t<<1|readBit();e--}return t}function receiveAndExtend(e){if(1===e)return 1===readBit()?1:-1;const t=receive(e);return t>=1<0){p--;return}let a=n;const r=s;for(;a<=r;){const r=decodeHuffman(e.huffmanTableAC),i=15&r,n=r>>4;if(0===i){if(n<15){p=receive(n)+(1<>4;if(0===i)if(l<15){p=receive(l)+(1<>4;if(0===r){if(n<15)break;i+=16;continue}i+=n;const s=_a[i];e.blockData[t+s]=receiveAndExtend(r);i++}};let O,T=0;const M=1===w?r[0].blocksPerLine*r[0].blocksPerColumn:h*a.mcusPerColumn;let D,R;for(;T<=M;){const a=i?Math.min(M-T,i):M;if(a>0){for(x=0;x0?"unexpected":"excessive"} MCU data, current marker is: ${O.invalid}`);t=O.offset}if(!(O.marker>=65488&&O.marker<=65495))break;t+=2}return t-d}function quantizeAndInverse(e,t,a){const r=e.quantizationTable,i=e.blockData;let n,s,o,c,l,h,u,d,f,g,p,m,b,y,w,S,x;if(!r)throw new JpegError("missing required Quantization Table.");for(let e=0;e<64;e+=8){f=i[t+e];g=i[t+e+1];p=i[t+e+2];m=i[t+e+3];b=i[t+e+4];y=i[t+e+5];w=i[t+e+6];S=i[t+e+7];f*=r[e];if(g|p|m|b|y|w|S){g*=r[e+1];p*=r[e+2];m*=r[e+3];b*=r[e+4];y*=r[e+5];w*=r[e+6];S*=r[e+7];n=za*f+128>>8;s=za*b+128>>8;o=p;c=w;l=$a*(g-S)+128>>8;d=$a*(g+S)+128>>8;h=m<<4;u=y<<4;n=n+s+1>>1;s=n-s;x=o*Wa+c*Ha+128>>8;o=o*Ha-c*Wa+128>>8;c=x;l=l+u+1>>1;u=l-u;d=d+h+1>>1;h=d-h;n=n+c+1>>1;c=n-c;s=s+o+1>>1;o=s-o;x=l*qa+d*Xa+2048>>12;l=l*Xa-d*qa+2048>>12;d=x;x=h*Ua+u*ja+2048>>12;h=h*ja-u*Ua+2048>>12;u=x;a[e]=n+d;a[e+7]=n-d;a[e+1]=s+u;a[e+6]=s-u;a[e+2]=o+h;a[e+5]=o-h;a[e+3]=c+l;a[e+4]=c-l}else{x=za*f+512>>10;a[e]=x;a[e+1]=x;a[e+2]=x;a[e+3]=x;a[e+4]=x;a[e+5]=x;a[e+6]=x;a[e+7]=x}}for(let e=0;e<8;++e){f=a[e];g=a[e+8];p=a[e+16];m=a[e+24];b=a[e+32];y=a[e+40];w=a[e+48];S=a[e+56];if(g|p|m|b|y|w|S){n=za*f+2048>>12;s=za*b+2048>>12;o=p;c=w;l=$a*(g-S)+2048>>12;d=$a*(g+S)+2048>>12;h=m;u=y;n=4112+(n+s+1>>1);s=n-s;x=o*Wa+c*Ha+2048>>12;o=o*Ha-c*Wa+2048>>12;c=x;l=l+u+1>>1;u=l-u;d=d+h+1>>1;h=d-h;n=n+c+1>>1;c=n-c;s=s+o+1>>1;o=s-o;x=l*qa+d*Xa+2048>>12;l=l*Xa-d*qa+2048>>12;d=x;x=h*Ua+u*ja+2048>>12;h=h*ja-u*Ua+2048>>12;u=x;f=n+d;S=n-d;g=s+u;w=s-u;p=o+h;y=o-h;m=c+l;b=c-l;f<16?f=0:f>=4080?f=255:f>>=4;g<16?g=0:g>=4080?g=255:g>>=4;p<16?p=0:p>=4080?p=255:p>>=4;m<16?m=0:m>=4080?m=255:m>>=4;b<16?b=0:b>=4080?b=255:b>>=4;y<16?y=0:y>=4080?y=255:y>>=4;w<16?w=0:w>=4080?w=255:w>>=4;S<16?S=0:S>=4080?S=255:S>>=4;i[t+e]=f;i[t+e+8]=g;i[t+e+16]=p;i[t+e+24]=m;i[t+e+32]=b;i[t+e+40]=y;i[t+e+48]=w;i[t+e+56]=S}else{x=za*f+8192>>14;x=x<-2040?0:x>=2024?255:x+2056>>4;i[t+e]=x;i[t+e+8]=x;i[t+e+16]=x;i[t+e+24]=x;i[t+e+32]=x;i[t+e+40]=x;i[t+e+48]=x;i[t+e+56]=x}}}function buildComponentData(e,t){const a=t.blocksPerLine,r=t.blocksPerColumn,i=new Int16Array(64);for(let e=0;e=r)return null;const n=readUint16(e,t);if(n>=65472&&n<=65534)return{invalid:null,marker:n,offset:t};let s=readUint16(e,i);for(;!(s>=65472&&s<=65534);){if(++i>=r)return null;s=readUint16(e,i)}return{invalid:n.toString(16),marker:s,offset:i}}function prepareComponents(e){const t=Math.ceil(e.samplesPerLine/8/e.maxH),a=Math.ceil(e.scanLines/8/e.maxV);for(const r of e.components){const i=Math.ceil(Math.ceil(e.samplesPerLine/8)*r.h/e.maxH),n=Math.ceil(Math.ceil(e.scanLines/8)*r.v/e.maxV),s=t*r.h,o=64*(a*r.v)*(s+1);r.blockData=new Int16Array(o);r.blocksPerLine=i;r.blocksPerColumn=n}e.mcusPerLine=t;e.mcusPerColumn=a}function readDataBlock(e,t){const a=readUint16(e,t);let r=(t+=2)+a-2;const i=findNextFileMarker(e,r,t);if(i?.invalid){warn("readDataBlock - incorrect length, current marker is: "+i.invalid);r=i.offset}const n=e.subarray(t,r);return{appData:n,oldOffset:t,newOffset:t+n.length}}function skipData(e,t){const a=readUint16(e,t),r=(t+=2)+a-2,i=findNextFileMarker(e,r,t);return i?.invalid?i.offset:r}class JpegImage{constructor({decodeTransform:e=null,colorTransform:t=-1}={}){this._decodeTransform=e;this._colorTransform=t}static canUseImageDecoder(e,t=-1){let a=null,r=0,i=null,n=readUint16(e,r);r+=2;if(65496!==n)throw new JpegError("SOI not found");n=readUint16(e,r);r+=2;e:for(;65497!==n;){switch(n){case 65505:const{appData:t,oldOffset:s,newOffset:o}=readDataBlock(e,r);r=o;if(69===t[0]&&120===t[1]&&105===t[2]&&102===t[3]&&0===t[4]&&0===t[5]){if(a)throw new JpegError("Duplicate EXIF-blocks found.");a={exifStart:s+6,exifEnd:o}}n=readUint16(e,r);r+=2;continue;case 65472:case 65473:case 65474:i=e[r+7];break e;case 65535:255!==e[r]&&r--}r=skipData(e,r);n=readUint16(e,r);r+=2}return 4===i||3===i&&0===t?null:a||{}}parse(e,{dnlScanLines:t=null}={}){let a,r,i=0,n=null,s=null,o=0;const c=[],l=[],h=[];let u=readUint16(e,i);i+=2;if(65496!==u)throw new JpegError("SOI not found");u=readUint16(e,i);i+=2;e:for(;65497!==u;){let d,f,g;switch(u){case 65504:case 65505:case 65506:case 65507:case 65508:case 65509:case 65510:case 65511:case 65512:case 65513:case 65514:case 65515:case 65516:case 65517:case 65518:case 65519:case 65534:const{appData:p,newOffset:m}=readDataBlock(e,i);i=m;65504===u&&74===p[0]&&70===p[1]&&73===p[2]&&70===p[3]&&0===p[4]&&(n={version:{major:p[5],minor:p[6]},densityUnits:p[7],xDensity:p[8]<<8|p[9],yDensity:p[10]<<8|p[11],thumbWidth:p[12],thumbHeight:p[13],thumbData:p.subarray(14,14+3*p[12]*p[13])});65518===u&&65===p[0]&&100===p[1]&&111===p[2]&&98===p[3]&&101===p[4]&&(s={version:p[5]<<8|p[6],flags0:p[7]<<8|p[8],flags1:p[9]<<8|p[10],transformCode:p[11]});break;case 65499:const b=readUint16(e,i);i+=2;const y=b+i-2;let w;for(;i>4){if(t>>4!=1)throw new JpegError("DQT - invalid table spec");for(f=0;f<64;f++){w=_a[f];a[w]=readUint16(e,i);i+=2}}else for(f=0;f<64;f++){w=_a[f];a[w]=e[i++]}c[15&t]=a}break;case 65472:case 65473:case 65474:if(a)throw new JpegError("Only single frame JPEGs supported");i+=2;a={};a.extended=65473===u;a.progressive=65474===u;a.precision=e[i++];const S=readUint16(e,i);i+=2;a.scanLines=t||S;a.samplesPerLine=readUint16(e,i);i+=2;a.components=[];a.componentIds={};const x=e[i++];let k=0,C=0;for(d=0;d>4,n=15&e[i+1];k>4?l:h)[15&t]=buildHuffmanTable(a,n)}break;case 65501:i+=2;r=readUint16(e,i);i+=2;break;case 65498:const F=1==++o&&!t;i+=2;const O=e[i++],T=[];for(d=0;d>4];n.huffmanTableAC=l[15&s];T.push(n)}const M=e[i++],D=e[i++],R=e[i++];try{i+=decodeScan(e,i,a,T,r,M,D,R>>4,15&R,F)}catch(t){if(t instanceof DNLMarkerError){warn(`${t.message} -- attempting to re-parse the JPEG image.`);return this.parse(e,{dnlScanLines:t.scanLines})}if(t instanceof EOIMarkerError){warn(`${t.message} -- ignoring the rest of the image data.`);break e}throw t}break;case 65500:i+=4;break;case 65535:255!==e[i]&&i--;break;default:const N=findNextFileMarker(e,i-2,i-3);if(N?.invalid){warn("JpegImage.parse - unexpected data, current marker is: "+N.invalid);i=N.offset;break}if(!N||i>=e.length-1){warn("JpegImage.parse - reached the end of the image data without finding an EOI marker (0xFFD9).");break e}throw new JpegError("JpegImage.parse - unknown marker: "+u.toString(16))}u=readUint16(e,i);i+=2}if(!a)throw new JpegError("JpegImage.parse - no frame data found.");this.width=a.samplesPerLine;this.height=a.scanLines;this.jfif=n;this.adobe=s;this.components=[];for(const e of a.components){const t=c[e.quantizationId];t&&(e.quantizationTable=t);this.components.push({index:e.index,output:buildComponentData(0,e),scaleX:e.h/a.maxH,scaleY:e.v/a.maxV,blocksPerLine:e.blocksPerLine,blocksPerColumn:e.blocksPerColumn})}this.numComponents=this.components.length}_getLinearizedBlockData(e,t,a=!1){const r=this.width/e,i=this.height/t;let n,s,o,c,l,h,u,d,f,g,p,m=0;const b=this.components.length,y=e*t*b,w=new Uint8ClampedArray(y),S=new Uint32Array(e),x=4294967288;let k;for(u=0;u>8)+C[f+1];return w}get _isColorConversionNeeded(){return this.adobe?!!this.adobe.transformCode:3===this.numComponents?0!==this._colorTransform&&(82!==this.components[0].index||71!==this.components[1].index||66!==this.components[2].index):1===this._colorTransform}_convertYccToRgb(e){let t,a,r;for(let i=0,n=e.length;i4)throw new JpegError("Unsupported color mode");const n=this._getLinearizedBlockData(e,t,i);if(1===this.numComponents&&(a||r)){const e=n.length*(a?4:3),t=new Uint8ClampedArray(e);let r=0;if(a)!function grayToRGBA(e,t){if(FeatureTest.isLittleEndian)for(let a=0,r=e.length;a0&&(e=e.subarray(t));break}return e}decodeImage(e){if(this.eof)return this.buffer;e=this.#B(e||this.bytes);const t=new JpegImage(this.jpegOptions);t.parse(e);const a=t.getData({width:this.drawWidth,height:this.drawHeight,forceRGBA:this.forceRGBA,forceRGB:this.forceRGB,isSourcePDF:!0});this.buffer=a;this.bufferLength=a.length;this.eof=!0;return this.buffer}get canAsyncDecodeImageFromBuffer(){return this.stream.isAsync}async getTransferableImage(){if(!await JpegStream.canUseImageDecoder)return null;const e=this.jpegOptions;if(e.decodeTransform)return null;let t;try{const a=this.canAsyncDecodeImageFromBuffer&&await this.stream.asyncGetBytes()||this.bytes;if(!a)return null;let r=this.#B(a);const i=JpegImage.canUseImageDecoder(r,e.colorTransform);if(!i)return null;if(i.exifStart){r=r.slice();r.fill(0,i.exifStart,i.exifEnd)}t=new ImageDecoder({data:r,type:"image/jpeg",preferAnimation:!1});return(await t.decode()).image}catch(e){warn(`getTransferableImage - failed: "${e}".`);return null}finally{t?.close()}}}var Ga,Va=(Ga=import.meta.url,async function(e={}){var t,a,r,i=e,n=new Promise(((e,r)=>{t=e;a=r})),s=Object.assign({},i),o="./this.program",quit_=(e,t)=>{throw t},c="";"undefined"!=typeof document&&document.currentScript&&(c=document.currentScript.src);Ga&&(c=Ga);c=c.startsWith("blob:")?"":c.slice(0,c.replace(/[?#].*/,"").lastIndexOf("/")+1);r=async e=>{var t=await fetch(e,{credentials:"same-origin"});if(t.ok)return t.arrayBuffer();throw new Error(t.status+" : "+t.url)};var l=i.print||console.log.bind(console),h=i.printErr||console.error.bind(console);Object.assign(i,s);s=null;i.arguments&&i.arguments;i.thisProgram&&(o=i.thisProgram);var u,d,f,g,p,m,b=i.wasmBinary,y=!1;function updateMemoryViews(){var e=u.buffer;i.HEAP8=f=new Int8Array(e);i.HEAP16=new Int16Array(e);i.HEAPU8=g=new Uint8Array(e);i.HEAPU16=new Uint16Array(e);i.HEAP32=p=new Int32Array(e);i.HEAPU32=m=new Uint32Array(e);i.HEAPF32=new Float32Array(e);i.HEAPF64=new Float64Array(e);i.HEAP64=new BigInt64Array(e);i.HEAPU64=new BigUint64Array(e)}var w,S=0,x=null;function abort(e){i.onAbort?.(e);h(e="Aborted("+e+")");y=!0;e+=". Build with -sASSERTIONS for more info.";var t=new WebAssembly.RuntimeError(e);a(t);throw t}function findWasmBinary(){return i.locateFile?function locateFile(e){return i.locateFile?i.locateFile(e,c):c+e}("openjpeg.wasm"):new URL("openjpeg.wasm",import.meta.url).href}async function getWasmBinary(e){if(!b)try{var t=await r(e);return new Uint8Array(t)}catch{}return function getBinarySync(e){if(e==w&&b)return new Uint8Array(b);throw"both async and sync fetching of the wasm failed"}(e)}async function instantiateAsync(e,t,a){if(!e&&"function"==typeof WebAssembly.instantiateStreaming)try{var r=fetch(t,{credentials:"same-origin"});return await WebAssembly.instantiateStreaming(r,a)}catch(e){h(`wasm streaming compile failed: ${e}`);h("falling back to ArrayBuffer instantiation")}return async function instantiateArrayBuffer(e,t){try{var a=await getWasmBinary(e);return await WebAssembly.instantiate(a,t)}catch(e){h(`failed to asynchronously prepare wasm: ${e}`);abort(e)}}(t,a)}class ExitStatus{name="ExitStatus";constructor(e){this.message=`Program terminated with exit(${e})`;this.status=e}}var callRuntimeCallbacks=e=>{for(;e.length>0;)e.shift()(i)},k=[],addOnPostRun=e=>k.unshift(e),C=[],addOnPreRun=e=>C.unshift(e),v=i.noExitRuntime||!0,F=0,O={},handleException=e=>{if(e instanceof ExitStatus||"unwind"==e)return d;quit_(0,e)},keepRuntimeAlive=()=>v||F>0,_proc_exit=e=>{d=e;if(!keepRuntimeAlive()){i.onExit?.(e);y=!0}quit_(0,new ExitStatus(e))},_exit=(e,t)=>{d=e;_proc_exit(e)},callUserCallback=e=>{if(!y)try{e();(()=>{if(!keepRuntimeAlive())try{_exit(d)}catch(e){handleException(e)}})()}catch(e){handleException(e)}},growMemory=e=>{var t=(e-u.buffer.byteLength+65535)/65536|0;try{u.grow(t);updateMemoryViews();return 1}catch(e){}},T={},getEnvStrings=()=>{if(!getEnvStrings.strings){var e={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:o||"./this.program"};for(var t in T)void 0===T[t]?delete e[t]:e[t]=T[t];var a=[];for(var t in e)a.push(`${t}=${e[t]}`);getEnvStrings.strings=a}return getEnvStrings.strings},M=[null,[],[]],D="undefined"!=typeof TextDecoder?new TextDecoder:void 0,UTF8ArrayToString=(e,t=0,a=NaN)=>{for(var r=t+a,i=t;e[i]&&!(i>=r);)++i;if(i-t>16&&e.buffer&&D)return D.decode(e.subarray(t,i));for(var n="";t>10,56320|1023&l)}}else n+=String.fromCharCode((31&s)<<6|o)}else n+=String.fromCharCode(s)}return n},printChar=(e,t)=>{var a=M[e];if(0===t||10===t){(1===e?l:h)(UTF8ArrayToString(a));a.length=0}else a.push(t)},UTF8ToString=(e,t)=>e?UTF8ArrayToString(g,e,t):"",R={l:()=>abort(""),k:()=>{v=!1;F=0},m:(e,t)=>{if(O[e]){clearTimeout(O[e].id);delete O[e]}if(!t)return 0;var a=setTimeout((()=>{delete O[e];callUserCallback((()=>E(e,performance.now())))}),t);O[e]={id:a,timeout_ms:t};return 0},g:function _copy_pixels_1(e,t){e>>=2;const a=i.imageData=new Uint8ClampedArray(t),r=p.subarray(e,e+t);a.set(r)},f:function _copy_pixels_3(e,t,a,r){e>>=2;t>>=2;a>>=2;const n=i.imageData=new Uint8ClampedArray(3*r),s=p.subarray(e,e+r),o=p.subarray(t,t+r),c=p.subarray(a,a+r);for(let e=0;e>=2;t>>=2;a>>=2;r>>=2;const s=i.imageData=new Uint8ClampedArray(4*n),o=p.subarray(e,e+n),c=p.subarray(t,t+n),l=p.subarray(a,a+n),h=p.subarray(r,r+n);for(let e=0;e{var t,a,r=g.length,i=2147483648;if((e>>>=0)>i)return!1;for(var n=1;n<=4;n*=2){var s=r*(1+.2/n);s=Math.min(s,e+100663296);var o=Math.min(i,(t=Math.max(e,s),a=65536,Math.ceil(t/a)*a));if(growMemory(o))return!0}return!1},p:(e,t)=>{var a=0;getEnvStrings().forEach(((r,i)=>{var n=t+a;m[e+4*i>>2]=n;((e,t)=>{for(var a=0;a{var a=getEnvStrings();m[e>>2]=a.length;var r=0;a.forEach((e=>r+=e.length+1));m[t>>2]=r;return 0},b:e=>52,o:function _fd_seek(e,t,a,r){t=(i=t)<-9007199254740992||i>9007199254740992?NaN:Number(i);var i;return 70},c:(e,t,a,r)=>{for(var i=0,n=0;n>2],o=m[t+4>>2];t+=8;for(var c=0;c>2]=i;return 0},r:function _gray_to_rgba(e,t){e>>=2;const a=i.imageData=new Uint8ClampedArray(4*t),r=p.subarray(e,e+t);for(let e=0;e>=2;t>>=2;const r=i.imageData=new Uint8ClampedArray(4*a),n=p.subarray(e,e+a),s=p.subarray(t,t+a);for(let e=0;e>=2;t>>=2;a>>=2;const n=i.imageData=new Uint8ClampedArray(4*r),s=p.subarray(e,e+r),o=p.subarray(t,t+r),c=p.subarray(a,a+r);for(let e=0;e{i.instantiateWasm(e,((e,a)=>{receiveInstance(e);t(e.exports)}))}));w??=findWasmBinary();try{var t=function receiveInstantiationResult(e){return receiveInstance(e.instance)}(await instantiateAsync(b,w,e));return t}catch(e){a(e);return Promise.reject(e)}}(),E=(N.t,i._malloc=N.u,i._free=N.v,i._jp2_decode=N.x,N.y);if(i.preInit){"function"==typeof i.preInit&&(i.preInit=[i.preInit]);for(;i.preInit.length>0;)i.preInit.pop()()}!function run(){if(S>0)x=run;else{!function preRun(){if(i.preRun){"function"==typeof i.preRun&&(i.preRun=[i.preRun]);for(;i.preRun.length;)addOnPreRun(i.preRun.shift())}callRuntimeCallbacks(C)}();if(S>0)x=run;else if(i.setStatus){i.setStatus("Running...");setTimeout((()=>{setTimeout((()=>i.setStatus("")),1);doRun()}),1)}else doRun()}function doRun(){i.calledRun=!0;if(!y){!function initRuntime(){N.t()}();t(i);i.onRuntimeInitialized?.();!function postRun(){if(i.postRun){"function"==typeof i.postRun&&(i.postRun=[i.postRun]);for(;i.postRun.length;)addOnPostRun(i.postRun.shift())}callRuntimeCallbacks(k)}()}}}();return n});const Ka=Va;class JpxError extends fa{constructor(e){super(e,"JpxError")}}class JpxImage{static#R=null;static#N=null;static#E=null;static#k=!0;static#P=!0;static#C=null;static setOptions({handler:e,useWasm:t,useWorkerFetch:a,wasmUrl:r}){this.#k=t;this.#P=a;this.#C=r;a||(this.#N=e)}static async#L(e){const t=`${this.#C}openjpeg_nowasm_fallback.js`;let a=null;try{a=(await import(t)).default()}catch(e){warn(`JpxImage#getJsModule: ${e}`)}e(a)}static async#_(e,t,a){const r="openjpeg.wasm";try{this.#R||(this.#P?this.#R=await fetchBinaryData(`${this.#C}${r}`):this.#R=await this.#N.sendWithPromise("FetchBinaryData",{type:"wasmFactory",filename:r}));return a((await WebAssembly.instantiate(this.#R,t)).instance)}catch(t){warn(`JpxImage#instantiateWasm: ${t}`);this.#L(e);return null}finally{this.#N=null}}static async decode(e,{numComponents:t=4,isIndexedColormap:a=!1,smaskInData:r=!1}={}){if(!this.#E){const{promise:e,resolve:t}=Promise.withResolvers(),a=[e];this.#k?a.push(Ka({warn,instantiateWasm:this.#_.bind(this,t)})):this.#L(t);this.#E=Promise.race(a)}const i=await this.#E;if(!i)throw new JpxError("OpenJPEG failed to initialize");let n;try{const s=e.length;n=i._malloc(s);i.HEAPU8.set(e,n);if(i._jp2_decode(n,s,t>0?t:0,!!a,!!r)){const{errorMessages:e}=i;if(e){delete i.errorMessages;throw new JpxError(e)}throw new JpxError("Unknown error")}const{imageData:o}=i;i.imageData=null;return o}finally{n&&i._free(n)}}static cleanup(){this.#E=null}static parseImageProperties(e){let t=e.getByte();for(;t>=0;){const a=t;t=e.getByte();if(65361===(a<<8|t)){e.skip(4);const t=e.getInt32()>>>0,a=e.getInt32()>>>0,r=e.getInt32()>>>0,i=e.getInt32()>>>0;e.skip(16);return{width:t-r,height:a-i,bitsPerComponent:8,componentsCount:e.getUint16()}}}throw new JpxError("No size marker found in JPX stream")}}function addState(e,t,a,r,i){let n=e;for(let e=0,a=t.length-1;e1e3){l=Math.max(l,d);f+=u+2;d=0;u=0}h.push({transform:t,x:d,y:f,w:a.width,h:a.height});d+=a.width+2;u=Math.max(u,a.height)}const g=Math.max(l,d)+1,p=f+u+1,m=new Uint8Array(g*p*4),b=g<<2;for(let e=0;e=0;){t[n-4]=t[n];t[n-3]=t[n+1];t[n-2]=t[n+2];t[n-1]=t[n+3];t[n+a]=t[n+a-4];t[n+a+1]=t[n+a-3];t[n+a+2]=t[n+a-2];t[n+a+3]=t[n+a-1];n-=b}}const y={width:g,height:p};if(e.isOffscreenCanvasSupported){const e=new OffscreenCanvas(g,p);e.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(m.buffer),g,p),0,0);y.bitmap=e.transferToImageBitmap();y.data=null}else{y.kind=v;y.data=m}a.splice(n,4*c,Zt);r.splice(n,4*c,[y,h]);return n+1}));addState(Ja,[Be,Ne,Vt,Re],null,(function iterateImageMaskGroup(e,t){const a=e.fnArray,r=(t-(e.iCurr-3))%4;switch(r){case 0:return a[t]===Be;case 1:return a[t]===Ne;case 2:return a[t]===Vt;case 3:return a[t]===Re}throw new Error(`iterateImageMaskGroup - invalid pos: ${r}`)}),(function foundImageMaskGroup(e,t){const a=e.fnArray,r=e.argsArray,i=e.iCurr,n=i-3,s=i-2,o=i-1;let c=Math.floor((t-n)/4);if(c<10)return t-(t-n)%4;let l,h,u=!1;const d=r[o][0],f=r[s][0],g=r[s][1],p=r[s][2],m=r[s][3];if(g===p){u=!0;l=s+4;let e=o+4;for(let t=1;t=4&&a[n-4]===a[s]&&a[n-3]===a[o]&&a[n-2]===a[c]&&a[n-1]===a[l]&&r[n-4][0]===h&&r[n-4][1]===u){d++;f-=5}let g=f+4;for(let e=1;e{const t=e.argsArray,a=t[e.iCurr-1][0];if(a!==qe&&a!==He&&a!==$e&&a!==Ge&&a!==Ve&&a!==Ke)return!0;const r=t[e.iCurr-2];return 1===r[0]&&0===r[1]&&0===r[2]&&1===r[3]}),(()=>!1),((e,t)=>{const{fnArray:a,argsArray:r}=e,i=e.iCurr,n=i-3,s=i-2,o=r[i-1],c=r[s],[,[l],h]=o;Util.scaleMinMax(c,h);for(let e=0,t=l.length;e=a)break}r=(r||Ja)[e[t]];if(r&&!Array.isArray(r)){n.iCurr=t;t++;if(!r.checkFn||(0,r.checkFn)(n)){i=r;r=null}else r=null}else t++}this.state=r;this.match=i;this.lastProcessed=t}flush(){for(;this.match;){const e=this.queue.fnArray.length;this.lastProcessed=(0,this.match.processFn)(this.context,e);this.match=null;this.state=null;this._optimize()}}reset(){this.state=null;this.match=null;this.lastProcessed=0}}class OperatorList{static CHUNK_SIZE=1e3;static CHUNK_SIZE_ABOUT=this.CHUNK_SIZE-5;static isOffscreenCanvasSupported=!1;constructor(e=0,t){this._streamSink=t;this.fnArray=[];this.argsArray=[];this.optimizer=!t||e&d?new NullOptimizer(this):new QueueOptimizer(this);this.dependencies=new Set;this._totalLength=0;this.weight=0;this._resolved=t?null:Promise.resolve()}static setOptions({isOffscreenCanvasSupported:e}){this.isOffscreenCanvasSupported=e}get length(){return this.argsArray.length}get ready(){return this._resolved||this._streamSink.ready}get totalLength(){return this._totalLength+this.length}addOp(e,t){this.optimizer.push(e,t);this.weight++;this._streamSink&&(this.weight>=OperatorList.CHUNK_SIZE||this.weight>=OperatorList.CHUNK_SIZE_ABOUT&&(e===Re||e===et))&&this.flush()}addImageOps(e,t,a,r=!1){if(r){this.addOp(Be);this.addOp(De,[[["SMask",!1]]])}void 0!==a&&this.addOp(_t,["OC",a]);this.addOp(e,t);void 0!==a&&this.addOp(jt,[]);r&&this.addOp(Re)}addDependency(e){if(!this.dependencies.has(e)){this.dependencies.add(e);this.addOp(ke,[e])}}addDependencies(e){for(const t of e)this.addDependency(t)}addOpList(e){if(e instanceof OperatorList){for(const t of e.dependencies)this.dependencies.add(t);for(let t=0,a=e.length;t>>0}function hexToStr(e,t){return 1===t?String.fromCharCode(e[0],e[1]):3===t?String.fromCharCode(e[0],e[1],e[2],e[3]):String.fromCharCode(...e.subarray(0,t+1))}function addHex(e,t,a){let r=0;for(let i=a;i>=0;i--){r+=e[i]+t[i];e[i]=255&r;r>>=8}}function incHex(e,t){let a=1;for(let r=t;r>=0&&a>0;r--){a+=e[r];e[r]=255&a;a>>=8}}const Ya=16;class BinaryCMapStream{constructor(e){this.buffer=e;this.pos=0;this.end=e.length;this.tmpBuf=new Uint8Array(19)}readByte(){return this.pos>=this.end?-1:this.buffer[this.pos++]}readNumber(){let e,t=0;do{const a=this.readByte();if(a<0)throw new FormatError("unexpected EOF in bcmap");e=!(128&a);t=t<<7|127&a}while(!e);return t}readSigned(){const e=this.readNumber();return 1&e?~(e>>>1):e>>>1}readHex(e,t){e.set(this.buffer.subarray(this.pos,this.pos+t+1));this.pos+=t+1}readHexNumber(e,t){let a;const r=this.tmpBuf;let i=0;do{const e=this.readByte();if(e<0)throw new FormatError("unexpected EOF in bcmap");a=!(128&e);r[i++]=127&e}while(!a);let n=t,s=0,o=0;for(;n>=0;){for(;o<8&&r.length>0;){s|=r[--i]<>=8;o-=8}}readHexSigned(e,t){this.readHexNumber(e,t);const a=1&e[t]?255:0;let r=0;for(let i=0;i<=t;i++){r=(1&r)<<8|e[i];e[i]=r>>1^a}}readString(){const e=this.readNumber(),t=new Array(e);for(let a=0;a=0;){const e=d>>5;if(7===e){switch(31&d){case 0:r.readString();break;case 1:n=r.readString()}continue}const a=!!(16&d),i=15&d;if(i+1>Ya)throw new Error("BinaryCMapReader.process: Invalid dataSize.");const f=1,g=r.readNumber();switch(e){case 0:r.readHex(s,i);r.readHexNumber(o,i);addHex(o,s,i);t.addCodespaceRange(i+1,hexToInt(s,i),hexToInt(o,i));for(let e=1;e=0;--i){r[a+i]=255&s;s>>=8}}}}class AsciiHexStream extends DecodeStream{constructor(e,t){t&&(t*=.5);super(t);this.str=e;this.dict=e.dict;this.firstDigit=-1}readBlock(){const e=this.str.getBytes(8e3);if(!e.length){this.eof=!0;return}const t=e.length+1>>1,a=this.ensureBuffer(this.bufferLength+t);let r=this.bufferLength,i=this.firstDigit;for(const t of e){let e;if(t>=48&&t<=57)e=15&t;else{if(!(t>=65&&t<=70||t>=97&&t<=102)){if(62===t){this.eof=!0;break}continue}e=9+(15&t)}if(i<0)i=e;else{a[r++]=i<<4|e;i=-1}}if(i>=0&&this.eof){a[r++]=i<<4;i=-1}this.firstDigit=i;this.bufferLength=r}}const Za=-1,Qa=[[-1,-1],[-1,-1],[7,8],[7,7],[6,6],[6,6],[6,5],[6,5],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2]],er=[[-1,-1],[12,-2],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[11,1792],[11,1792],[12,1984],[12,2048],[12,2112],[12,2176],[12,2240],[12,2304],[11,1856],[11,1856],[11,1920],[11,1920],[12,2368],[12,2432],[12,2496],[12,2560]],tr=[[-1,-1],[-1,-1],[-1,-1],[-1,-1],[8,29],[8,29],[8,30],[8,30],[8,45],[8,45],[8,46],[8,46],[7,22],[7,22],[7,22],[7,22],[7,23],[7,23],[7,23],[7,23],[8,47],[8,47],[8,48],[8,48],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[7,20],[7,20],[7,20],[7,20],[8,33],[8,33],[8,34],[8,34],[8,35],[8,35],[8,36],[8,36],[8,37],[8,37],[8,38],[8,38],[7,19],[7,19],[7,19],[7,19],[8,31],[8,31],[8,32],[8,32],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[8,53],[8,53],[8,54],[8,54],[7,26],[7,26],[7,26],[7,26],[8,39],[8,39],[8,40],[8,40],[8,41],[8,41],[8,42],[8,42],[8,43],[8,43],[8,44],[8,44],[7,21],[7,21],[7,21],[7,21],[7,28],[7,28],[7,28],[7,28],[8,61],[8,61],[8,62],[8,62],[8,63],[8,63],[8,0],[8,0],[8,320],[8,320],[8,384],[8,384],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[7,27],[7,27],[7,27],[7,27],[8,59],[8,59],[8,60],[8,60],[9,1472],[9,1536],[9,1600],[9,1728],[7,18],[7,18],[7,18],[7,18],[7,24],[7,24],[7,24],[7,24],[8,49],[8,49],[8,50],[8,50],[8,51],[8,51],[8,52],[8,52],[7,25],[7,25],[7,25],[7,25],[8,55],[8,55],[8,56],[8,56],[8,57],[8,57],[8,58],[8,58],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[8,448],[8,448],[8,512],[8,512],[9,704],[9,768],[8,640],[8,640],[8,576],[8,576],[9,832],[9,896],[9,960],[9,1024],[9,1088],[9,1152],[9,1216],[9,1280],[9,1344],[9,1408],[7,256],[7,256],[7,256],[7,256],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7]],ar=[[-1,-1],[-1,-1],[12,-2],[12,-2],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[11,1792],[11,1792],[11,1792],[11,1792],[12,1984],[12,1984],[12,2048],[12,2048],[12,2112],[12,2112],[12,2176],[12,2176],[12,2240],[12,2240],[12,2304],[12,2304],[11,1856],[11,1856],[11,1856],[11,1856],[11,1920],[11,1920],[11,1920],[11,1920],[12,2368],[12,2368],[12,2432],[12,2432],[12,2496],[12,2496],[12,2560],[12,2560],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[12,52],[12,52],[13,640],[13,704],[13,768],[13,832],[12,55],[12,55],[12,56],[12,56],[13,1280],[13,1344],[13,1408],[13,1472],[12,59],[12,59],[12,60],[12,60],[13,1536],[13,1600],[11,24],[11,24],[11,24],[11,24],[11,25],[11,25],[11,25],[11,25],[13,1664],[13,1728],[12,320],[12,320],[12,384],[12,384],[12,448],[12,448],[13,512],[13,576],[12,53],[12,53],[12,54],[12,54],[13,896],[13,960],[13,1024],[13,1088],[13,1152],[13,1216],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64]],rr=[[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[11,23],[11,23],[12,50],[12,51],[12,44],[12,45],[12,46],[12,47],[12,57],[12,58],[12,61],[12,256],[10,16],[10,16],[10,16],[10,16],[10,17],[10,17],[10,17],[10,17],[12,48],[12,49],[12,62],[12,63],[12,30],[12,31],[12,32],[12,33],[12,40],[12,41],[11,22],[11,22],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[12,128],[12,192],[12,26],[12,27],[12,28],[12,29],[11,19],[11,19],[11,20],[11,20],[12,34],[12,35],[12,36],[12,37],[12,38],[12,39],[11,21],[11,21],[12,42],[12,43],[10,0],[10,0],[10,0],[10,0],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12]],ir=[[-1,-1],[-1,-1],[-1,-1],[-1,-1],[6,9],[6,8],[5,7],[5,7],[4,6],[4,6],[4,6],[4,6],[4,5],[4,5],[4,5],[4,5],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2]];class CCITTFaxDecoder{constructor(e,t={}){if("function"!=typeof e?.next)throw new Error('CCITTFaxDecoder - invalid "source" parameter.');this.source=e;this.eof=!1;this.encoding=t.K||0;this.eoline=t.EndOfLine||!1;this.byteAlign=t.EncodedByteAlign||!1;this.columns=t.Columns||1728;this.rows=t.Rows||0;this.eoblock=t.EndOfBlock??!0;this.black=t.BlackIs1||!1;this.codingLine=new Uint32Array(this.columns+1);this.refLine=new Uint32Array(this.columns+2);this.codingLine[0]=this.columns;this.codingPos=0;this.row=0;this.nextLine2D=this.encoding<0;this.inputBits=0;this.inputBuf=0;this.outputBits=0;this.rowsDone=!1;let a;for(;0===(a=this._lookBits(12));)this._eatBits(1);1===a&&this._eatBits(12);if(this.encoding>0){this.nextLine2D=!this._lookBits(1);this._eatBits(1)}}readNextChar(){if(this.eof)return-1;const e=this.refLine,t=this.codingLine,a=this.columns;let r,i,n,s,o;if(0===this.outputBits){this.rowsDone&&(this.eof=!0);if(this.eof)return-1;this.err=!1;let n,o,c;if(this.nextLine2D){for(s=0;t[s]=64);do{o+=c=this._getWhiteCode()}while(c>=64)}else{do{n+=c=this._getWhiteCode()}while(c>=64);do{o+=c=this._getBlackCode()}while(c>=64)}this._addPixels(t[this.codingPos]+n,i);t[this.codingPos]0?--r:++r;for(;e[r]<=t[this.codingPos]&&e[r]0?--r:++r;for(;e[r]<=t[this.codingPos]&&e[r]0?--r:++r;for(;e[r]<=t[this.codingPos]&&e[r]=64);else do{n+=c=this._getWhiteCode()}while(c>=64);this._addPixels(t[this.codingPos]+n,i);i^=1}}let l=!1;this.byteAlign&&(this.inputBits&=-8);if(this.eoblock||this.row!==this.rows-1){n=this._lookBits(12);if(this.eoline)for(;n!==Za&&1!==n;){this._eatBits(1);n=this._lookBits(12)}else for(;0===n;){this._eatBits(1);n=this._lookBits(12)}if(1===n){this._eatBits(12);l=!0}else n===Za&&(this.eof=!0)}else this.rowsDone=!0;if(!this.eof&&this.encoding>0&&!this.rowsDone){this.nextLine2D=!this._lookBits(1);this._eatBits(1)}if(this.eoblock&&l&&this.byteAlign){n=this._lookBits(12);if(1===n){this._eatBits(12);if(this.encoding>0){this._lookBits(1);this._eatBits(1)}if(this.encoding>=0)for(s=0;s<4;++s){n=this._lookBits(12);1!==n&&info("bad rtc code: "+n);this._eatBits(12);if(this.encoding>0){this._lookBits(1);this._eatBits(1)}}this.eof=!0}}else if(this.err&&this.eoline){for(;;){n=this._lookBits(13);if(n===Za){this.eof=!0;return-1}if(n>>1==1)break;this._eatBits(1)}this._eatBits(12);if(this.encoding>0){this._eatBits(1);this.nextLine2D=!(1&n)}}this.outputBits=t[0]>0?t[this.codingPos=0]:t[this.codingPos=1];this.row++}if(this.outputBits>=8){o=1&this.codingPos?0:255;this.outputBits-=8;if(0===this.outputBits&&t[this.codingPos]n){o<<=n;1&this.codingPos||(o|=255>>8-n);this.outputBits-=n;n=0}else{o<<=this.outputBits;1&this.codingPos||(o|=255>>8-this.outputBits);n-=this.outputBits;this.outputBits=0;if(t[this.codingPos]0){o<<=n;n=0}}}while(n)}this.black&&(o^=255);return o}_addPixels(e,t){const a=this.codingLine;let r=this.codingPos;if(e>a[r]){if(e>this.columns){info("row is wrong length");this.err=!0;e=this.columns}1&r^t&&++r;a[r]=e}this.codingPos=r}_addPixelsNeg(e,t){const a=this.codingLine;let r=this.codingPos;if(e>a[r]){if(e>this.columns){info("row is wrong length");this.err=!0;e=this.columns}1&r^t&&++r;a[r]=e}else if(e0&&e=i){const t=a[e-i];if(t[0]===r){this._eatBits(r);return[!0,t[1],!0]}}}return[!1,0,!1]}_getTwoDimCode(){let e,t=0;if(this.eoblock){t=this._lookBits(7);e=Qa[t];if(e?.[0]>0){this._eatBits(e[0]);return e[1]}}else{const e=this._findTableCode(1,7,Qa);if(e[0]&&e[2])return e[1]}info("Bad two dim code");return Za}_getWhiteCode(){let e,t=0;if(this.eoblock){t=this._lookBits(12);if(t===Za)return 1;e=t>>5?tr[t>>3]:er[t];if(e[0]>0){this._eatBits(e[0]);return e[1]}}else{let e=this._findTableCode(1,9,tr);if(e[0])return e[1];e=this._findTableCode(11,12,er);if(e[0])return e[1]}info("bad white code");this._eatBits(1);return 1}_getBlackCode(){let e,t;if(this.eoblock){e=this._lookBits(13);if(e===Za)return 1;t=e>>7?!(e>>9)&&e>>7?rr[(e>>1)-64]:ir[e>>7]:ar[e];if(t[0]>0){this._eatBits(t[0]);return t[1]}}else{let e=this._findTableCode(2,6,ir);if(e[0])return e[1];e=this._findTableCode(7,12,rr,64);if(e[0])return e[1];e=this._findTableCode(10,13,ar);if(e[0])return e[1]}info("bad black code");this._eatBits(1);return 1}_lookBits(e){let t;for(;this.inputBits>16-e;this.inputBuf=this.inputBuf<<8|t;this.inputBits+=8}return this.inputBuf>>this.inputBits-e&65535>>16-e}_eatBits(e){(this.inputBits-=e)<0&&(this.inputBits=0)}}class CCITTFaxStream extends DecodeStream{constructor(e,t,a){super(t);this.str=e;this.dict=e.dict;a instanceof Dict||(a=Dict.empty);const r={next:()=>e.getByte()};this.ccittFaxDecoder=new CCITTFaxDecoder(r,{K:a.get("K"),EndOfLine:a.get("EndOfLine"),EncodedByteAlign:a.get("EncodedByteAlign"),Columns:a.get("Columns"),Rows:a.get("Rows"),EndOfBlock:a.get("EndOfBlock"),BlackIs1:a.get("BlackIs1")})}readBlock(){for(;!this.eof;){const e=this.ccittFaxDecoder.readNextChar();if(-1===e){this.eof=!0;return}this.ensureBuffer(this.bufferLength+1);this.buffer[this.bufferLength++]=e}}}const nr=new Int32Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),sr=new Int32Array([3,4,5,6,7,8,9,10,65547,65549,65551,65553,131091,131095,131099,131103,196643,196651,196659,196667,262211,262227,262243,262259,327811,327843,327875,327907,258,258,258]),or=new Int32Array([1,2,3,4,65541,65543,131081,131085,196625,196633,262177,262193,327745,327777,393345,393409,459009,459137,524801,525057,590849,591361,657409,658433,724993,727041,794625,798721,868353,876545]),cr=[new Int32Array([459008,524368,524304,524568,459024,524400,524336,590016,459016,524384,524320,589984,524288,524416,524352,590048,459012,524376,524312,589968,459028,524408,524344,590032,459020,524392,524328,59e4,524296,524424,524360,590064,459010,524372,524308,524572,459026,524404,524340,590024,459018,524388,524324,589992,524292,524420,524356,590056,459014,524380,524316,589976,459030,524412,524348,590040,459022,524396,524332,590008,524300,524428,524364,590072,459009,524370,524306,524570,459025,524402,524338,590020,459017,524386,524322,589988,524290,524418,524354,590052,459013,524378,524314,589972,459029,524410,524346,590036,459021,524394,524330,590004,524298,524426,524362,590068,459011,524374,524310,524574,459027,524406,524342,590028,459019,524390,524326,589996,524294,524422,524358,590060,459015,524382,524318,589980,459031,524414,524350,590044,459023,524398,524334,590012,524302,524430,524366,590076,459008,524369,524305,524569,459024,524401,524337,590018,459016,524385,524321,589986,524289,524417,524353,590050,459012,524377,524313,589970,459028,524409,524345,590034,459020,524393,524329,590002,524297,524425,524361,590066,459010,524373,524309,524573,459026,524405,524341,590026,459018,524389,524325,589994,524293,524421,524357,590058,459014,524381,524317,589978,459030,524413,524349,590042,459022,524397,524333,590010,524301,524429,524365,590074,459009,524371,524307,524571,459025,524403,524339,590022,459017,524387,524323,589990,524291,524419,524355,590054,459013,524379,524315,589974,459029,524411,524347,590038,459021,524395,524331,590006,524299,524427,524363,590070,459011,524375,524311,524575,459027,524407,524343,590030,459019,524391,524327,589998,524295,524423,524359,590062,459015,524383,524319,589982,459031,524415,524351,590046,459023,524399,524335,590014,524303,524431,524367,590078,459008,524368,524304,524568,459024,524400,524336,590017,459016,524384,524320,589985,524288,524416,524352,590049,459012,524376,524312,589969,459028,524408,524344,590033,459020,524392,524328,590001,524296,524424,524360,590065,459010,524372,524308,524572,459026,524404,524340,590025,459018,524388,524324,589993,524292,524420,524356,590057,459014,524380,524316,589977,459030,524412,524348,590041,459022,524396,524332,590009,524300,524428,524364,590073,459009,524370,524306,524570,459025,524402,524338,590021,459017,524386,524322,589989,524290,524418,524354,590053,459013,524378,524314,589973,459029,524410,524346,590037,459021,524394,524330,590005,524298,524426,524362,590069,459011,524374,524310,524574,459027,524406,524342,590029,459019,524390,524326,589997,524294,524422,524358,590061,459015,524382,524318,589981,459031,524414,524350,590045,459023,524398,524334,590013,524302,524430,524366,590077,459008,524369,524305,524569,459024,524401,524337,590019,459016,524385,524321,589987,524289,524417,524353,590051,459012,524377,524313,589971,459028,524409,524345,590035,459020,524393,524329,590003,524297,524425,524361,590067,459010,524373,524309,524573,459026,524405,524341,590027,459018,524389,524325,589995,524293,524421,524357,590059,459014,524381,524317,589979,459030,524413,524349,590043,459022,524397,524333,590011,524301,524429,524365,590075,459009,524371,524307,524571,459025,524403,524339,590023,459017,524387,524323,589991,524291,524419,524355,590055,459013,524379,524315,589975,459029,524411,524347,590039,459021,524395,524331,590007,524299,524427,524363,590071,459011,524375,524311,524575,459027,524407,524343,590031,459019,524391,524327,589999,524295,524423,524359,590063,459015,524383,524319,589983,459031,524415,524351,590047,459023,524399,524335,590015,524303,524431,524367,590079]),9],lr=[new Int32Array([327680,327696,327688,327704,327684,327700,327692,327708,327682,327698,327690,327706,327686,327702,327694,0,327681,327697,327689,327705,327685,327701,327693,327709,327683,327699,327691,327707,327687,327703,327695,0]),5];class FlateStream extends DecodeStream{constructor(e,t){super(t);this.str=e;this.dict=e.dict;const a=e.getByte(),r=e.getByte();if(-1===a||-1===r)throw new FormatError(`Invalid header in flate stream: ${a}, ${r}`);if(8!=(15&a))throw new FormatError(`Unknown compression method in flate stream: ${a}, ${r}`);if(((a<<8)+r)%31!=0)throw new FormatError(`Bad FCHECK in flate stream: ${a}, ${r}`);if(32&r)throw new FormatError(`FDICT bit set in flate stream: ${a}, ${r}`);this.codeSize=0;this.codeBuf=0}async getImageData(e,t){const a=await this.asyncGetBytes();return a?.subarray(0,e)||this.getBytes(e)}async asyncGetBytes(){this.str.reset();const e=this.str.getBytes();try{const{readable:t,writable:a}=new DecompressionStream("deflate"),r=a.getWriter();await r.ready;r.write(e).then((async()=>{await r.ready;await r.close()})).catch((()=>{}));const i=[];let n=0;for await(const e of t){i.push(e);n+=e.byteLength}const s=new Uint8Array(n);let o=0;for(const e of i){s.set(e,o);o+=e.byteLength}return s}catch{this.str=new Stream(e,2,e.length,this.str.dict);this.reset();return null}}get isAsync(){return!0}getBits(e){const t=this.str;let a,r=this.codeSize,i=this.codeBuf;for(;r>e;this.codeSize=r-=e;return a}getCode(e){const t=this.str,a=e[0],r=e[1];let i,n=this.codeSize,s=this.codeBuf;for(;n>16,l=65535&o;if(c<1||n>c;this.codeSize=n-c;return l}generateHuffmanTable(e){const t=e.length;let a,r=0;for(a=0;ar&&(r=e[a]);const i=1<>=1}for(a=e;a>=1;if(0===t){let t;if(-1===(t=r.getByte())){this.#j("Bad block header in flate stream");return}let a=t;if(-1===(t=r.getByte())){this.#j("Bad block header in flate stream");return}a|=t<<8;if(-1===(t=r.getByte())){this.#j("Bad block header in flate stream");return}let i=t;if(-1===(t=r.getByte())){this.#j("Bad block header in flate stream");return}i|=t<<8;if(i!==(65535&~a)&&(0!==a||0!==i))throw new FormatError("Bad uncompressed block length in flate stream");this.codeBuf=0;this.codeSize=0;const n=this.bufferLength,s=n+a;e=this.ensureBuffer(s);this.bufferLength=s;if(0===a)-1===r.peekByte()&&(this.eof=!0);else{const t=r.getBytes(a);e.set(t,n);t.length0;)h[o++]=f}i=this.generateHuffmanTable(h.subarray(0,e));n=this.generateHuffmanTable(h.subarray(e,l))}}e=this.buffer;let s=e?e.length:0,o=this.bufferLength;for(;;){let t=this.getCode(i);if(t<256){if(o+1>=s){e=this.ensureBuffer(o+1);s=e.length}e[o++]=t;continue}if(256===t){this.bufferLength=o;return}t-=257;t=sr[t];let r=t>>16;r>0&&(r=this.getBits(r));a=(65535&t)+r;t=this.getCode(n);t=or[t];r=t>>16;r>0&&(r=this.getBits(r));const c=(65535&t)+r;if(o+a>=s){e=this.ensureBuffer(o+a);s=e.length}for(let t=0;t>9&127;this.clow=this.clow<<7&65535;this.ct-=7;this.a=32768}byteIn(){const e=this.data;let t=this.bp;if(255===e[t])if(e[t+1]>143){this.clow+=65280;this.ct=8}else{t++;this.clow+=e[t]<<9;this.ct=7;this.bp=t}else{t++;this.clow+=t65535){this.chigh+=this.clow>>16;this.clow&=65535}}readBit(e,t){let a=e[t]>>1,r=1&e[t];const i=hr[a],n=i.qe;let s,o=this.a-n;if(this.chigh>15&1;this.clow=this.clow<<1&65535;this.ct--}while(!(32768&o));this.a=o;e[t]=a<<1|r;return s}}class Jbig2Error extends fa{constructor(e){super(e,"Jbig2Error")}}class ContextCache{getContexts(e){return e in this?this[e]:this[e]=new Int8Array(65536)}}class DecodingContext{constructor(e,t,a){this.data=e;this.start=t;this.end=a}get decoder(){return shadow(this,"decoder",new ArithmeticDecoder(this.data,this.start,this.end))}get contextCache(){return shadow(this,"contextCache",new ContextCache)}}function decodeInteger(e,t,a){const r=e.getContexts(t);let i=1;function readBits(e){let t=0;for(let n=0;n>>0}const n=readBits(1),s=readBits(1)?readBits(1)?readBits(1)?readBits(1)?readBits(1)?readBits(32)+4436:readBits(12)+340:readBits(8)+84:readBits(6)+20:readBits(4)+4:readBits(2);let o;0===n?o=s:s>0&&(o=-s);return o>=-2147483648&&o<=va?o:null}function decodeIAID(e,t,a){const r=e.getContexts("IAID");let i=1;for(let e=0;ee.y-t.y||e.x-t.x));const h=l.length,u=new Int8Array(h),d=new Int8Array(h),f=[];let g,p,m=0,b=0,y=0,w=0;for(p=0;p=v&&E=F){q=q<<1&m;for(p=0;p=0&&_=0){j=D[L][_];j&&(q|=j<=e?l<<=1:l=l<<1|x[o][c]}for(f=0;f=w||c<0||c>=y?l<<=1:l=l<<1|r[o][c]}const g=k.readBit(C,l);t[s]=g}}return x}function decodeTextRegion(e,t,a,r,i,n,s,o,c,l,h,u,d,f,g,p,m,b,y){if(e&&t)throw new Jbig2Error("refinement with Huffman is not supported");const w=[];let S,x;for(S=0;S1&&(i=e?y.readBits(b):decodeInteger(C,"IAIT",k));const n=s*v+i,F=e?f.symbolIDTable.decode(y):decodeIAID(C,k,c),O=t&&(e?y.readBit():decodeInteger(C,"IARI",k));let T=o[F],M=T[0].length,D=T.length;if(O){const e=decodeInteger(C,"IARDW",k),t=decodeInteger(C,"IARDH",k);M+=e;D+=t;T=decodeRefinement(M,D,g,T,(e>>1)+decodeInteger(C,"IARDX",k),(t>>1)+decodeInteger(C,"IARDY",k),!1,p,m)}let R=0;l?1&u?R=D-1:r+=D-1:u>1?r+=M-1:R=M-1;const N=n-(1&u?0:D-1),E=r-(2&u?M-1:0);let L,_,j;if(l)for(L=0;L>5&7;const c=[31&s];let l=t+6;if(7===s){o=536870911&readUint32(e,l-1);l+=3;let t=o+7>>3;c[0]=e[l++];for(;--t>0;)c.push(e[l++])}else if(5===s||6===s)throw new Jbig2Error("invalid referred-to flags");a.retainBits=c;let h=4;a.number<=256?h=1:a.number<=65536&&(h=2);const u=[];let d,f;for(d=0;d>>24&255;n[3]=t.height>>16&255;n[4]=t.height>>8&255;n[5]=255&t.height;for(d=l,f=e.length;d>2&3;e.huffmanDWSelector=t>>4&3;e.bitmapSizeSelector=t>>6&1;e.aggregationInstancesSelector=t>>7&1;e.bitmapCodingContextUsed=!!(256&t);e.bitmapCodingContextRetained=!!(512&t);e.template=t>>10&3;e.refinementTemplate=t>>12&1;l+=2;if(!e.huffman){c=0===e.template?4:1;s=[];for(o=0;o>2&3;h.stripSize=1<>4&3;h.transposed=!!(64&u);h.combinationOperator=u>>7&3;h.defaultPixelValue=u>>9&1;h.dsOffset=u<<17>>27;h.refinementTemplate=u>>15&1;if(h.huffman){const e=readUint16(r,l);l+=2;h.huffmanFS=3&e;h.huffmanDS=e>>2&3;h.huffmanDT=e>>4&3;h.huffmanRefinementDW=e>>6&3;h.huffmanRefinementDH=e>>8&3;h.huffmanRefinementDX=e>>10&3;h.huffmanRefinementDY=e>>12&3;h.huffmanRefinementSizeSelector=!!(16384&e)}if(h.refinement&&!h.refinementTemplate){s=[];for(o=0;o<2;o++){s.push({x:readInt8(r,l),y:readInt8(r,l+1)});l+=2}h.refinementAt=s}h.numberOfSymbolInstances=readUint32(r,l);l+=4;n=[h,a.referredTo,r,l,i];break;case 16:const d={},f=r[l++];d.mmr=!!(1&f);d.template=f>>1&3;d.patternWidth=r[l++];d.patternHeight=r[l++];d.maxPatternIndex=readUint32(r,l);l+=4;n=[d,a.number,r,l,i];break;case 22:case 23:const g={};g.info=readRegionSegmentInformation(r,l);l+=mr;const p=r[l++];g.mmr=!!(1&p);g.template=p>>1&3;g.enableSkip=!!(8&p);g.combinationOperator=p>>4&7;g.defaultPixelValue=p>>7&1;g.gridWidth=readUint32(r,l);l+=4;g.gridHeight=readUint32(r,l);l+=4;g.gridOffsetX=4294967295&readUint32(r,l);l+=4;g.gridOffsetY=4294967295&readUint32(r,l);l+=4;g.gridVectorX=readUint16(r,l);l+=2;g.gridVectorY=readUint16(r,l);l+=2;n=[g,a.referredTo,r,l,i];break;case 38:case 39:const m={};m.info=readRegionSegmentInformation(r,l);l+=mr;const b=r[l++];m.mmr=!!(1&b);m.template=b>>1&3;m.prediction=!!(8&b);if(!m.mmr){c=0===m.template?4:1;s=[];for(o=0;o>2&1;y.combinationOperator=w>>3&3;y.requiresBuffer=!!(32&w);y.combinationOperatorOverride=!!(64&w);n=[y];break;case 49:case 50:case 51:case 62:break;case 53:n=[a.number,r,l,i];break;default:throw new Jbig2Error(`segment type ${a.typeName}(${a.type}) is not implemented`)}const h="on"+a.typeName;h in t&&t[h].apply(t,n)}function processSegments(e,t){for(let a=0,r=e.length;a>3,a=new Uint8ClampedArray(t*e.height);e.defaultPixelValue&&a.fill(255);this.buffer=a}drawBitmap(e,t){const a=this.currentPageInfo,r=e.width,i=e.height,n=a.width+7>>3,s=a.combinationOperatorOverride?e.combinationOperator:a.combinationOperator,o=this.buffer,c=128>>(7&e.x);let l,h,u,d,f=e.y*n+(e.x>>3);switch(s){case 0:for(l=0;l>=1;if(!u){u=128;d++}}f+=n}break;case 2:for(l=0;l>=1;if(!u){u=128;d++}}f+=n}break;default:throw new Jbig2Error(`operator ${s} is not supported`)}}onImmediateGenericRegion(e,t,a,r){const i=e.info,n=new DecodingContext(t,a,r),s=decodeBitmap(e.mmr,i.width,i.height,e.template,e.prediction,null,e.at,n);this.drawBitmap(i,s)}onImmediateLosslessGenericRegion(){this.onImmediateGenericRegion(...arguments)}onSymbolDictionary(e,t,a,r,i,n){let s,o;if(e.huffman){s=function getSymbolDictionaryHuffmanTables(e,t,a){let r,i,n,s,o=0;switch(e.huffmanDHSelector){case 0:case 1:r=getStandardTable(e.huffmanDHSelector+4);break;case 3:r=getCustomHuffmanTable(o,t,a);o++;break;default:throw new Jbig2Error("invalid Huffman DH selector")}switch(e.huffmanDWSelector){case 0:case 1:i=getStandardTable(e.huffmanDWSelector+2);break;case 3:i=getCustomHuffmanTable(o,t,a);o++;break;default:throw new Jbig2Error("invalid Huffman DW selector")}if(e.bitmapSizeSelector){n=getCustomHuffmanTable(o,t,a);o++}else n=getStandardTable(1);s=e.aggregationInstancesSelector?getCustomHuffmanTable(o,t,a):getStandardTable(1);return{tableDeltaHeight:r,tableDeltaWidth:i,tableBitmapSize:n,tableAggregateInstances:s}}(e,a,this.customTables);o=new Reader(r,i,n)}let c=this.symbols;c||(this.symbols=c={});const l=[];for(const e of a){const t=c[e];t&&l.push(...t)}const h=new DecodingContext(r,i,n);c[t]=function decodeSymbolDictionary(e,t,a,r,i,n,s,o,c,l,h,u){if(e&&t)throw new Jbig2Error("symbol refinement with Huffman is not supported");const d=[];let f=0,g=log2(a.length+r);const p=h.decoder,m=h.contextCache;let b,y;if(e){b=getStandardTable(1);y=[];g=Math.max(g,1)}for(;d.length1)w=decodeTextRegion(e,t,r,f,0,i,1,a.concat(d),g,0,0,1,0,n,c,l,h,0,u);else{const e=decodeIAID(m,p,g),t=decodeInteger(m,"IARDX",p),i=decodeInteger(m,"IARDY",p);w=decodeRefinement(r,f,c,e=32){let a,r,s;switch(t){case 32:if(0===e)throw new Jbig2Error("no previous value in symbol ID table");r=i.readBits(2)+3;a=n[e-1].prefixLength;break;case 33:r=i.readBits(3)+3;a=0;break;case 34:r=i.readBits(7)+11;a=0;break;default:throw new Jbig2Error("invalid code length in symbol ID table")}for(s=0;s=0;m--){T=e?decodeMMRBitmap(O,c,l,!0):decodeBitmap(!1,c,l,a,!1,null,v,g);F[m]=T}for(M=0;M=0;b--){R^=F[b][M][D];N|=R<>8;_=u+M*d-D*f>>8;if(L>=0&&L+x<=r&&_>=0&&_+k<=i)for(m=0;m=i)){U=p[t];j=E[m];for(b=0;b=0&&e>1&7),c=1+(r>>4&7),l=[];let h,u,d=i;do{h=s.readBits(o);u=s.readBits(c);l.push(new HuffmanLine([d,h,u,0]));d+=1<>t&1;if(t<=0)this.children[a]=new HuffmanTreeNode(e);else{let r=this.children[a];r||(this.children[a]=r=new HuffmanTreeNode(null));r.buildTree(e,t-1)}}decodeNode(e){if(this.isLeaf){if(this.isOOB)return null;const t=e.readBits(this.rangeLength);return this.rangeLow+(this.isLowerRange?-t:t)}const t=this.children[e.readBit()];if(!t)throw new Jbig2Error("invalid Huffman data");return t.decodeNode(e)}}class HuffmanTable{constructor(e,t){t||this.assignPrefixCodes(e);this.rootNode=new HuffmanTreeNode(null);for(let t=0,a=e.length;t0&&this.rootNode.buildTree(a,a.prefixLength-1)}}decode(e){return this.rootNode.decodeNode(e)}assignPrefixCodes(e){const t=e.length;let a=0;for(let r=0;r=this.end)throw new Jbig2Error("end of data while reading bit");this.currentByte=this.data[this.position++];this.shift=7}const e=this.currentByte>>this.shift&1;this.shift--;return e}readBits(e){let t,a=0;for(t=e-1;t>=0;t--)a|=this.readBit()<=this.end?-1:this.data[this.position++]}}function getCustomHuffmanTable(e,t,a){let r=0;for(let i=0,n=t.length;i>a&1;a--}}if(r&&!o){const e=5;for(let t=0;t>>t&(1<0;if(e<256){d[0]=e;f=1}else{if(!(e>=258)){if(256===e){h=9;s=258;f=0;continue}this.eof=!0;delete this.lzwState;break}if(e=0;t--){d[t]=o[a];a=l[a]}}else d[f++]=d[0]}if(i){l[s]=u;c[s]=c[u]+1;o[s]=d[0];s++;h=s+n&s+n-1?h:0|Math.min(Math.log(s+n)/.6931471805599453+1,12)}u=e;g+=f;if(r15))throw new FormatError(`Unsupported predictor: ${r}`);this.readBlock=2===r?this.readBlockTiff:this.readBlockPng;this.str=e;this.dict=e.dict;const i=this.colors=a.get("Colors")||1,n=this.bits=a.get("BPC","BitsPerComponent")||8,s=this.columns=a.get("Columns")||1;this.pixBytes=i*n+7>>3;this.rowBytes=s*i*n+7>>3;return this}readBlockTiff(){const e=this.rowBytes,t=this.bufferLength,a=this.ensureBuffer(t+e),r=this.bits,i=this.colors,n=this.str.getBytes(e);this.eof=!n.length;if(this.eof)return;let s,o=0,c=0,l=0,h=0,u=t;if(1===r&&1===i)for(s=0;s>1;e^=e>>2;e^=e>>4;o=(1&e)<<7;a[u++]=e}else if(8===r){for(s=0;s>8&255;a[u++]=255&e}}else{const e=new Uint8Array(i+1),u=(1<>l-r)&u;l-=r;c=c<=8){a[f++]=c>>h-8&255;h-=8}}h>0&&(a[f++]=(c<<8-h)+(o&(1<<8-h)-1))}this.bufferLength+=e}readBlockPng(){const e=this.rowBytes,t=this.pixBytes,a=this.str.getByte(),r=this.str.getBytes(e);this.eof=!r.length;if(this.eof)return;const i=this.bufferLength,n=this.ensureBuffer(i+e);let s=n.subarray(i-e,i);0===s.length&&(s=new Uint8Array(e));let o,c,l,h=i;switch(a){case 0:for(o=0;o>1)+r[o];for(;o>1)+r[o]&255;h++}break;case 4:for(o=0;o0){const e=this.str.getBytes(r);t.set(e,a);a+=r}}else{r=257-r;t=this.ensureBuffer(a+r+1);t.fill(e[1],a,a+r);a+=r}this.bufferLength=a}}class Parser{constructor({lexer:e,xref:t,allowStreams:a=!1,recoveryMode:r=!1}){this.lexer=e;this.xref=t;this.allowStreams=a;this.recoveryMode=r;this.imageCache=Object.create(null);this._imageId=0;this.refill()}refill(){this.buf1=this.lexer.getObj();this.buf2=this.lexer.getObj()}shift(){if(this.buf2 instanceof Cmd&&"ID"===this.buf2.cmd){this.buf1=this.buf2;this.buf2=null}else{this.buf1=this.buf2;this.buf2=this.lexer.getObj()}}tryShift(){try{this.shift();return!0}catch(e){if(e instanceof MissingDataException)throw e;return!1}}getObj(e=null){const t=this.buf1;this.shift();if(t instanceof Cmd)switch(t.cmd){case"BI":return this.makeInlineImage(e);case"[":const a=[];for(;!isCmd(this.buf1,"]")&&this.buf1!==wa;)a.push(this.getObj(e));if(this.buf1===wa){if(this.recoveryMode)return a;throw new ParserEOFException("End of file inside array.")}this.shift();return a;case"<<":const r=new Dict(this.xref);for(;!isCmd(this.buf1,">>")&&this.buf1!==wa;){if(!(this.buf1 instanceof Name)){info("Malformed dictionary: key must be a name object");this.shift();continue}const t=this.buf1.name;this.shift();if(this.buf1===wa)break;r.set(t,this.getObj(e))}if(this.buf1===wa){if(this.recoveryMode)return r;throw new ParserEOFException("End of file inside dictionary.")}if(isCmd(this.buf2,"stream"))return this.allowStreams?this.makeStream(r,e):r;this.shift();return r;default:return t}if(Number.isInteger(t)){if(Number.isInteger(this.buf1)&&isCmd(this.buf2,"R")){const e=Ref.get(t,this.buf1);this.shift();this.shift();return e}return t}return"string"==typeof t&&e?e.decryptString(t):t}findDefaultInlineStreamEnd(e){const{knownCommands:t}=this.lexer,a=e.pos;let r,i,n=0;for(;-1!==(r=e.getByte());)if(0===n)n=69===r?1:0;else if(1===n)n=73===r?2:0;else if(32===r||10===r||13===r){i=e.pos;const a=e.peekBytes(15),s=a.length;if(0===s)break;for(let e=0;e127))){n=0;break}}if(2!==n)continue;if(!t){warn("findDefaultInlineStreamEnd - `lexer.knownCommands` is undefined.");continue}const o=new Lexer(new Stream(e.peekBytes(75)),t);o._hexStringWarn=()=>{};let c=0;for(;;){const e=o.getObj();if(e===wa){n=0;break}if(e instanceof Cmd){const a=t[e.cmd];if(!a){n=0;break}if(a.variableArgs?c<=a.numArgs:c===a.numArgs)break;c=0}else c++}if(2===n)break}else n=0;if(-1===r){warn("findDefaultInlineStreamEnd: Reached the end of the stream without finding a valid EI marker");if(i){warn('... trying to recover by using the last "EI" occurrence.');e.skip(-(e.pos-i))}}let s=4;e.skip(-s);r=e.peekByte();e.skip(s);isWhiteSpace(r)||s--;return e.pos-s-a}findDCTDecodeInlineStreamEnd(e){const t=e.pos;let a,r,i=!1;for(;-1!==(a=e.getByte());)if(255===a){switch(e.getByte()){case 0:break;case 255:e.skip(-1);break;case 217:i=!0;break;case 192:case 193:case 194:case 195:case 197:case 198:case 199:case 201:case 202:case 203:case 205:case 206:case 207:case 196:case 204:case 218:case 219:case 220:case 221:case 222:case 223:case 224:case 225:case 226:case 227:case 228:case 229:case 230:case 231:case 232:case 233:case 234:case 235:case 236:case 237:case 238:case 239:case 254:r=e.getUint16();r>2?e.skip(r-2):e.skip(-2)}if(i)break}const n=e.pos-t;if(-1===a){warn("Inline DCTDecode image stream: EOI marker not found, searching for /EI/ instead.");e.skip(-n);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return n}findASCII85DecodeInlineStreamEnd(e){const t=e.pos;let a;for(;-1!==(a=e.getByte());)if(126===a){const t=e.pos;a=e.peekByte();for(;isWhiteSpace(a);){e.skip();a=e.peekByte()}if(62===a){e.skip();break}if(e.pos>t){const t=e.peekBytes(2);if(69===t[0]&&73===t[1])break}}const r=e.pos-t;if(-1===a){warn("Inline ASCII85Decode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-r);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return r}findASCIIHexDecodeInlineStreamEnd(e){const t=e.pos;let a;for(;-1!==(a=e.getByte())&&62!==a;);const r=e.pos-t;if(-1===a){warn("Inline ASCIIHexDecode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-r);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return r}inlineStreamSkipEI(e){let t,a=0;for(;-1!==(t=e.getByte());)if(0===a)a=69===t?1:0;else if(1===a)a=73===t?2:0;else if(2===a)break}makeInlineImage(e){const t=this.lexer,a=t.stream,r=Object.create(null);let i;for(;!isCmd(this.buf1,"ID")&&this.buf1!==wa;){if(!(this.buf1 instanceof Name))throw new FormatError("Dictionary key must be a name object");const t=this.buf1.name;this.shift();if(this.buf1===wa)break;r[t]=this.getObj(e)}-1!==t.beginInlineImagePos&&(i=a.pos-t.beginInlineImagePos);const n=this.xref.fetchIfRef(r.F||r.Filter);let s;if(n instanceof Name)s=n.name;else if(Array.isArray(n)){const e=this.xref.fetchIfRef(n[0]);e instanceof Name&&(s=e.name)}const o=a.pos;let c,l;switch(s){case"DCT":case"DCTDecode":c=this.findDCTDecodeInlineStreamEnd(a);break;case"A85":case"ASCII85Decode":c=this.findASCII85DecodeInlineStreamEnd(a);break;case"AHx":case"ASCIIHexDecode":c=this.findASCIIHexDecodeInlineStreamEnd(a);break;default:c=this.findDefaultInlineStreamEnd(a)}if(c<1e3&&i>0){const e=a.pos;a.pos=t.beginInlineImagePos;l=function getInlineImageCacheKey(e){const t=[],a=e.length;let r=0;for(;r=r){let r=!1;for(const e of i){const t=e.length;let i=0;for(;i=n){r=!0;break}if(i>=t){if(isWhiteSpace(s[c+o+i])){info(`Found "${bytesToString([...a,...e])}" when searching for endstream command.`);r=!0}break}}if(r){t.pos+=c;return t.pos-e}}c++}t.pos+=o}return-1}makeStream(e,t){const a=this.lexer;let r=a.stream;a.skipToNextLine();const i=r.pos-1;let n=e.get("Length");if(!Number.isInteger(n)){info(`Bad length "${n&&n.toString()}" in stream.`);n=0}r.pos=i+n;a.nextChar();if(this.tryShift()&&isCmd(this.buf2,"endstream"))this.shift();else{n=this.#U(i);if(n<0)throw new FormatError("Missing endstream command.");a.nextChar();this.shift();this.shift()}this.shift();r=r.makeSubStream(i,n,e);t&&(r=t.createStream(r,n));r=this.filter(r,e,n);r.dict=e;return r}filter(e,t,a){let r=t.get("F","Filter"),i=t.get("DP","DecodeParms");if(r instanceof Name){Array.isArray(i)&&warn("/DecodeParms should not be an Array, when /Filter is a Name.");return this.makeFilter(e,r.name,a,i)}let n=a;if(Array.isArray(r)){const t=r,a=i;for(let s=0,o=t.length;s=48&&e<=57?15&e:e>=65&&e<=70||e>=97&&e<=102?9+(15&e):-1}class Lexer{constructor(e,t=null){this.stream=e;this.nextChar();this.strBuf=[];this.knownCommands=t;this._hexStringNumWarn=0;this.beginInlineImagePos=-1}nextChar(){return this.currentChar=this.stream.getByte()}peekChar(){return this.stream.peekByte()}getNumber(){let e=this.currentChar,t=!1,a=0,r=1;if(45===e){r=-1;e=this.nextChar();45===e&&(e=this.nextChar())}else 43===e&&(e=this.nextChar());if(10===e||13===e)do{e=this.nextChar()}while(10===e||13===e);if(46===e){a=10;e=this.nextChar()}if(e<48||e>57){const t=`Invalid number: ${String.fromCharCode(e)} (charCode ${e})`;if(isWhiteSpace(e)||40===e||60===e||-1===e){info(`Lexer.getNumber - "${t}".`);return 0}throw new FormatError(t)}let i=e-48,n=0,s=1;for(;(e=this.nextChar())>=0;)if(e>=48&&e<=57){const r=e-48;if(t)n=10*n+r;else{0!==a&&(a*=10);i=10*i+r}}else if(46===e){if(0!==a)break;a=1}else if(45===e)warn("Badly formatted number: minus sign in the middle");else{if(69!==e&&101!==e)break;e=this.peekChar();if(43===e||45===e){s=45===e?-1:1;this.nextChar()}else if(e<48||e>57)break;t=!0}0!==a&&(i/=a);t&&(i*=10**(s*n));return r*i}getString(){let e=1,t=!1;const a=this.strBuf;a.length=0;let r=this.nextChar();for(;;){let i=!1;switch(0|r){case-1:warn("Unterminated string");t=!0;break;case 40:++e;a.push("(");break;case 41:if(0==--e){this.nextChar();t=!0}else a.push(")");break;case 92:r=this.nextChar();switch(r){case-1:warn("Unterminated string");t=!0;break;case 110:a.push("\n");break;case 114:a.push("\r");break;case 116:a.push("\t");break;case 98:a.push("\b");break;case 102:a.push("\f");break;case 92:case 40:case 41:a.push(String.fromCharCode(r));break;case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:let e=15&r;r=this.nextChar();i=!0;if(r>=48&&r<=55){e=(e<<3)+(15&r);r=this.nextChar();if(r>=48&&r<=55){i=!1;e=(e<<3)+(15&r)}}a.push(String.fromCharCode(e));break;case 13:10===this.peekChar()&&this.nextChar();break;case 10:break;default:a.push(String.fromCharCode(r))}break;default:a.push(String.fromCharCode(r))}if(t)break;i||(r=this.nextChar())}return a.join("")}getName(){let e,t;const a=this.strBuf;a.length=0;for(;(e=this.nextChar())>=0&&!yr[e];)if(35===e){e=this.nextChar();if(yr[e]){warn("Lexer_getName: NUMBER SIGN (#) should be followed by a hexadecimal number.");a.push("#");break}const r=toHexDigit(e);if(-1!==r){t=e;e=this.nextChar();const i=toHexDigit(e);if(-1===i){warn(`Lexer_getName: Illegal digit (${String.fromCharCode(e)}) in hexadecimal number.`);a.push("#",String.fromCharCode(t));if(yr[e])break;a.push(String.fromCharCode(e));continue}a.push(String.fromCharCode(r<<4|i))}else a.push("#",String.fromCharCode(e))}else a.push(String.fromCharCode(e));a.length>127&&warn(`Name token is longer than allowed by the spec: ${a.length}`);return Name.get(a.join(""))}_hexStringWarn(e){5!=this._hexStringNumWarn++?this._hexStringNumWarn>5||warn(`getHexString - ignoring invalid character: ${e}`):warn("getHexString - ignoring additional invalid characters.")}getHexString(){const e=this.strBuf;e.length=0;let t=this.currentChar,a=-1,r=-1;this._hexStringNumWarn=0;for(;;){if(t<0){warn("Unterminated hex string");break}if(62===t){this.nextChar();break}if(1!==yr[t]){r=toHexDigit(t);if(-1===r)this._hexStringWarn(t);else if(-1===a)a=r;else{e.push(String.fromCharCode(a<<4|r));a=-1}t=this.nextChar()}else t=this.nextChar()}-1!==a&&e.push(String.fromCharCode(a<<4));return e.join("")}getObj(){let e=!1,t=this.currentChar;for(;;){if(t<0)return wa;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(1!==yr[t])break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return this.getNumber();case 40:return this.getString();case 47:return this.getName();case 91:this.nextChar();return Cmd.get("[");case 93:this.nextChar();return Cmd.get("]");case 60:t=this.nextChar();if(60===t){this.nextChar();return Cmd.get("<<")}return this.getHexString();case 62:t=this.nextChar();if(62===t){this.nextChar();return Cmd.get(">>")}return Cmd.get(">");case 123:this.nextChar();return Cmd.get("{");case 125:this.nextChar();return Cmd.get("}");case 41:this.nextChar();throw new FormatError(`Illegal character: ${t}`)}let a=String.fromCharCode(t);if(t<32||t>127){const e=this.peekChar();if(e>=32&&e<=127){this.nextChar();return Cmd.get(a)}}const r=this.knownCommands;let i=void 0!==r?.[a];for(;(t=this.nextChar())>=0&&!yr[t];){const e=a+String.fromCharCode(t);if(i&&void 0===r[e])break;if(128===a.length)throw new FormatError(`Command token too long: ${a.length}`);a=e;i=void 0!==r?.[a]}if("true"===a)return!0;if("false"===a)return!1;if("null"===a)return null;"BI"===a&&(this.beginInlineImagePos=this.stream.pos);return Cmd.get(a)}skipToNextLine(){let e=this.currentChar;for(;e>=0;){if(13===e){e=this.nextChar();10===e&&this.nextChar();break}if(10===e){this.nextChar();break}e=this.nextChar()}}}class Linearization{static create(e){function getInt(e,t,a=!1){const r=e.get(t);if(Number.isInteger(r)&&(a?r>=0:r>0))return r;throw new Error(`The "${t}" parameter in the linearization dictionary is invalid.`)}const t=new Parser({lexer:new Lexer(e),xref:null}),a=t.getObj(),r=t.getObj(),i=t.getObj(),n=t.getObj();let s,o;if(!(Number.isInteger(a)&&Number.isInteger(r)&&isCmd(i,"obj")&&n instanceof Dict&&"number"==typeof(s=n.get("Linearized"))&&s>0))return null;if((o=getInt(n,"L"))!==e.length)throw new Error('The "L" parameter in the linearization dictionary does not equal the stream length.');return{length:o,hints:function getHints(e){const t=e.get("H");let a;if(Array.isArray(t)&&(2===(a=t.length)||4===a)){for(let e=0;e0))throw new Error(`Hint (${e}) in the linearization dictionary is invalid.`)}return t}throw new Error("Hint array in the linearization dictionary is invalid.")}(n),objectNumberFirst:getInt(n,"O"),endFirst:getInt(n,"E"),numPages:getInt(n,"N"),mainXRefEntriesOffset:getInt(n,"T"),pageFirst:n.has("P")?getInt(n,"P",!0):0}}}const wr=["Adobe-GB1-UCS2","Adobe-CNS1-UCS2","Adobe-Japan1-UCS2","Adobe-Korea1-UCS2","78-EUC-H","78-EUC-V","78-H","78-RKSJ-H","78-RKSJ-V","78-V","78ms-RKSJ-H","78ms-RKSJ-V","83pv-RKSJ-H","90ms-RKSJ-H","90ms-RKSJ-V","90msp-RKSJ-H","90msp-RKSJ-V","90pv-RKSJ-H","90pv-RKSJ-V","Add-H","Add-RKSJ-H","Add-RKSJ-V","Add-V","Adobe-CNS1-0","Adobe-CNS1-1","Adobe-CNS1-2","Adobe-CNS1-3","Adobe-CNS1-4","Adobe-CNS1-5","Adobe-CNS1-6","Adobe-GB1-0","Adobe-GB1-1","Adobe-GB1-2","Adobe-GB1-3","Adobe-GB1-4","Adobe-GB1-5","Adobe-Japan1-0","Adobe-Japan1-1","Adobe-Japan1-2","Adobe-Japan1-3","Adobe-Japan1-4","Adobe-Japan1-5","Adobe-Japan1-6","Adobe-Korea1-0","Adobe-Korea1-1","Adobe-Korea1-2","B5-H","B5-V","B5pc-H","B5pc-V","CNS-EUC-H","CNS-EUC-V","CNS1-H","CNS1-V","CNS2-H","CNS2-V","ETHK-B5-H","ETHK-B5-V","ETen-B5-H","ETen-B5-V","ETenms-B5-H","ETenms-B5-V","EUC-H","EUC-V","Ext-H","Ext-RKSJ-H","Ext-RKSJ-V","Ext-V","GB-EUC-H","GB-EUC-V","GB-H","GB-V","GBK-EUC-H","GBK-EUC-V","GBK2K-H","GBK2K-V","GBKp-EUC-H","GBKp-EUC-V","GBT-EUC-H","GBT-EUC-V","GBT-H","GBT-V","GBTpc-EUC-H","GBTpc-EUC-V","GBpc-EUC-H","GBpc-EUC-V","H","HKdla-B5-H","HKdla-B5-V","HKdlb-B5-H","HKdlb-B5-V","HKgccs-B5-H","HKgccs-B5-V","HKm314-B5-H","HKm314-B5-V","HKm471-B5-H","HKm471-B5-V","HKscs-B5-H","HKscs-B5-V","Hankaku","Hiragana","KSC-EUC-H","KSC-EUC-V","KSC-H","KSC-Johab-H","KSC-Johab-V","KSC-V","KSCms-UHC-H","KSCms-UHC-HW-H","KSCms-UHC-HW-V","KSCms-UHC-V","KSCpc-EUC-H","KSCpc-EUC-V","Katakana","NWP-H","NWP-V","RKSJ-H","RKSJ-V","Roman","UniCNS-UCS2-H","UniCNS-UCS2-V","UniCNS-UTF16-H","UniCNS-UTF16-V","UniCNS-UTF32-H","UniCNS-UTF32-V","UniCNS-UTF8-H","UniCNS-UTF8-V","UniGB-UCS2-H","UniGB-UCS2-V","UniGB-UTF16-H","UniGB-UTF16-V","UniGB-UTF32-H","UniGB-UTF32-V","UniGB-UTF8-H","UniGB-UTF8-V","UniJIS-UCS2-H","UniJIS-UCS2-HW-H","UniJIS-UCS2-HW-V","UniJIS-UCS2-V","UniJIS-UTF16-H","UniJIS-UTF16-V","UniJIS-UTF32-H","UniJIS-UTF32-V","UniJIS-UTF8-H","UniJIS-UTF8-V","UniJIS2004-UTF16-H","UniJIS2004-UTF16-V","UniJIS2004-UTF32-H","UniJIS2004-UTF32-V","UniJIS2004-UTF8-H","UniJIS2004-UTF8-V","UniJISPro-UCS2-HW-V","UniJISPro-UCS2-V","UniJISPro-UTF8-V","UniJISX0213-UTF32-H","UniJISX0213-UTF32-V","UniJISX02132004-UTF32-H","UniJISX02132004-UTF32-V","UniKS-UCS2-H","UniKS-UCS2-V","UniKS-UTF16-H","UniKS-UTF16-V","UniKS-UTF32-H","UniKS-UTF32-V","UniKS-UTF8-H","UniKS-UTF8-V","V","WP-Symbol"],Sr=2**24-1;class CMap{constructor(e=!1){this.codespaceRanges=[[],[],[],[]];this.numCodespaceRanges=0;this._map=[];this.name="";this.vertical=!1;this.useCMap=null;this.builtInCMap=e}addCodespaceRange(e,t,a){this.codespaceRanges[e-1].push(t,a);this.numCodespaceRanges++}mapCidRange(e,t,a){if(t-e>Sr)throw new Error("mapCidRange - ignoring data above MAX_MAP_RANGE.");for(;e<=t;)this._map[e++]=a++}mapBfRange(e,t,a){if(t-e>Sr)throw new Error("mapBfRange - ignoring data above MAX_MAP_RANGE.");const r=a.length-1;for(;e<=t;){this._map[e++]=a;const t=a.charCodeAt(r)+1;t>255?a=a.substring(0,r-1)+String.fromCharCode(a.charCodeAt(r-1)+1)+"\0":a=a.substring(0,r)+String.fromCharCode(t)}}mapBfRangeToArray(e,t,a){if(t-e>Sr)throw new Error("mapBfRangeToArray - ignoring data above MAX_MAP_RANGE.");const r=a.length;let i=0;for(;e<=t&&i>>0;const s=i[n];for(let e=0,t=s.length;e=t&&r<=i){a.charcode=r;a.length=n+1;return}}}a.charcode=0;a.length=1}getCharCodeLength(e){const t=this.codespaceRanges;for(let a=0,r=t.length;a=i&&e<=n)return a+1}}return 1}get length(){return this._map.length}get isIdentityCMap(){if("Identity-H"!==this.name&&"Identity-V"!==this.name)return!1;if(65536!==this._map.length)return!1;for(let e=0;e<65536;e++)if(this._map[e]!==e)return!1;return!0}}class IdentityCMap extends CMap{constructor(e,t){super();this.vertical=e;this.addCodespaceRange(t,0,65535)}mapCidRange(e,t,a){unreachable("should not call mapCidRange")}mapBfRange(e,t,a){unreachable("should not call mapBfRange")}mapBfRangeToArray(e,t,a){unreachable("should not call mapBfRangeToArray")}mapOne(e,t){unreachable("should not call mapCidOne")}lookup(e){return Number.isInteger(e)&&e<=65535?e:void 0}contains(e){return Number.isInteger(e)&&e<=65535}forEach(e){for(let t=0;t<=65535;t++)e(t,t)}charCodeOf(e){return Number.isInteger(e)&&e<=65535?e:-1}getMap(){const e=new Array(65536);for(let t=0;t<=65535;t++)e[t]=t;return e}get length(){return 65536}get isIdentityCMap(){unreachable("should not access .isIdentityCMap")}}function strToInt(e){let t=0;for(let a=0;a>>0}function expectString(e){if("string"!=typeof e)throw new FormatError("Malformed CMap: expected string.")}function expectInt(e){if(!Number.isInteger(e))throw new FormatError("Malformed CMap: expected int.")}function parseBfChar(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endbfchar"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectString(a);const i=a;e.mapOne(r,i)}}function parseBfRange(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endbfrange"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectString(a);const i=strToInt(a);a=t.getObj();if(Number.isInteger(a)||"string"==typeof a){const t=Number.isInteger(a)?String.fromCharCode(a):a;e.mapBfRange(r,i,t)}else{if(!isCmd(a,"["))break;{a=t.getObj();const n=[];for(;!isCmd(a,"]")&&a!==wa;){n.push(a);a=t.getObj()}e.mapBfRangeToArray(r,i,n)}}}throw new FormatError("Invalid bf range.")}function parseCidChar(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endcidchar"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectInt(a);const i=a;e.mapOne(r,i)}}function parseCidRange(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endcidrange"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectString(a);const i=strToInt(a);a=t.getObj();expectInt(a);const n=a;e.mapCidRange(r,i,n)}}function parseCodespaceRange(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endcodespacerange"))return;if("string"!=typeof a)break;const r=strToInt(a);a=t.getObj();if("string"!=typeof a)break;const i=strToInt(a);e.addCodespaceRange(a.length,r,i)}throw new FormatError("Invalid codespace range.")}function parseWMode(e,t){const a=t.getObj();Number.isInteger(a)&&(e.vertical=!!a)}function parseCMapName(e,t){const a=t.getObj();a instanceof Name&&(e.name=a.name)}async function parseCMap(e,t,a,r){let i,n;e:for(;;)try{const a=t.getObj();if(a===wa)break;if(a instanceof Name){"WMode"===a.name?parseWMode(e,t):"CMapName"===a.name&&parseCMapName(e,t);i=a}else if(a instanceof Cmd)switch(a.cmd){case"endcmap":break e;case"usecmap":i instanceof Name&&(n=i.name);break;case"begincodespacerange":parseCodespaceRange(e,t);break;case"beginbfchar":parseBfChar(e,t);break;case"begincidchar":parseCidChar(e,t);break;case"beginbfrange":parseBfRange(e,t);break;case"begincidrange":parseCidRange(e,t)}}catch(e){if(e instanceof MissingDataException)throw e;warn("Invalid cMap data: "+e);continue}!r&&n&&(r=n);return r?extendCMap(e,a,r):e}async function extendCMap(e,t,a){e.useCMap=await createBuiltInCMap(a,t);if(0===e.numCodespaceRanges){const t=e.useCMap.codespaceRanges;for(let a=0;aextendCMap(i,t,e)));const n=new Lexer(new Stream(a));return parseCMap(i,n,t,null)}class CMapFactory{static async create({encoding:e,fetchBuiltInCMap:t,useCMap:a}){if(e instanceof Name)return createBuiltInCMap(e.name,t);if(e instanceof BaseStream){const r=await parseCMap(new CMap,new Lexer(e),t,a);return r.isIdentityCMap?createBuiltInCMap(r.name,t):r}throw new Error("Encoding required.")}}const xr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclamsmall","Hungarumlautsmall","","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","","asuperior","bsuperior","centsuperior","dsuperior","esuperior","","","","isuperior","","","lsuperior","msuperior","nsuperior","osuperior","","","rsuperior","ssuperior","tsuperior","","ff","fi","fl","ffi","ffl","parenleftinferior","","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","exclamdownsmall","centoldstyle","Lslashsmall","","","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","","Dotaccentsmall","","","Macronsmall","","","figuredash","hypheninferior","","","Ogoneksmall","Ringsmall","Cedillasmall","","","","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","","","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],Ar=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclamsmall","Hungarumlautsmall","centoldstyle","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","","threequartersemdash","","questionsmall","","","","","Ethsmall","","","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","","","","","","","ff","fi","fl","ffi","ffl","parenleftinferior","","parenrightinferior","Circumflexsmall","hypheninferior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","","","asuperior","centsuperior","","","","","Aacutesmall","Agravesmall","Acircumflexsmall","Adieresissmall","Atildesmall","Aringsmall","Ccedillasmall","Eacutesmall","Egravesmall","Ecircumflexsmall","Edieresissmall","Iacutesmall","Igravesmall","Icircumflexsmall","Idieresissmall","Ntildesmall","Oacutesmall","Ogravesmall","Ocircumflexsmall","Odieresissmall","Otildesmall","Uacutesmall","Ugravesmall","Ucircumflexsmall","Udieresissmall","","eightsuperior","fourinferior","threeinferior","sixinferior","eightinferior","seveninferior","Scaronsmall","","centinferior","twoinferior","","Dieresissmall","","Caronsmall","osuperior","fiveinferior","","commainferior","periodinferior","Yacutesmall","","dollarinferior","","","Thornsmall","","nineinferior","zeroinferior","Zcaronsmall","AEsmall","Oslashsmall","questiondownsmall","oneinferior","Lslashsmall","","","","","","","Cedillasmall","","","","","","OEsmall","figuredash","hyphensuperior","","","","","exclamdownsmall","","Ydieresissmall","","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","ninesuperior","zerosuperior","","esuperior","rsuperior","tsuperior","","","isuperior","ssuperior","dsuperior","","","","","","lsuperior","Ogoneksmall","Brevesmall","Macronsmall","bsuperior","nsuperior","msuperior","commasuperior","periodsuperior","Dotaccentsmall","Ringsmall","","","",""],kr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","space","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron"],Cr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","","endash","dagger","daggerdbl","periodcentered","","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","","questiondown","","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","","ring","cedilla","","hungarumlaut","ogonek","caron","emdash","","","","","","","","","","","","","","","","","AE","","ordfeminine","","","","","Lslash","Oslash","OE","ordmasculine","","","","","","ae","","","","dotlessi","","","lslash","oslash","oe","germandbls","","","",""],vr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","bullet","Euro","bullet","quotesinglbase","florin","quotedblbase","ellipsis","dagger","daggerdbl","circumflex","perthousand","Scaron","guilsinglleft","OE","bullet","Zcaron","bullet","bullet","quoteleft","quoteright","quotedblleft","quotedblright","bullet","endash","emdash","tilde","trademark","scaron","guilsinglright","oe","bullet","zcaron","Ydieresis","space","exclamdown","cent","sterling","currency","yen","brokenbar","section","dieresis","copyright","ordfeminine","guillemotleft","logicalnot","hyphen","registered","macron","degree","plusminus","twosuperior","threesuperior","acute","mu","paragraph","periodcentered","cedilla","onesuperior","ordmasculine","guillemotright","onequarter","onehalf","threequarters","questiondown","Agrave","Aacute","Acircumflex","Atilde","Adieresis","Aring","AE","Ccedilla","Egrave","Eacute","Ecircumflex","Edieresis","Igrave","Iacute","Icircumflex","Idieresis","Eth","Ntilde","Ograve","Oacute","Ocircumflex","Otilde","Odieresis","multiply","Oslash","Ugrave","Uacute","Ucircumflex","Udieresis","Yacute","Thorn","germandbls","agrave","aacute","acircumflex","atilde","adieresis","aring","ae","ccedilla","egrave","eacute","ecircumflex","edieresis","igrave","iacute","icircumflex","idieresis","eth","ntilde","ograve","oacute","ocircumflex","otilde","odieresis","divide","oslash","ugrave","uacute","ucircumflex","udieresis","yacute","thorn","ydieresis"],Fr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","universal","numbersign","existential","percent","ampersand","suchthat","parenleft","parenright","asteriskmath","plus","comma","minus","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","congruent","Alpha","Beta","Chi","Delta","Epsilon","Phi","Gamma","Eta","Iota","theta1","Kappa","Lambda","Mu","Nu","Omicron","Pi","Theta","Rho","Sigma","Tau","Upsilon","sigma1","Omega","Xi","Psi","Zeta","bracketleft","therefore","bracketright","perpendicular","underscore","radicalex","alpha","beta","chi","delta","epsilon","phi","gamma","eta","iota","phi1","kappa","lambda","mu","nu","omicron","pi","theta","rho","sigma","tau","upsilon","omega1","omega","xi","psi","zeta","braceleft","bar","braceright","similar","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Euro","Upsilon1","minute","lessequal","fraction","infinity","florin","club","diamond","heart","spade","arrowboth","arrowleft","arrowup","arrowright","arrowdown","degree","plusminus","second","greaterequal","multiply","proportional","partialdiff","bullet","divide","notequal","equivalence","approxequal","ellipsis","arrowvertex","arrowhorizex","carriagereturn","aleph","Ifraktur","Rfraktur","weierstrass","circlemultiply","circleplus","emptyset","intersection","union","propersuperset","reflexsuperset","notsubset","propersubset","reflexsubset","element","notelement","angle","gradient","registerserif","copyrightserif","trademarkserif","product","radical","dotmath","logicalnot","logicaland","logicalor","arrowdblboth","arrowdblleft","arrowdblup","arrowdblright","arrowdbldown","lozenge","angleleft","registersans","copyrightsans","trademarksans","summation","parenlefttp","parenleftex","parenleftbt","bracketlefttp","bracketleftex","bracketleftbt","bracelefttp","braceleftmid","braceleftbt","braceex","","angleright","integral","integraltp","integralex","integralbt","parenrighttp","parenrightex","parenrightbt","bracketrighttp","bracketrightex","bracketrightbt","bracerighttp","bracerightmid","bracerightbt",""],Ir=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","a1","a2","a202","a3","a4","a5","a119","a118","a117","a11","a12","a13","a14","a15","a16","a105","a17","a18","a19","a20","a21","a22","a23","a24","a25","a26","a27","a28","a6","a7","a8","a9","a10","a29","a30","a31","a32","a33","a34","a35","a36","a37","a38","a39","a40","a41","a42","a43","a44","a45","a46","a47","a48","a49","a50","a51","a52","a53","a54","a55","a56","a57","a58","a59","a60","a61","a62","a63","a64","a65","a66","a67","a68","a69","a70","a71","a72","a73","a74","a203","a75","a204","a76","a77","a78","a79","a81","a82","a83","a84","a97","a98","a99","a100","","a89","a90","a93","a94","a91","a92","a205","a85","a206","a86","a87","a88","a95","a96","","","","","","","","","","","","","","","","","","","","a101","a102","a103","a104","a106","a107","a108","a112","a111","a110","a109","a120","a121","a122","a123","a124","a125","a126","a127","a128","a129","a130","a131","a132","a133","a134","a135","a136","a137","a138","a139","a140","a141","a142","a143","a144","a145","a146","a147","a148","a149","a150","a151","a152","a153","a154","a155","a156","a157","a158","a159","a160","a161","a163","a164","a196","a165","a192","a166","a167","a168","a169","a170","a171","a172","a173","a162","a174","a175","a176","a177","a178","a179","a193","a180","a199","a181","a200","a182","","a201","a183","a184","a197","a185","a194","a198","a186","a195","a187","a188","a189","a190","a191",""];function getEncoding(e){switch(e){case"WinAnsiEncoding":return vr;case"StandardEncoding":return Cr;case"MacRomanEncoding":return kr;case"SymbolSetEncoding":return Fr;case"ZapfDingbatsEncoding":return Ir;case"ExpertEncoding":return xr;case"MacExpertEncoding":return Ar;default:return null}}const Or=getLookupTableFactory((function(e){e.A=65;e.AE=198;e.AEacute=508;e.AEmacron=482;e.AEsmall=63462;e.Aacute=193;e.Aacutesmall=63457;e.Abreve=258;e.Abreveacute=7854;e.Abrevecyrillic=1232;e.Abrevedotbelow=7862;e.Abrevegrave=7856;e.Abrevehookabove=7858;e.Abrevetilde=7860;e.Acaron=461;e.Acircle=9398;e.Acircumflex=194;e.Acircumflexacute=7844;e.Acircumflexdotbelow=7852;e.Acircumflexgrave=7846;e.Acircumflexhookabove=7848;e.Acircumflexsmall=63458;e.Acircumflextilde=7850;e.Acute=63177;e.Acutesmall=63412;e.Acyrillic=1040;e.Adblgrave=512;e.Adieresis=196;e.Adieresiscyrillic=1234;e.Adieresismacron=478;e.Adieresissmall=63460;e.Adotbelow=7840;e.Adotmacron=480;e.Agrave=192;e.Agravesmall=63456;e.Ahookabove=7842;e.Aiecyrillic=1236;e.Ainvertedbreve=514;e.Alpha=913;e.Alphatonos=902;e.Amacron=256;e.Amonospace=65313;e.Aogonek=260;e.Aring=197;e.Aringacute=506;e.Aringbelow=7680;e.Aringsmall=63461;e.Asmall=63329;e.Atilde=195;e.Atildesmall=63459;e.Aybarmenian=1329;e.B=66;e.Bcircle=9399;e.Bdotaccent=7682;e.Bdotbelow=7684;e.Becyrillic=1041;e.Benarmenian=1330;e.Beta=914;e.Bhook=385;e.Blinebelow=7686;e.Bmonospace=65314;e.Brevesmall=63220;e.Bsmall=63330;e.Btopbar=386;e.C=67;e.Caarmenian=1342;e.Cacute=262;e.Caron=63178;e.Caronsmall=63221;e.Ccaron=268;e.Ccedilla=199;e.Ccedillaacute=7688;e.Ccedillasmall=63463;e.Ccircle=9400;e.Ccircumflex=264;e.Cdot=266;e.Cdotaccent=266;e.Cedillasmall=63416;e.Chaarmenian=1353;e.Cheabkhasiancyrillic=1212;e.Checyrillic=1063;e.Chedescenderabkhasiancyrillic=1214;e.Chedescendercyrillic=1206;e.Chedieresiscyrillic=1268;e.Cheharmenian=1347;e.Chekhakassiancyrillic=1227;e.Cheverticalstrokecyrillic=1208;e.Chi=935;e.Chook=391;e.Circumflexsmall=63222;e.Cmonospace=65315;e.Coarmenian=1361;e.Csmall=63331;e.D=68;e.DZ=497;e.DZcaron=452;e.Daarmenian=1332;e.Dafrican=393;e.Dcaron=270;e.Dcedilla=7696;e.Dcircle=9401;e.Dcircumflexbelow=7698;e.Dcroat=272;e.Ddotaccent=7690;e.Ddotbelow=7692;e.Decyrillic=1044;e.Deicoptic=1006;e.Delta=8710;e.Deltagreek=916;e.Dhook=394;e.Dieresis=63179;e.DieresisAcute=63180;e.DieresisGrave=63181;e.Dieresissmall=63400;e.Digammagreek=988;e.Djecyrillic=1026;e.Dlinebelow=7694;e.Dmonospace=65316;e.Dotaccentsmall=63223;e.Dslash=272;e.Dsmall=63332;e.Dtopbar=395;e.Dz=498;e.Dzcaron=453;e.Dzeabkhasiancyrillic=1248;e.Dzecyrillic=1029;e.Dzhecyrillic=1039;e.E=69;e.Eacute=201;e.Eacutesmall=63465;e.Ebreve=276;e.Ecaron=282;e.Ecedillabreve=7708;e.Echarmenian=1333;e.Ecircle=9402;e.Ecircumflex=202;e.Ecircumflexacute=7870;e.Ecircumflexbelow=7704;e.Ecircumflexdotbelow=7878;e.Ecircumflexgrave=7872;e.Ecircumflexhookabove=7874;e.Ecircumflexsmall=63466;e.Ecircumflextilde=7876;e.Ecyrillic=1028;e.Edblgrave=516;e.Edieresis=203;e.Edieresissmall=63467;e.Edot=278;e.Edotaccent=278;e.Edotbelow=7864;e.Efcyrillic=1060;e.Egrave=200;e.Egravesmall=63464;e.Eharmenian=1335;e.Ehookabove=7866;e.Eightroman=8551;e.Einvertedbreve=518;e.Eiotifiedcyrillic=1124;e.Elcyrillic=1051;e.Elevenroman=8554;e.Emacron=274;e.Emacronacute=7702;e.Emacrongrave=7700;e.Emcyrillic=1052;e.Emonospace=65317;e.Encyrillic=1053;e.Endescendercyrillic=1186;e.Eng=330;e.Enghecyrillic=1188;e.Enhookcyrillic=1223;e.Eogonek=280;e.Eopen=400;e.Epsilon=917;e.Epsilontonos=904;e.Ercyrillic=1056;e.Ereversed=398;e.Ereversedcyrillic=1069;e.Escyrillic=1057;e.Esdescendercyrillic=1194;e.Esh=425;e.Esmall=63333;e.Eta=919;e.Etarmenian=1336;e.Etatonos=905;e.Eth=208;e.Ethsmall=63472;e.Etilde=7868;e.Etildebelow=7706;e.Euro=8364;e.Ezh=439;e.Ezhcaron=494;e.Ezhreversed=440;e.F=70;e.Fcircle=9403;e.Fdotaccent=7710;e.Feharmenian=1366;e.Feicoptic=996;e.Fhook=401;e.Fitacyrillic=1138;e.Fiveroman=8548;e.Fmonospace=65318;e.Fourroman=8547;e.Fsmall=63334;e.G=71;e.GBsquare=13191;e.Gacute=500;e.Gamma=915;e.Gammaafrican=404;e.Gangiacoptic=1002;e.Gbreve=286;e.Gcaron=486;e.Gcedilla=290;e.Gcircle=9404;e.Gcircumflex=284;e.Gcommaaccent=290;e.Gdot=288;e.Gdotaccent=288;e.Gecyrillic=1043;e.Ghadarmenian=1346;e.Ghemiddlehookcyrillic=1172;e.Ghestrokecyrillic=1170;e.Gheupturncyrillic=1168;e.Ghook=403;e.Gimarmenian=1331;e.Gjecyrillic=1027;e.Gmacron=7712;e.Gmonospace=65319;e.Grave=63182;e.Gravesmall=63328;e.Gsmall=63335;e.Gsmallhook=667;e.Gstroke=484;e.H=72;e.H18533=9679;e.H18543=9642;e.H18551=9643;e.H22073=9633;e.HPsquare=13259;e.Haabkhasiancyrillic=1192;e.Hadescendercyrillic=1202;e.Hardsigncyrillic=1066;e.Hbar=294;e.Hbrevebelow=7722;e.Hcedilla=7720;e.Hcircle=9405;e.Hcircumflex=292;e.Hdieresis=7718;e.Hdotaccent=7714;e.Hdotbelow=7716;e.Hmonospace=65320;e.Hoarmenian=1344;e.Horicoptic=1e3;e.Hsmall=63336;e.Hungarumlaut=63183;e.Hungarumlautsmall=63224;e.Hzsquare=13200;e.I=73;e.IAcyrillic=1071;e.IJ=306;e.IUcyrillic=1070;e.Iacute=205;e.Iacutesmall=63469;e.Ibreve=300;e.Icaron=463;e.Icircle=9406;e.Icircumflex=206;e.Icircumflexsmall=63470;e.Icyrillic=1030;e.Idblgrave=520;e.Idieresis=207;e.Idieresisacute=7726;e.Idieresiscyrillic=1252;e.Idieresissmall=63471;e.Idot=304;e.Idotaccent=304;e.Idotbelow=7882;e.Iebrevecyrillic=1238;e.Iecyrillic=1045;e.Ifraktur=8465;e.Igrave=204;e.Igravesmall=63468;e.Ihookabove=7880;e.Iicyrillic=1048;e.Iinvertedbreve=522;e.Iishortcyrillic=1049;e.Imacron=298;e.Imacroncyrillic=1250;e.Imonospace=65321;e.Iniarmenian=1339;e.Iocyrillic=1025;e.Iogonek=302;e.Iota=921;e.Iotaafrican=406;e.Iotadieresis=938;e.Iotatonos=906;e.Ismall=63337;e.Istroke=407;e.Itilde=296;e.Itildebelow=7724;e.Izhitsacyrillic=1140;e.Izhitsadblgravecyrillic=1142;e.J=74;e.Jaarmenian=1345;e.Jcircle=9407;e.Jcircumflex=308;e.Jecyrillic=1032;e.Jheharmenian=1355;e.Jmonospace=65322;e.Jsmall=63338;e.K=75;e.KBsquare=13189;e.KKsquare=13261;e.Kabashkircyrillic=1184;e.Kacute=7728;e.Kacyrillic=1050;e.Kadescendercyrillic=1178;e.Kahookcyrillic=1219;e.Kappa=922;e.Kastrokecyrillic=1182;e.Kaverticalstrokecyrillic=1180;e.Kcaron=488;e.Kcedilla=310;e.Kcircle=9408;e.Kcommaaccent=310;e.Kdotbelow=7730;e.Keharmenian=1364;e.Kenarmenian=1343;e.Khacyrillic=1061;e.Kheicoptic=998;e.Khook=408;e.Kjecyrillic=1036;e.Klinebelow=7732;e.Kmonospace=65323;e.Koppacyrillic=1152;e.Koppagreek=990;e.Ksicyrillic=1134;e.Ksmall=63339;e.L=76;e.LJ=455;e.LL=63167;e.Lacute=313;e.Lambda=923;e.Lcaron=317;e.Lcedilla=315;e.Lcircle=9409;e.Lcircumflexbelow=7740;e.Lcommaaccent=315;e.Ldot=319;e.Ldotaccent=319;e.Ldotbelow=7734;e.Ldotbelowmacron=7736;e.Liwnarmenian=1340;e.Lj=456;e.Ljecyrillic=1033;e.Llinebelow=7738;e.Lmonospace=65324;e.Lslash=321;e.Lslashsmall=63225;e.Lsmall=63340;e.M=77;e.MBsquare=13190;e.Macron=63184;e.Macronsmall=63407;e.Macute=7742;e.Mcircle=9410;e.Mdotaccent=7744;e.Mdotbelow=7746;e.Menarmenian=1348;e.Mmonospace=65325;e.Msmall=63341;e.Mturned=412;e.Mu=924;e.N=78;e.NJ=458;e.Nacute=323;e.Ncaron=327;e.Ncedilla=325;e.Ncircle=9411;e.Ncircumflexbelow=7754;e.Ncommaaccent=325;e.Ndotaccent=7748;e.Ndotbelow=7750;e.Nhookleft=413;e.Nineroman=8552;e.Nj=459;e.Njecyrillic=1034;e.Nlinebelow=7752;e.Nmonospace=65326;e.Nowarmenian=1350;e.Nsmall=63342;e.Ntilde=209;e.Ntildesmall=63473;e.Nu=925;e.O=79;e.OE=338;e.OEsmall=63226;e.Oacute=211;e.Oacutesmall=63475;e.Obarredcyrillic=1256;e.Obarreddieresiscyrillic=1258;e.Obreve=334;e.Ocaron=465;e.Ocenteredtilde=415;e.Ocircle=9412;e.Ocircumflex=212;e.Ocircumflexacute=7888;e.Ocircumflexdotbelow=7896;e.Ocircumflexgrave=7890;e.Ocircumflexhookabove=7892;e.Ocircumflexsmall=63476;e.Ocircumflextilde=7894;e.Ocyrillic=1054;e.Odblacute=336;e.Odblgrave=524;e.Odieresis=214;e.Odieresiscyrillic=1254;e.Odieresissmall=63478;e.Odotbelow=7884;e.Ogoneksmall=63227;e.Ograve=210;e.Ogravesmall=63474;e.Oharmenian=1365;e.Ohm=8486;e.Ohookabove=7886;e.Ohorn=416;e.Ohornacute=7898;e.Ohorndotbelow=7906;e.Ohorngrave=7900;e.Ohornhookabove=7902;e.Ohorntilde=7904;e.Ohungarumlaut=336;e.Oi=418;e.Oinvertedbreve=526;e.Omacron=332;e.Omacronacute=7762;e.Omacrongrave=7760;e.Omega=8486;e.Omegacyrillic=1120;e.Omegagreek=937;e.Omegaroundcyrillic=1146;e.Omegatitlocyrillic=1148;e.Omegatonos=911;e.Omicron=927;e.Omicrontonos=908;e.Omonospace=65327;e.Oneroman=8544;e.Oogonek=490;e.Oogonekmacron=492;e.Oopen=390;e.Oslash=216;e.Oslashacute=510;e.Oslashsmall=63480;e.Osmall=63343;e.Ostrokeacute=510;e.Otcyrillic=1150;e.Otilde=213;e.Otildeacute=7756;e.Otildedieresis=7758;e.Otildesmall=63477;e.P=80;e.Pacute=7764;e.Pcircle=9413;e.Pdotaccent=7766;e.Pecyrillic=1055;e.Peharmenian=1354;e.Pemiddlehookcyrillic=1190;e.Phi=934;e.Phook=420;e.Pi=928;e.Piwrarmenian=1363;e.Pmonospace=65328;e.Psi=936;e.Psicyrillic=1136;e.Psmall=63344;e.Q=81;e.Qcircle=9414;e.Qmonospace=65329;e.Qsmall=63345;e.R=82;e.Raarmenian=1356;e.Racute=340;e.Rcaron=344;e.Rcedilla=342;e.Rcircle=9415;e.Rcommaaccent=342;e.Rdblgrave=528;e.Rdotaccent=7768;e.Rdotbelow=7770;e.Rdotbelowmacron=7772;e.Reharmenian=1360;e.Rfraktur=8476;e.Rho=929;e.Ringsmall=63228;e.Rinvertedbreve=530;e.Rlinebelow=7774;e.Rmonospace=65330;e.Rsmall=63346;e.Rsmallinverted=641;e.Rsmallinvertedsuperior=694;e.S=83;e.SF010000=9484;e.SF020000=9492;e.SF030000=9488;e.SF040000=9496;e.SF050000=9532;e.SF060000=9516;e.SF070000=9524;e.SF080000=9500;e.SF090000=9508;e.SF100000=9472;e.SF110000=9474;e.SF190000=9569;e.SF200000=9570;e.SF210000=9558;e.SF220000=9557;e.SF230000=9571;e.SF240000=9553;e.SF250000=9559;e.SF260000=9565;e.SF270000=9564;e.SF280000=9563;e.SF360000=9566;e.SF370000=9567;e.SF380000=9562;e.SF390000=9556;e.SF400000=9577;e.SF410000=9574;e.SF420000=9568;e.SF430000=9552;e.SF440000=9580;e.SF450000=9575;e.SF460000=9576;e.SF470000=9572;e.SF480000=9573;e.SF490000=9561;e.SF500000=9560;e.SF510000=9554;e.SF520000=9555;e.SF530000=9579;e.SF540000=9578;e.Sacute=346;e.Sacutedotaccent=7780;e.Sampigreek=992;e.Scaron=352;e.Scarondotaccent=7782;e.Scaronsmall=63229;e.Scedilla=350;e.Schwa=399;e.Schwacyrillic=1240;e.Schwadieresiscyrillic=1242;e.Scircle=9416;e.Scircumflex=348;e.Scommaaccent=536;e.Sdotaccent=7776;e.Sdotbelow=7778;e.Sdotbelowdotaccent=7784;e.Seharmenian=1357;e.Sevenroman=8550;e.Shaarmenian=1351;e.Shacyrillic=1064;e.Shchacyrillic=1065;e.Sheicoptic=994;e.Shhacyrillic=1210;e.Shimacoptic=1004;e.Sigma=931;e.Sixroman=8549;e.Smonospace=65331;e.Softsigncyrillic=1068;e.Ssmall=63347;e.Stigmagreek=986;e.T=84;e.Tau=932;e.Tbar=358;e.Tcaron=356;e.Tcedilla=354;e.Tcircle=9417;e.Tcircumflexbelow=7792;e.Tcommaaccent=354;e.Tdotaccent=7786;e.Tdotbelow=7788;e.Tecyrillic=1058;e.Tedescendercyrillic=1196;e.Tenroman=8553;e.Tetsecyrillic=1204;e.Theta=920;e.Thook=428;e.Thorn=222;e.Thornsmall=63486;e.Threeroman=8546;e.Tildesmall=63230;e.Tiwnarmenian=1359;e.Tlinebelow=7790;e.Tmonospace=65332;e.Toarmenian=1337;e.Tonefive=444;e.Tonesix=388;e.Tonetwo=423;e.Tretroflexhook=430;e.Tsecyrillic=1062;e.Tshecyrillic=1035;e.Tsmall=63348;e.Twelveroman=8555;e.Tworoman=8545;e.U=85;e.Uacute=218;e.Uacutesmall=63482;e.Ubreve=364;e.Ucaron=467;e.Ucircle=9418;e.Ucircumflex=219;e.Ucircumflexbelow=7798;e.Ucircumflexsmall=63483;e.Ucyrillic=1059;e.Udblacute=368;e.Udblgrave=532;e.Udieresis=220;e.Udieresisacute=471;e.Udieresisbelow=7794;e.Udieresiscaron=473;e.Udieresiscyrillic=1264;e.Udieresisgrave=475;e.Udieresismacron=469;e.Udieresissmall=63484;e.Udotbelow=7908;e.Ugrave=217;e.Ugravesmall=63481;e.Uhookabove=7910;e.Uhorn=431;e.Uhornacute=7912;e.Uhorndotbelow=7920;e.Uhorngrave=7914;e.Uhornhookabove=7916;e.Uhorntilde=7918;e.Uhungarumlaut=368;e.Uhungarumlautcyrillic=1266;e.Uinvertedbreve=534;e.Ukcyrillic=1144;e.Umacron=362;e.Umacroncyrillic=1262;e.Umacrondieresis=7802;e.Umonospace=65333;e.Uogonek=370;e.Upsilon=933;e.Upsilon1=978;e.Upsilonacutehooksymbolgreek=979;e.Upsilonafrican=433;e.Upsilondieresis=939;e.Upsilondieresishooksymbolgreek=980;e.Upsilonhooksymbol=978;e.Upsilontonos=910;e.Uring=366;e.Ushortcyrillic=1038;e.Usmall=63349;e.Ustraightcyrillic=1198;e.Ustraightstrokecyrillic=1200;e.Utilde=360;e.Utildeacute=7800;e.Utildebelow=7796;e.V=86;e.Vcircle=9419;e.Vdotbelow=7806;e.Vecyrillic=1042;e.Vewarmenian=1358;e.Vhook=434;e.Vmonospace=65334;e.Voarmenian=1352;e.Vsmall=63350;e.Vtilde=7804;e.W=87;e.Wacute=7810;e.Wcircle=9420;e.Wcircumflex=372;e.Wdieresis=7812;e.Wdotaccent=7814;e.Wdotbelow=7816;e.Wgrave=7808;e.Wmonospace=65335;e.Wsmall=63351;e.X=88;e.Xcircle=9421;e.Xdieresis=7820;e.Xdotaccent=7818;e.Xeharmenian=1341;e.Xi=926;e.Xmonospace=65336;e.Xsmall=63352;e.Y=89;e.Yacute=221;e.Yacutesmall=63485;e.Yatcyrillic=1122;e.Ycircle=9422;e.Ycircumflex=374;e.Ydieresis=376;e.Ydieresissmall=63487;e.Ydotaccent=7822;e.Ydotbelow=7924;e.Yericyrillic=1067;e.Yerudieresiscyrillic=1272;e.Ygrave=7922;e.Yhook=435;e.Yhookabove=7926;e.Yiarmenian=1349;e.Yicyrillic=1031;e.Yiwnarmenian=1362;e.Ymonospace=65337;e.Ysmall=63353;e.Ytilde=7928;e.Yusbigcyrillic=1130;e.Yusbigiotifiedcyrillic=1132;e.Yuslittlecyrillic=1126;e.Yuslittleiotifiedcyrillic=1128;e.Z=90;e.Zaarmenian=1334;e.Zacute=377;e.Zcaron=381;e.Zcaronsmall=63231;e.Zcircle=9423;e.Zcircumflex=7824;e.Zdot=379;e.Zdotaccent=379;e.Zdotbelow=7826;e.Zecyrillic=1047;e.Zedescendercyrillic=1176;e.Zedieresiscyrillic=1246;e.Zeta=918;e.Zhearmenian=1338;e.Zhebrevecyrillic=1217;e.Zhecyrillic=1046;e.Zhedescendercyrillic=1174;e.Zhedieresiscyrillic=1244;e.Zlinebelow=7828;e.Zmonospace=65338;e.Zsmall=63354;e.Zstroke=437;e.a=97;e.aabengali=2438;e.aacute=225;e.aadeva=2310;e.aagujarati=2694;e.aagurmukhi=2566;e.aamatragurmukhi=2622;e.aarusquare=13059;e.aavowelsignbengali=2494;e.aavowelsigndeva=2366;e.aavowelsigngujarati=2750;e.abbreviationmarkarmenian=1375;e.abbreviationsigndeva=2416;e.abengali=2437;e.abopomofo=12570;e.abreve=259;e.abreveacute=7855;e.abrevecyrillic=1233;e.abrevedotbelow=7863;e.abrevegrave=7857;e.abrevehookabove=7859;e.abrevetilde=7861;e.acaron=462;e.acircle=9424;e.acircumflex=226;e.acircumflexacute=7845;e.acircumflexdotbelow=7853;e.acircumflexgrave=7847;e.acircumflexhookabove=7849;e.acircumflextilde=7851;e.acute=180;e.acutebelowcmb=791;e.acutecmb=769;e.acutecomb=769;e.acutedeva=2388;e.acutelowmod=719;e.acutetonecmb=833;e.acyrillic=1072;e.adblgrave=513;e.addakgurmukhi=2673;e.adeva=2309;e.adieresis=228;e.adieresiscyrillic=1235;e.adieresismacron=479;e.adotbelow=7841;e.adotmacron=481;e.ae=230;e.aeacute=509;e.aekorean=12624;e.aemacron=483;e.afii00208=8213;e.afii08941=8356;e.afii10017=1040;e.afii10018=1041;e.afii10019=1042;e.afii10020=1043;e.afii10021=1044;e.afii10022=1045;e.afii10023=1025;e.afii10024=1046;e.afii10025=1047;e.afii10026=1048;e.afii10027=1049;e.afii10028=1050;e.afii10029=1051;e.afii10030=1052;e.afii10031=1053;e.afii10032=1054;e.afii10033=1055;e.afii10034=1056;e.afii10035=1057;e.afii10036=1058;e.afii10037=1059;e.afii10038=1060;e.afii10039=1061;e.afii10040=1062;e.afii10041=1063;e.afii10042=1064;e.afii10043=1065;e.afii10044=1066;e.afii10045=1067;e.afii10046=1068;e.afii10047=1069;e.afii10048=1070;e.afii10049=1071;e.afii10050=1168;e.afii10051=1026;e.afii10052=1027;e.afii10053=1028;e.afii10054=1029;e.afii10055=1030;e.afii10056=1031;e.afii10057=1032;e.afii10058=1033;e.afii10059=1034;e.afii10060=1035;e.afii10061=1036;e.afii10062=1038;e.afii10063=63172;e.afii10064=63173;e.afii10065=1072;e.afii10066=1073;e.afii10067=1074;e.afii10068=1075;e.afii10069=1076;e.afii10070=1077;e.afii10071=1105;e.afii10072=1078;e.afii10073=1079;e.afii10074=1080;e.afii10075=1081;e.afii10076=1082;e.afii10077=1083;e.afii10078=1084;e.afii10079=1085;e.afii10080=1086;e.afii10081=1087;e.afii10082=1088;e.afii10083=1089;e.afii10084=1090;e.afii10085=1091;e.afii10086=1092;e.afii10087=1093;e.afii10088=1094;e.afii10089=1095;e.afii10090=1096;e.afii10091=1097;e.afii10092=1098;e.afii10093=1099;e.afii10094=1100;e.afii10095=1101;e.afii10096=1102;e.afii10097=1103;e.afii10098=1169;e.afii10099=1106;e.afii10100=1107;e.afii10101=1108;e.afii10102=1109;e.afii10103=1110;e.afii10104=1111;e.afii10105=1112;e.afii10106=1113;e.afii10107=1114;e.afii10108=1115;e.afii10109=1116;e.afii10110=1118;e.afii10145=1039;e.afii10146=1122;e.afii10147=1138;e.afii10148=1140;e.afii10192=63174;e.afii10193=1119;e.afii10194=1123;e.afii10195=1139;e.afii10196=1141;e.afii10831=63175;e.afii10832=63176;e.afii10846=1241;e.afii299=8206;e.afii300=8207;e.afii301=8205;e.afii57381=1642;e.afii57388=1548;e.afii57392=1632;e.afii57393=1633;e.afii57394=1634;e.afii57395=1635;e.afii57396=1636;e.afii57397=1637;e.afii57398=1638;e.afii57399=1639;e.afii57400=1640;e.afii57401=1641;e.afii57403=1563;e.afii57407=1567;e.afii57409=1569;e.afii57410=1570;e.afii57411=1571;e.afii57412=1572;e.afii57413=1573;e.afii57414=1574;e.afii57415=1575;e.afii57416=1576;e.afii57417=1577;e.afii57418=1578;e.afii57419=1579;e.afii57420=1580;e.afii57421=1581;e.afii57422=1582;e.afii57423=1583;e.afii57424=1584;e.afii57425=1585;e.afii57426=1586;e.afii57427=1587;e.afii57428=1588;e.afii57429=1589;e.afii57430=1590;e.afii57431=1591;e.afii57432=1592;e.afii57433=1593;e.afii57434=1594;e.afii57440=1600;e.afii57441=1601;e.afii57442=1602;e.afii57443=1603;e.afii57444=1604;e.afii57445=1605;e.afii57446=1606;e.afii57448=1608;e.afii57449=1609;e.afii57450=1610;e.afii57451=1611;e.afii57452=1612;e.afii57453=1613;e.afii57454=1614;e.afii57455=1615;e.afii57456=1616;e.afii57457=1617;e.afii57458=1618;e.afii57470=1607;e.afii57505=1700;e.afii57506=1662;e.afii57507=1670;e.afii57508=1688;e.afii57509=1711;e.afii57511=1657;e.afii57512=1672;e.afii57513=1681;e.afii57514=1722;e.afii57519=1746;e.afii57534=1749;e.afii57636=8362;e.afii57645=1470;e.afii57658=1475;e.afii57664=1488;e.afii57665=1489;e.afii57666=1490;e.afii57667=1491;e.afii57668=1492;e.afii57669=1493;e.afii57670=1494;e.afii57671=1495;e.afii57672=1496;e.afii57673=1497;e.afii57674=1498;e.afii57675=1499;e.afii57676=1500;e.afii57677=1501;e.afii57678=1502;e.afii57679=1503;e.afii57680=1504;e.afii57681=1505;e.afii57682=1506;e.afii57683=1507;e.afii57684=1508;e.afii57685=1509;e.afii57686=1510;e.afii57687=1511;e.afii57688=1512;e.afii57689=1513;e.afii57690=1514;e.afii57694=64298;e.afii57695=64299;e.afii57700=64331;e.afii57705=64287;e.afii57716=1520;e.afii57717=1521;e.afii57718=1522;e.afii57723=64309;e.afii57793=1460;e.afii57794=1461;e.afii57795=1462;e.afii57796=1467;e.afii57797=1464;e.afii57798=1463;e.afii57799=1456;e.afii57800=1458;e.afii57801=1457;e.afii57802=1459;e.afii57803=1474;e.afii57804=1473;e.afii57806=1465;e.afii57807=1468;e.afii57839=1469;e.afii57841=1471;e.afii57842=1472;e.afii57929=700;e.afii61248=8453;e.afii61289=8467;e.afii61352=8470;e.afii61573=8236;e.afii61574=8237;e.afii61575=8238;e.afii61664=8204;e.afii63167=1645;e.afii64937=701;e.agrave=224;e.agujarati=2693;e.agurmukhi=2565;e.ahiragana=12354;e.ahookabove=7843;e.aibengali=2448;e.aibopomofo=12574;e.aideva=2320;e.aiecyrillic=1237;e.aigujarati=2704;e.aigurmukhi=2576;e.aimatragurmukhi=2632;e.ainarabic=1593;e.ainfinalarabic=65226;e.aininitialarabic=65227;e.ainmedialarabic=65228;e.ainvertedbreve=515;e.aivowelsignbengali=2504;e.aivowelsigndeva=2376;e.aivowelsigngujarati=2760;e.akatakana=12450;e.akatakanahalfwidth=65393;e.akorean=12623;e.alef=1488;e.alefarabic=1575;e.alefdageshhebrew=64304;e.aleffinalarabic=65166;e.alefhamzaabovearabic=1571;e.alefhamzaabovefinalarabic=65156;e.alefhamzabelowarabic=1573;e.alefhamzabelowfinalarabic=65160;e.alefhebrew=1488;e.aleflamedhebrew=64335;e.alefmaddaabovearabic=1570;e.alefmaddaabovefinalarabic=65154;e.alefmaksuraarabic=1609;e.alefmaksurafinalarabic=65264;e.alefmaksurainitialarabic=65267;e.alefmaksuramedialarabic=65268;e.alefpatahhebrew=64302;e.alefqamatshebrew=64303;e.aleph=8501;e.allequal=8780;e.alpha=945;e.alphatonos=940;e.amacron=257;e.amonospace=65345;e.ampersand=38;e.ampersandmonospace=65286;e.ampersandsmall=63270;e.amsquare=13250;e.anbopomofo=12578;e.angbopomofo=12580;e.angbracketleft=12296;e.angbracketright=12297;e.angkhankhuthai=3674;e.angle=8736;e.anglebracketleft=12296;e.anglebracketleftvertical=65087;e.anglebracketright=12297;e.anglebracketrightvertical=65088;e.angleleft=9001;e.angleright=9002;e.angstrom=8491;e.anoteleia=903;e.anudattadeva=2386;e.anusvarabengali=2434;e.anusvaradeva=2306;e.anusvaragujarati=2690;e.aogonek=261;e.apaatosquare=13056;e.aparen=9372;e.apostrophearmenian=1370;e.apostrophemod=700;e.apple=63743;e.approaches=8784;e.approxequal=8776;e.approxequalorimage=8786;e.approximatelyequal=8773;e.araeaekorean=12686;e.araeakorean=12685;e.arc=8978;e.arighthalfring=7834;e.aring=229;e.aringacute=507;e.aringbelow=7681;e.arrowboth=8596;e.arrowdashdown=8675;e.arrowdashleft=8672;e.arrowdashright=8674;e.arrowdashup=8673;e.arrowdblboth=8660;e.arrowdbldown=8659;e.arrowdblleft=8656;e.arrowdblright=8658;e.arrowdblup=8657;e.arrowdown=8595;e.arrowdownleft=8601;e.arrowdownright=8600;e.arrowdownwhite=8681;e.arrowheaddownmod=709;e.arrowheadleftmod=706;e.arrowheadrightmod=707;e.arrowheadupmod=708;e.arrowhorizex=63719;e.arrowleft=8592;e.arrowleftdbl=8656;e.arrowleftdblstroke=8653;e.arrowleftoverright=8646;e.arrowleftwhite=8678;e.arrowright=8594;e.arrowrightdblstroke=8655;e.arrowrightheavy=10142;e.arrowrightoverleft=8644;e.arrowrightwhite=8680;e.arrowtableft=8676;e.arrowtabright=8677;e.arrowup=8593;e.arrowupdn=8597;e.arrowupdnbse=8616;e.arrowupdownbase=8616;e.arrowupleft=8598;e.arrowupleftofdown=8645;e.arrowupright=8599;e.arrowupwhite=8679;e.arrowvertex=63718;e.asciicircum=94;e.asciicircummonospace=65342;e.asciitilde=126;e.asciitildemonospace=65374;e.ascript=593;e.ascriptturned=594;e.asmallhiragana=12353;e.asmallkatakana=12449;e.asmallkatakanahalfwidth=65383;e.asterisk=42;e.asteriskaltonearabic=1645;e.asteriskarabic=1645;e.asteriskmath=8727;e.asteriskmonospace=65290;e.asterisksmall=65121;e.asterism=8258;e.asuperior=63209;e.asymptoticallyequal=8771;e.at=64;e.atilde=227;e.atmonospace=65312;e.atsmall=65131;e.aturned=592;e.aubengali=2452;e.aubopomofo=12576;e.audeva=2324;e.augujarati=2708;e.augurmukhi=2580;e.aulengthmarkbengali=2519;e.aumatragurmukhi=2636;e.auvowelsignbengali=2508;e.auvowelsigndeva=2380;e.auvowelsigngujarati=2764;e.avagrahadeva=2365;e.aybarmenian=1377;e.ayin=1506;e.ayinaltonehebrew=64288;e.ayinhebrew=1506;e.b=98;e.babengali=2476;e.backslash=92;e.backslashmonospace=65340;e.badeva=2348;e.bagujarati=2732;e.bagurmukhi=2604;e.bahiragana=12400;e.bahtthai=3647;e.bakatakana=12496;e.bar=124;e.barmonospace=65372;e.bbopomofo=12549;e.bcircle=9425;e.bdotaccent=7683;e.bdotbelow=7685;e.beamedsixteenthnotes=9836;e.because=8757;e.becyrillic=1073;e.beharabic=1576;e.behfinalarabic=65168;e.behinitialarabic=65169;e.behiragana=12409;e.behmedialarabic=65170;e.behmeeminitialarabic=64671;e.behmeemisolatedarabic=64520;e.behnoonfinalarabic=64621;e.bekatakana=12505;e.benarmenian=1378;e.bet=1489;e.beta=946;e.betasymbolgreek=976;e.betdagesh=64305;e.betdageshhebrew=64305;e.bethebrew=1489;e.betrafehebrew=64332;e.bhabengali=2477;e.bhadeva=2349;e.bhagujarati=2733;e.bhagurmukhi=2605;e.bhook=595;e.bihiragana=12403;e.bikatakana=12499;e.bilabialclick=664;e.bindigurmukhi=2562;e.birusquare=13105;e.blackcircle=9679;e.blackdiamond=9670;e.blackdownpointingtriangle=9660;e.blackleftpointingpointer=9668;e.blackleftpointingtriangle=9664;e.blacklenticularbracketleft=12304;e.blacklenticularbracketleftvertical=65083;e.blacklenticularbracketright=12305;e.blacklenticularbracketrightvertical=65084;e.blacklowerlefttriangle=9699;e.blacklowerrighttriangle=9698;e.blackrectangle=9644;e.blackrightpointingpointer=9658;e.blackrightpointingtriangle=9654;e.blacksmallsquare=9642;e.blacksmilingface=9787;e.blacksquare=9632;e.blackstar=9733;e.blackupperlefttriangle=9700;e.blackupperrighttriangle=9701;e.blackuppointingsmalltriangle=9652;e.blackuppointingtriangle=9650;e.blank=9251;e.blinebelow=7687;e.block=9608;e.bmonospace=65346;e.bobaimaithai=3610;e.bohiragana=12412;e.bokatakana=12508;e.bparen=9373;e.bqsquare=13251;e.braceex=63732;e.braceleft=123;e.braceleftbt=63731;e.braceleftmid=63730;e.braceleftmonospace=65371;e.braceleftsmall=65115;e.bracelefttp=63729;e.braceleftvertical=65079;e.braceright=125;e.bracerightbt=63742;e.bracerightmid=63741;e.bracerightmonospace=65373;e.bracerightsmall=65116;e.bracerighttp=63740;e.bracerightvertical=65080;e.bracketleft=91;e.bracketleftbt=63728;e.bracketleftex=63727;e.bracketleftmonospace=65339;e.bracketlefttp=63726;e.bracketright=93;e.bracketrightbt=63739;e.bracketrightex=63738;e.bracketrightmonospace=65341;e.bracketrighttp=63737;e.breve=728;e.brevebelowcmb=814;e.brevecmb=774;e.breveinvertedbelowcmb=815;e.breveinvertedcmb=785;e.breveinverteddoublecmb=865;e.bridgebelowcmb=810;e.bridgeinvertedbelowcmb=826;e.brokenbar=166;e.bstroke=384;e.bsuperior=63210;e.btopbar=387;e.buhiragana=12406;e.bukatakana=12502;e.bullet=8226;e.bulletinverse=9688;e.bulletoperator=8729;e.bullseye=9678;e.c=99;e.caarmenian=1390;e.cabengali=2458;e.cacute=263;e.cadeva=2330;e.cagujarati=2714;e.cagurmukhi=2586;e.calsquare=13192;e.candrabindubengali=2433;e.candrabinducmb=784;e.candrabindudeva=2305;e.candrabindugujarati=2689;e.capslock=8682;e.careof=8453;e.caron=711;e.caronbelowcmb=812;e.caroncmb=780;e.carriagereturn=8629;e.cbopomofo=12568;e.ccaron=269;e.ccedilla=231;e.ccedillaacute=7689;e.ccircle=9426;e.ccircumflex=265;e.ccurl=597;e.cdot=267;e.cdotaccent=267;e.cdsquare=13253;e.cedilla=184;e.cedillacmb=807;e.cent=162;e.centigrade=8451;e.centinferior=63199;e.centmonospace=65504;e.centoldstyle=63394;e.centsuperior=63200;e.chaarmenian=1401;e.chabengali=2459;e.chadeva=2331;e.chagujarati=2715;e.chagurmukhi=2587;e.chbopomofo=12564;e.cheabkhasiancyrillic=1213;e.checkmark=10003;e.checyrillic=1095;e.chedescenderabkhasiancyrillic=1215;e.chedescendercyrillic=1207;e.chedieresiscyrillic=1269;e.cheharmenian=1395;e.chekhakassiancyrillic=1228;e.cheverticalstrokecyrillic=1209;e.chi=967;e.chieuchacirclekorean=12919;e.chieuchaparenkorean=12823;e.chieuchcirclekorean=12905;e.chieuchkorean=12618;e.chieuchparenkorean=12809;e.chochangthai=3594;e.chochanthai=3592;e.chochingthai=3593;e.chochoethai=3596;e.chook=392;e.cieucacirclekorean=12918;e.cieucaparenkorean=12822;e.cieuccirclekorean=12904;e.cieuckorean=12616;e.cieucparenkorean=12808;e.cieucuparenkorean=12828;e.circle=9675;e.circlecopyrt=169;e.circlemultiply=8855;e.circleot=8857;e.circleplus=8853;e.circlepostalmark=12342;e.circlewithlefthalfblack=9680;e.circlewithrighthalfblack=9681;e.circumflex=710;e.circumflexbelowcmb=813;e.circumflexcmb=770;e.clear=8999;e.clickalveolar=450;e.clickdental=448;e.clicklateral=449;e.clickretroflex=451;e.club=9827;e.clubsuitblack=9827;e.clubsuitwhite=9831;e.cmcubedsquare=13220;e.cmonospace=65347;e.cmsquaredsquare=13216;e.coarmenian=1409;e.colon=58;e.colonmonetary=8353;e.colonmonospace=65306;e.colonsign=8353;e.colonsmall=65109;e.colontriangularhalfmod=721;e.colontriangularmod=720;e.comma=44;e.commaabovecmb=787;e.commaaboverightcmb=789;e.commaaccent=63171;e.commaarabic=1548;e.commaarmenian=1373;e.commainferior=63201;e.commamonospace=65292;e.commareversedabovecmb=788;e.commareversedmod=701;e.commasmall=65104;e.commasuperior=63202;e.commaturnedabovecmb=786;e.commaturnedmod=699;e.compass=9788;e.congruent=8773;e.contourintegral=8750;e.control=8963;e.controlACK=6;e.controlBEL=7;e.controlBS=8;e.controlCAN=24;e.controlCR=13;e.controlDC1=17;e.controlDC2=18;e.controlDC3=19;e.controlDC4=20;e.controlDEL=127;e.controlDLE=16;e.controlEM=25;e.controlENQ=5;e.controlEOT=4;e.controlESC=27;e.controlETB=23;e.controlETX=3;e.controlFF=12;e.controlFS=28;e.controlGS=29;e.controlHT=9;e.controlLF=10;e.controlNAK=21;e.controlNULL=0;e.controlRS=30;e.controlSI=15;e.controlSO=14;e.controlSOT=2;e.controlSTX=1;e.controlSUB=26;e.controlSYN=22;e.controlUS=31;e.controlVT=11;e.copyright=169;e.copyrightsans=63721;e.copyrightserif=63193;e.cornerbracketleft=12300;e.cornerbracketlefthalfwidth=65378;e.cornerbracketleftvertical=65089;e.cornerbracketright=12301;e.cornerbracketrighthalfwidth=65379;e.cornerbracketrightvertical=65090;e.corporationsquare=13183;e.cosquare=13255;e.coverkgsquare=13254;e.cparen=9374;e.cruzeiro=8354;e.cstretched=663;e.curlyand=8911;e.curlyor=8910;e.currency=164;e.cyrBreve=63185;e.cyrFlex=63186;e.cyrbreve=63188;e.cyrflex=63189;e.d=100;e.daarmenian=1380;e.dabengali=2470;e.dadarabic=1590;e.dadeva=2342;e.dadfinalarabic=65214;e.dadinitialarabic=65215;e.dadmedialarabic=65216;e.dagesh=1468;e.dageshhebrew=1468;e.dagger=8224;e.daggerdbl=8225;e.dagujarati=2726;e.dagurmukhi=2598;e.dahiragana=12384;e.dakatakana=12480;e.dalarabic=1583;e.dalet=1491;e.daletdagesh=64307;e.daletdageshhebrew=64307;e.dalethebrew=1491;e.dalfinalarabic=65194;e.dammaarabic=1615;e.dammalowarabic=1615;e.dammatanaltonearabic=1612;e.dammatanarabic=1612;e.danda=2404;e.dargahebrew=1447;e.dargalefthebrew=1447;e.dasiapneumatacyrilliccmb=1157;e.dblGrave=63187;e.dblanglebracketleft=12298;e.dblanglebracketleftvertical=65085;e.dblanglebracketright=12299;e.dblanglebracketrightvertical=65086;e.dblarchinvertedbelowcmb=811;e.dblarrowleft=8660;e.dblarrowright=8658;e.dbldanda=2405;e.dblgrave=63190;e.dblgravecmb=783;e.dblintegral=8748;e.dbllowline=8215;e.dbllowlinecmb=819;e.dbloverlinecmb=831;e.dblprimemod=698;e.dblverticalbar=8214;e.dblverticallineabovecmb=782;e.dbopomofo=12553;e.dbsquare=13256;e.dcaron=271;e.dcedilla=7697;e.dcircle=9427;e.dcircumflexbelow=7699;e.dcroat=273;e.ddabengali=2465;e.ddadeva=2337;e.ddagujarati=2721;e.ddagurmukhi=2593;e.ddalarabic=1672;e.ddalfinalarabic=64393;e.dddhadeva=2396;e.ddhabengali=2466;e.ddhadeva=2338;e.ddhagujarati=2722;e.ddhagurmukhi=2594;e.ddotaccent=7691;e.ddotbelow=7693;e.decimalseparatorarabic=1643;e.decimalseparatorpersian=1643;e.decyrillic=1076;e.degree=176;e.dehihebrew=1453;e.dehiragana=12391;e.deicoptic=1007;e.dekatakana=12487;e.deleteleft=9003;e.deleteright=8998;e.delta=948;e.deltaturned=397;e.denominatorminusonenumeratorbengali=2552;e.dezh=676;e.dhabengali=2471;e.dhadeva=2343;e.dhagujarati=2727;e.dhagurmukhi=2599;e.dhook=599;e.dialytikatonos=901;e.dialytikatonoscmb=836;e.diamond=9830;e.diamondsuitwhite=9826;e.dieresis=168;e.dieresisacute=63191;e.dieresisbelowcmb=804;e.dieresiscmb=776;e.dieresisgrave=63192;e.dieresistonos=901;e.dihiragana=12386;e.dikatakana=12482;e.dittomark=12291;e.divide=247;e.divides=8739;e.divisionslash=8725;e.djecyrillic=1106;e.dkshade=9619;e.dlinebelow=7695;e.dlsquare=13207;e.dmacron=273;e.dmonospace=65348;e.dnblock=9604;e.dochadathai=3598;e.dodekthai=3604;e.dohiragana=12393;e.dokatakana=12489;e.dollar=36;e.dollarinferior=63203;e.dollarmonospace=65284;e.dollaroldstyle=63268;e.dollarsmall=65129;e.dollarsuperior=63204;e.dong=8363;e.dorusquare=13094;e.dotaccent=729;e.dotaccentcmb=775;e.dotbelowcmb=803;e.dotbelowcomb=803;e.dotkatakana=12539;e.dotlessi=305;e.dotlessj=63166;e.dotlessjstrokehook=644;e.dotmath=8901;e.dottedcircle=9676;e.doubleyodpatah=64287;e.doubleyodpatahhebrew=64287;e.downtackbelowcmb=798;e.downtackmod=725;e.dparen=9375;e.dsuperior=63211;e.dtail=598;e.dtopbar=396;e.duhiragana=12389;e.dukatakana=12485;e.dz=499;e.dzaltone=675;e.dzcaron=454;e.dzcurl=677;e.dzeabkhasiancyrillic=1249;e.dzecyrillic=1109;e.dzhecyrillic=1119;e.e=101;e.eacute=233;e.earth=9793;e.ebengali=2447;e.ebopomofo=12572;e.ebreve=277;e.ecandradeva=2317;e.ecandragujarati=2701;e.ecandravowelsigndeva=2373;e.ecandravowelsigngujarati=2757;e.ecaron=283;e.ecedillabreve=7709;e.echarmenian=1381;e.echyiwnarmenian=1415;e.ecircle=9428;e.ecircumflex=234;e.ecircumflexacute=7871;e.ecircumflexbelow=7705;e.ecircumflexdotbelow=7879;e.ecircumflexgrave=7873;e.ecircumflexhookabove=7875;e.ecircumflextilde=7877;e.ecyrillic=1108;e.edblgrave=517;e.edeva=2319;e.edieresis=235;e.edot=279;e.edotaccent=279;e.edotbelow=7865;e.eegurmukhi=2575;e.eematragurmukhi=2631;e.efcyrillic=1092;e.egrave=232;e.egujarati=2703;e.eharmenian=1383;e.ehbopomofo=12573;e.ehiragana=12360;e.ehookabove=7867;e.eibopomofo=12575;e.eight=56;e.eightarabic=1640;e.eightbengali=2542;e.eightcircle=9319;e.eightcircleinversesansserif=10129;e.eightdeva=2414;e.eighteencircle=9329;e.eighteenparen=9349;e.eighteenperiod=9369;e.eightgujarati=2798;e.eightgurmukhi=2670;e.eighthackarabic=1640;e.eighthangzhou=12328;e.eighthnotebeamed=9835;e.eightideographicparen=12839;e.eightinferior=8328;e.eightmonospace=65304;e.eightoldstyle=63288;e.eightparen=9339;e.eightperiod=9359;e.eightpersian=1784;e.eightroman=8567;e.eightsuperior=8312;e.eightthai=3672;e.einvertedbreve=519;e.eiotifiedcyrillic=1125;e.ekatakana=12456;e.ekatakanahalfwidth=65396;e.ekonkargurmukhi=2676;e.ekorean=12628;e.elcyrillic=1083;e.element=8712;e.elevencircle=9322;e.elevenparen=9342;e.elevenperiod=9362;e.elevenroman=8570;e.ellipsis=8230;e.ellipsisvertical=8942;e.emacron=275;e.emacronacute=7703;e.emacrongrave=7701;e.emcyrillic=1084;e.emdash=8212;e.emdashvertical=65073;e.emonospace=65349;e.emphasismarkarmenian=1371;e.emptyset=8709;e.enbopomofo=12579;e.encyrillic=1085;e.endash=8211;e.endashvertical=65074;e.endescendercyrillic=1187;e.eng=331;e.engbopomofo=12581;e.enghecyrillic=1189;e.enhookcyrillic=1224;e.enspace=8194;e.eogonek=281;e.eokorean=12627;e.eopen=603;e.eopenclosed=666;e.eopenreversed=604;e.eopenreversedclosed=606;e.eopenreversedhook=605;e.eparen=9376;e.epsilon=949;e.epsilontonos=941;e.equal=61;e.equalmonospace=65309;e.equalsmall=65126;e.equalsuperior=8316;e.equivalence=8801;e.erbopomofo=12582;e.ercyrillic=1088;e.ereversed=600;e.ereversedcyrillic=1101;e.escyrillic=1089;e.esdescendercyrillic=1195;e.esh=643;e.eshcurl=646;e.eshortdeva=2318;e.eshortvowelsigndeva=2374;e.eshreversedloop=426;e.eshsquatreversed=645;e.esmallhiragana=12359;e.esmallkatakana=12455;e.esmallkatakanahalfwidth=65386;e.estimated=8494;e.esuperior=63212;e.eta=951;e.etarmenian=1384;e.etatonos=942;e.eth=240;e.etilde=7869;e.etildebelow=7707;e.etnahtafoukhhebrew=1425;e.etnahtafoukhlefthebrew=1425;e.etnahtahebrew=1425;e.etnahtalefthebrew=1425;e.eturned=477;e.eukorean=12641;e.euro=8364;e.evowelsignbengali=2503;e.evowelsigndeva=2375;e.evowelsigngujarati=2759;e.exclam=33;e.exclamarmenian=1372;e.exclamdbl=8252;e.exclamdown=161;e.exclamdownsmall=63393;e.exclammonospace=65281;e.exclamsmall=63265;e.existential=8707;e.ezh=658;e.ezhcaron=495;e.ezhcurl=659;e.ezhreversed=441;e.ezhtail=442;e.f=102;e.fadeva=2398;e.fagurmukhi=2654;e.fahrenheit=8457;e.fathaarabic=1614;e.fathalowarabic=1614;e.fathatanarabic=1611;e.fbopomofo=12552;e.fcircle=9429;e.fdotaccent=7711;e.feharabic=1601;e.feharmenian=1414;e.fehfinalarabic=65234;e.fehinitialarabic=65235;e.fehmedialarabic=65236;e.feicoptic=997;e.female=9792;e.ff=64256;e.f_f=64256;e.ffi=64259;e.f_f_i=64259;e.ffl=64260;e.f_f_l=64260;e.fi=64257;e.f_i=64257;e.fifteencircle=9326;e.fifteenparen=9346;e.fifteenperiod=9366;e.figuredash=8210;e.filledbox=9632;e.filledrect=9644;e.finalkaf=1498;e.finalkafdagesh=64314;e.finalkafdageshhebrew=64314;e.finalkafhebrew=1498;e.finalmem=1501;e.finalmemhebrew=1501;e.finalnun=1503;e.finalnunhebrew=1503;e.finalpe=1507;e.finalpehebrew=1507;e.finaltsadi=1509;e.finaltsadihebrew=1509;e.firsttonechinese=713;e.fisheye=9673;e.fitacyrillic=1139;e.five=53;e.fivearabic=1637;e.fivebengali=2539;e.fivecircle=9316;e.fivecircleinversesansserif=10126;e.fivedeva=2411;e.fiveeighths=8541;e.fivegujarati=2795;e.fivegurmukhi=2667;e.fivehackarabic=1637;e.fivehangzhou=12325;e.fiveideographicparen=12836;e.fiveinferior=8325;e.fivemonospace=65301;e.fiveoldstyle=63285;e.fiveparen=9336;e.fiveperiod=9356;e.fivepersian=1781;e.fiveroman=8564;e.fivesuperior=8309;e.fivethai=3669;e.fl=64258;e.f_l=64258;e.florin=402;e.fmonospace=65350;e.fmsquare=13209;e.fofanthai=3615;e.fofathai=3613;e.fongmanthai=3663;e.forall=8704;e.four=52;e.fourarabic=1636;e.fourbengali=2538;e.fourcircle=9315;e.fourcircleinversesansserif=10125;e.fourdeva=2410;e.fourgujarati=2794;e.fourgurmukhi=2666;e.fourhackarabic=1636;e.fourhangzhou=12324;e.fourideographicparen=12835;e.fourinferior=8324;e.fourmonospace=65300;e.fournumeratorbengali=2551;e.fouroldstyle=63284;e.fourparen=9335;e.fourperiod=9355;e.fourpersian=1780;e.fourroman=8563;e.foursuperior=8308;e.fourteencircle=9325;e.fourteenparen=9345;e.fourteenperiod=9365;e.fourthai=3668;e.fourthtonechinese=715;e.fparen=9377;e.fraction=8260;e.franc=8355;e.g=103;e.gabengali=2455;e.gacute=501;e.gadeva=2327;e.gafarabic=1711;e.gaffinalarabic=64403;e.gafinitialarabic=64404;e.gafmedialarabic=64405;e.gagujarati=2711;e.gagurmukhi=2583;e.gahiragana=12364;e.gakatakana=12460;e.gamma=947;e.gammalatinsmall=611;e.gammasuperior=736;e.gangiacoptic=1003;e.gbopomofo=12557;e.gbreve=287;e.gcaron=487;e.gcedilla=291;e.gcircle=9430;e.gcircumflex=285;e.gcommaaccent=291;e.gdot=289;e.gdotaccent=289;e.gecyrillic=1075;e.gehiragana=12370;e.gekatakana=12466;e.geometricallyequal=8785;e.gereshaccenthebrew=1436;e.gereshhebrew=1523;e.gereshmuqdamhebrew=1437;e.germandbls=223;e.gershayimaccenthebrew=1438;e.gershayimhebrew=1524;e.getamark=12307;e.ghabengali=2456;e.ghadarmenian=1394;e.ghadeva=2328;e.ghagujarati=2712;e.ghagurmukhi=2584;e.ghainarabic=1594;e.ghainfinalarabic=65230;e.ghaininitialarabic=65231;e.ghainmedialarabic=65232;e.ghemiddlehookcyrillic=1173;e.ghestrokecyrillic=1171;e.gheupturncyrillic=1169;e.ghhadeva=2394;e.ghhagurmukhi=2650;e.ghook=608;e.ghzsquare=13203;e.gihiragana=12366;e.gikatakana=12462;e.gimarmenian=1379;e.gimel=1490;e.gimeldagesh=64306;e.gimeldageshhebrew=64306;e.gimelhebrew=1490;e.gjecyrillic=1107;e.glottalinvertedstroke=446;e.glottalstop=660;e.glottalstopinverted=662;e.glottalstopmod=704;e.glottalstopreversed=661;e.glottalstopreversedmod=705;e.glottalstopreversedsuperior=740;e.glottalstopstroke=673;e.glottalstopstrokereversed=674;e.gmacron=7713;e.gmonospace=65351;e.gohiragana=12372;e.gokatakana=12468;e.gparen=9378;e.gpasquare=13228;e.gradient=8711;e.grave=96;e.gravebelowcmb=790;e.gravecmb=768;e.gravecomb=768;e.gravedeva=2387;e.gravelowmod=718;e.gravemonospace=65344;e.gravetonecmb=832;e.greater=62;e.greaterequal=8805;e.greaterequalorless=8923;e.greatermonospace=65310;e.greaterorequivalent=8819;e.greaterorless=8823;e.greateroverequal=8807;e.greatersmall=65125;e.gscript=609;e.gstroke=485;e.guhiragana=12368;e.guillemotleft=171;e.guillemotright=187;e.guilsinglleft=8249;e.guilsinglright=8250;e.gukatakana=12464;e.guramusquare=13080;e.gysquare=13257;e.h=104;e.haabkhasiancyrillic=1193;e.haaltonearabic=1729;e.habengali=2489;e.hadescendercyrillic=1203;e.hadeva=2361;e.hagujarati=2745;e.hagurmukhi=2617;e.haharabic=1581;e.hahfinalarabic=65186;e.hahinitialarabic=65187;e.hahiragana=12399;e.hahmedialarabic=65188;e.haitusquare=13098;e.hakatakana=12495;e.hakatakanahalfwidth=65418;e.halantgurmukhi=2637;e.hamzaarabic=1569;e.hamzalowarabic=1569;e.hangulfiller=12644;e.hardsigncyrillic=1098;e.harpoonleftbarbup=8636;e.harpoonrightbarbup=8640;e.hasquare=13258;e.hatafpatah=1458;e.hatafpatah16=1458;e.hatafpatah23=1458;e.hatafpatah2f=1458;e.hatafpatahhebrew=1458;e.hatafpatahnarrowhebrew=1458;e.hatafpatahquarterhebrew=1458;e.hatafpatahwidehebrew=1458;e.hatafqamats=1459;e.hatafqamats1b=1459;e.hatafqamats28=1459;e.hatafqamats34=1459;e.hatafqamatshebrew=1459;e.hatafqamatsnarrowhebrew=1459;e.hatafqamatsquarterhebrew=1459;e.hatafqamatswidehebrew=1459;e.hatafsegol=1457;e.hatafsegol17=1457;e.hatafsegol24=1457;e.hatafsegol30=1457;e.hatafsegolhebrew=1457;e.hatafsegolnarrowhebrew=1457;e.hatafsegolquarterhebrew=1457;e.hatafsegolwidehebrew=1457;e.hbar=295;e.hbopomofo=12559;e.hbrevebelow=7723;e.hcedilla=7721;e.hcircle=9431;e.hcircumflex=293;e.hdieresis=7719;e.hdotaccent=7715;e.hdotbelow=7717;e.he=1492;e.heart=9829;e.heartsuitblack=9829;e.heartsuitwhite=9825;e.hedagesh=64308;e.hedageshhebrew=64308;e.hehaltonearabic=1729;e.heharabic=1607;e.hehebrew=1492;e.hehfinalaltonearabic=64423;e.hehfinalalttwoarabic=65258;e.hehfinalarabic=65258;e.hehhamzaabovefinalarabic=64421;e.hehhamzaaboveisolatedarabic=64420;e.hehinitialaltonearabic=64424;e.hehinitialarabic=65259;e.hehiragana=12408;e.hehmedialaltonearabic=64425;e.hehmedialarabic=65260;e.heiseierasquare=13179;e.hekatakana=12504;e.hekatakanahalfwidth=65421;e.hekutaarusquare=13110;e.henghook=615;e.herutusquare=13113;e.het=1495;e.hethebrew=1495;e.hhook=614;e.hhooksuperior=689;e.hieuhacirclekorean=12923;e.hieuhaparenkorean=12827;e.hieuhcirclekorean=12909;e.hieuhkorean=12622;e.hieuhparenkorean=12813;e.hihiragana=12402;e.hikatakana=12498;e.hikatakanahalfwidth=65419;e.hiriq=1460;e.hiriq14=1460;e.hiriq21=1460;e.hiriq2d=1460;e.hiriqhebrew=1460;e.hiriqnarrowhebrew=1460;e.hiriqquarterhebrew=1460;e.hiriqwidehebrew=1460;e.hlinebelow=7830;e.hmonospace=65352;e.hoarmenian=1392;e.hohipthai=3627;e.hohiragana=12411;e.hokatakana=12507;e.hokatakanahalfwidth=65422;e.holam=1465;e.holam19=1465;e.holam26=1465;e.holam32=1465;e.holamhebrew=1465;e.holamnarrowhebrew=1465;e.holamquarterhebrew=1465;e.holamwidehebrew=1465;e.honokhukthai=3630;e.hookabovecomb=777;e.hookcmb=777;e.hookpalatalizedbelowcmb=801;e.hookretroflexbelowcmb=802;e.hoonsquare=13122;e.horicoptic=1001;e.horizontalbar=8213;e.horncmb=795;e.hotsprings=9832;e.house=8962;e.hparen=9379;e.hsuperior=688;e.hturned=613;e.huhiragana=12405;e.huiitosquare=13107;e.hukatakana=12501;e.hukatakanahalfwidth=65420;e.hungarumlaut=733;e.hungarumlautcmb=779;e.hv=405;e.hyphen=45;e.hypheninferior=63205;e.hyphenmonospace=65293;e.hyphensmall=65123;e.hyphensuperior=63206;e.hyphentwo=8208;e.i=105;e.iacute=237;e.iacyrillic=1103;e.ibengali=2439;e.ibopomofo=12583;e.ibreve=301;e.icaron=464;e.icircle=9432;e.icircumflex=238;e.icyrillic=1110;e.idblgrave=521;e.ideographearthcircle=12943;e.ideographfirecircle=12939;e.ideographicallianceparen=12863;e.ideographiccallparen=12858;e.ideographiccentrecircle=12965;e.ideographicclose=12294;e.ideographiccomma=12289;e.ideographiccommaleft=65380;e.ideographiccongratulationparen=12855;e.ideographiccorrectcircle=12963;e.ideographicearthparen=12847;e.ideographicenterpriseparen=12861;e.ideographicexcellentcircle=12957;e.ideographicfestivalparen=12864;e.ideographicfinancialcircle=12950;e.ideographicfinancialparen=12854;e.ideographicfireparen=12843;e.ideographichaveparen=12850;e.ideographichighcircle=12964;e.ideographiciterationmark=12293;e.ideographiclaborcircle=12952;e.ideographiclaborparen=12856;e.ideographicleftcircle=12967;e.ideographiclowcircle=12966;e.ideographicmedicinecircle=12969;e.ideographicmetalparen=12846;e.ideographicmoonparen=12842;e.ideographicnameparen=12852;e.ideographicperiod=12290;e.ideographicprintcircle=12958;e.ideographicreachparen=12867;e.ideographicrepresentparen=12857;e.ideographicresourceparen=12862;e.ideographicrightcircle=12968;e.ideographicsecretcircle=12953;e.ideographicselfparen=12866;e.ideographicsocietyparen=12851;e.ideographicspace=12288;e.ideographicspecialparen=12853;e.ideographicstockparen=12849;e.ideographicstudyparen=12859;e.ideographicsunparen=12848;e.ideographicsuperviseparen=12860;e.ideographicwaterparen=12844;e.ideographicwoodparen=12845;e.ideographiczero=12295;e.ideographmetalcircle=12942;e.ideographmooncircle=12938;e.ideographnamecircle=12948;e.ideographsuncircle=12944;e.ideographwatercircle=12940;e.ideographwoodcircle=12941;e.ideva=2311;e.idieresis=239;e.idieresisacute=7727;e.idieresiscyrillic=1253;e.idotbelow=7883;e.iebrevecyrillic=1239;e.iecyrillic=1077;e.ieungacirclekorean=12917;e.ieungaparenkorean=12821;e.ieungcirclekorean=12903;e.ieungkorean=12615;e.ieungparenkorean=12807;e.igrave=236;e.igujarati=2695;e.igurmukhi=2567;e.ihiragana=12356;e.ihookabove=7881;e.iibengali=2440;e.iicyrillic=1080;e.iideva=2312;e.iigujarati=2696;e.iigurmukhi=2568;e.iimatragurmukhi=2624;e.iinvertedbreve=523;e.iishortcyrillic=1081;e.iivowelsignbengali=2496;e.iivowelsigndeva=2368;e.iivowelsigngujarati=2752;e.ij=307;e.ikatakana=12452;e.ikatakanahalfwidth=65394;e.ikorean=12643;e.ilde=732;e.iluyhebrew=1452;e.imacron=299;e.imacroncyrillic=1251;e.imageorapproximatelyequal=8787;e.imatragurmukhi=2623;e.imonospace=65353;e.increment=8710;e.infinity=8734;e.iniarmenian=1387;e.integral=8747;e.integralbottom=8993;e.integralbt=8993;e.integralex=63733;e.integraltop=8992;e.integraltp=8992;e.intersection=8745;e.intisquare=13061;e.invbullet=9688;e.invcircle=9689;e.invsmileface=9787;e.iocyrillic=1105;e.iogonek=303;e.iota=953;e.iotadieresis=970;e.iotadieresistonos=912;e.iotalatin=617;e.iotatonos=943;e.iparen=9380;e.irigurmukhi=2674;e.ismallhiragana=12355;e.ismallkatakana=12451;e.ismallkatakanahalfwidth=65384;e.issharbengali=2554;e.istroke=616;e.isuperior=63213;e.iterationhiragana=12445;e.iterationkatakana=12541;e.itilde=297;e.itildebelow=7725;e.iubopomofo=12585;e.iucyrillic=1102;e.ivowelsignbengali=2495;e.ivowelsigndeva=2367;e.ivowelsigngujarati=2751;e.izhitsacyrillic=1141;e.izhitsadblgravecyrillic=1143;e.j=106;e.jaarmenian=1393;e.jabengali=2460;e.jadeva=2332;e.jagujarati=2716;e.jagurmukhi=2588;e.jbopomofo=12560;e.jcaron=496;e.jcircle=9433;e.jcircumflex=309;e.jcrossedtail=669;e.jdotlessstroke=607;e.jecyrillic=1112;e.jeemarabic=1580;e.jeemfinalarabic=65182;e.jeeminitialarabic=65183;e.jeemmedialarabic=65184;e.jeharabic=1688;e.jehfinalarabic=64395;e.jhabengali=2461;e.jhadeva=2333;e.jhagujarati=2717;e.jhagurmukhi=2589;e.jheharmenian=1403;e.jis=12292;e.jmonospace=65354;e.jparen=9381;e.jsuperior=690;e.k=107;e.kabashkircyrillic=1185;e.kabengali=2453;e.kacute=7729;e.kacyrillic=1082;e.kadescendercyrillic=1179;e.kadeva=2325;e.kaf=1499;e.kafarabic=1603;e.kafdagesh=64315;e.kafdageshhebrew=64315;e.kaffinalarabic=65242;e.kafhebrew=1499;e.kafinitialarabic=65243;e.kafmedialarabic=65244;e.kafrafehebrew=64333;e.kagujarati=2709;e.kagurmukhi=2581;e.kahiragana=12363;e.kahookcyrillic=1220;e.kakatakana=12459;e.kakatakanahalfwidth=65398;e.kappa=954;e.kappasymbolgreek=1008;e.kapyeounmieumkorean=12657;e.kapyeounphieuphkorean=12676;e.kapyeounpieupkorean=12664;e.kapyeounssangpieupkorean=12665;e.karoriisquare=13069;e.kashidaautoarabic=1600;e.kashidaautonosidebearingarabic=1600;e.kasmallkatakana=12533;e.kasquare=13188;e.kasraarabic=1616;e.kasratanarabic=1613;e.kastrokecyrillic=1183;e.katahiraprolongmarkhalfwidth=65392;e.kaverticalstrokecyrillic=1181;e.kbopomofo=12558;e.kcalsquare=13193;e.kcaron=489;e.kcedilla=311;e.kcircle=9434;e.kcommaaccent=311;e.kdotbelow=7731;e.keharmenian=1412;e.kehiragana=12369;e.kekatakana=12465;e.kekatakanahalfwidth=65401;e.kenarmenian=1391;e.kesmallkatakana=12534;e.kgreenlandic=312;e.khabengali=2454;e.khacyrillic=1093;e.khadeva=2326;e.khagujarati=2710;e.khagurmukhi=2582;e.khaharabic=1582;e.khahfinalarabic=65190;e.khahinitialarabic=65191;e.khahmedialarabic=65192;e.kheicoptic=999;e.khhadeva=2393;e.khhagurmukhi=2649;e.khieukhacirclekorean=12920;e.khieukhaparenkorean=12824;e.khieukhcirclekorean=12906;e.khieukhkorean=12619;e.khieukhparenkorean=12810;e.khokhaithai=3586;e.khokhonthai=3589;e.khokhuatthai=3587;e.khokhwaithai=3588;e.khomutthai=3675;e.khook=409;e.khorakhangthai=3590;e.khzsquare=13201;e.kihiragana=12365;e.kikatakana=12461;e.kikatakanahalfwidth=65399;e.kiroguramusquare=13077;e.kiromeetorusquare=13078;e.kirosquare=13076;e.kiyeokacirclekorean=12910;e.kiyeokaparenkorean=12814;e.kiyeokcirclekorean=12896;e.kiyeokkorean=12593;e.kiyeokparenkorean=12800;e.kiyeoksioskorean=12595;e.kjecyrillic=1116;e.klinebelow=7733;e.klsquare=13208;e.kmcubedsquare=13222;e.kmonospace=65355;e.kmsquaredsquare=13218;e.kohiragana=12371;e.kohmsquare=13248;e.kokaithai=3585;e.kokatakana=12467;e.kokatakanahalfwidth=65402;e.kooposquare=13086;e.koppacyrillic=1153;e.koreanstandardsymbol=12927;e.koroniscmb=835;e.kparen=9382;e.kpasquare=13226;e.ksicyrillic=1135;e.ktsquare=13263;e.kturned=670;e.kuhiragana=12367;e.kukatakana=12463;e.kukatakanahalfwidth=65400;e.kvsquare=13240;e.kwsquare=13246;e.l=108;e.labengali=2482;e.lacute=314;e.ladeva=2354;e.lagujarati=2738;e.lagurmukhi=2610;e.lakkhangyaothai=3653;e.lamaleffinalarabic=65276;e.lamalefhamzaabovefinalarabic=65272;e.lamalefhamzaaboveisolatedarabic=65271;e.lamalefhamzabelowfinalarabic=65274;e.lamalefhamzabelowisolatedarabic=65273;e.lamalefisolatedarabic=65275;e.lamalefmaddaabovefinalarabic=65270;e.lamalefmaddaaboveisolatedarabic=65269;e.lamarabic=1604;e.lambda=955;e.lambdastroke=411;e.lamed=1500;e.lameddagesh=64316;e.lameddageshhebrew=64316;e.lamedhebrew=1500;e.lamfinalarabic=65246;e.lamhahinitialarabic=64714;e.laminitialarabic=65247;e.lamjeeminitialarabic=64713;e.lamkhahinitialarabic=64715;e.lamlamhehisolatedarabic=65010;e.lammedialarabic=65248;e.lammeemhahinitialarabic=64904;e.lammeeminitialarabic=64716;e.largecircle=9711;e.lbar=410;e.lbelt=620;e.lbopomofo=12556;e.lcaron=318;e.lcedilla=316;e.lcircle=9435;e.lcircumflexbelow=7741;e.lcommaaccent=316;e.ldot=320;e.ldotaccent=320;e.ldotbelow=7735;e.ldotbelowmacron=7737;e.leftangleabovecmb=794;e.lefttackbelowcmb=792;e.less=60;e.lessequal=8804;e.lessequalorgreater=8922;e.lessmonospace=65308;e.lessorequivalent=8818;e.lessorgreater=8822;e.lessoverequal=8806;e.lesssmall=65124;e.lezh=622;e.lfblock=9612;e.lhookretroflex=621;e.lira=8356;e.liwnarmenian=1388;e.lj=457;e.ljecyrillic=1113;e.ll=63168;e.lladeva=2355;e.llagujarati=2739;e.llinebelow=7739;e.llladeva=2356;e.llvocalicbengali=2529;e.llvocalicdeva=2401;e.llvocalicvowelsignbengali=2531;e.llvocalicvowelsigndeva=2403;e.lmiddletilde=619;e.lmonospace=65356;e.lmsquare=13264;e.lochulathai=3628;e.logicaland=8743;e.logicalnot=172;e.logicalnotreversed=8976;e.logicalor=8744;e.lolingthai=3621;e.longs=383;e.lowlinecenterline=65102;e.lowlinecmb=818;e.lowlinedashed=65101;e.lozenge=9674;e.lparen=9383;e.lslash=322;e.lsquare=8467;e.lsuperior=63214;e.ltshade=9617;e.luthai=3622;e.lvocalicbengali=2444;e.lvocalicdeva=2316;e.lvocalicvowelsignbengali=2530;e.lvocalicvowelsigndeva=2402;e.lxsquare=13267;e.m=109;e.mabengali=2478;e.macron=175;e.macronbelowcmb=817;e.macroncmb=772;e.macronlowmod=717;e.macronmonospace=65507;e.macute=7743;e.madeva=2350;e.magujarati=2734;e.magurmukhi=2606;e.mahapakhhebrew=1444;e.mahapakhlefthebrew=1444;e.mahiragana=12414;e.maichattawalowleftthai=63637;e.maichattawalowrightthai=63636;e.maichattawathai=3659;e.maichattawaupperleftthai=63635;e.maieklowleftthai=63628;e.maieklowrightthai=63627;e.maiekthai=3656;e.maiekupperleftthai=63626;e.maihanakatleftthai=63620;e.maihanakatthai=3633;e.maitaikhuleftthai=63625;e.maitaikhuthai=3655;e.maitholowleftthai=63631;e.maitholowrightthai=63630;e.maithothai=3657;e.maithoupperleftthai=63629;e.maitrilowleftthai=63634;e.maitrilowrightthai=63633;e.maitrithai=3658;e.maitriupperleftthai=63632;e.maiyamokthai=3654;e.makatakana=12510;e.makatakanahalfwidth=65423;e.male=9794;e.mansyonsquare=13127;e.maqafhebrew=1470;e.mars=9794;e.masoracirclehebrew=1455;e.masquare=13187;e.mbopomofo=12551;e.mbsquare=13268;e.mcircle=9436;e.mcubedsquare=13221;e.mdotaccent=7745;e.mdotbelow=7747;e.meemarabic=1605;e.meemfinalarabic=65250;e.meeminitialarabic=65251;e.meemmedialarabic=65252;e.meemmeeminitialarabic=64721;e.meemmeemisolatedarabic=64584;e.meetorusquare=13133;e.mehiragana=12417;e.meizierasquare=13182;e.mekatakana=12513;e.mekatakanahalfwidth=65426;e.mem=1502;e.memdagesh=64318;e.memdageshhebrew=64318;e.memhebrew=1502;e.menarmenian=1396;e.merkhahebrew=1445;e.merkhakefulahebrew=1446;e.merkhakefulalefthebrew=1446;e.merkhalefthebrew=1445;e.mhook=625;e.mhzsquare=13202;e.middledotkatakanahalfwidth=65381;e.middot=183;e.mieumacirclekorean=12914;e.mieumaparenkorean=12818;e.mieumcirclekorean=12900;e.mieumkorean=12609;e.mieumpansioskorean=12656;e.mieumparenkorean=12804;e.mieumpieupkorean=12654;e.mieumsioskorean=12655;e.mihiragana=12415;e.mikatakana=12511;e.mikatakanahalfwidth=65424;e.minus=8722;e.minusbelowcmb=800;e.minuscircle=8854;e.minusmod=727;e.minusplus=8723;e.minute=8242;e.miribaarusquare=13130;e.mirisquare=13129;e.mlonglegturned=624;e.mlsquare=13206;e.mmcubedsquare=13219;e.mmonospace=65357;e.mmsquaredsquare=13215;e.mohiragana=12418;e.mohmsquare=13249;e.mokatakana=12514;e.mokatakanahalfwidth=65427;e.molsquare=13270;e.momathai=3617;e.moverssquare=13223;e.moverssquaredsquare=13224;e.mparen=9384;e.mpasquare=13227;e.mssquare=13235;e.msuperior=63215;e.mturned=623;e.mu=181;e.mu1=181;e.muasquare=13186;e.muchgreater=8811;e.muchless=8810;e.mufsquare=13196;e.mugreek=956;e.mugsquare=13197;e.muhiragana=12416;e.mukatakana=12512;e.mukatakanahalfwidth=65425;e.mulsquare=13205;e.multiply=215;e.mumsquare=13211;e.munahhebrew=1443;e.munahlefthebrew=1443;e.musicalnote=9834;e.musicalnotedbl=9835;e.musicflatsign=9837;e.musicsharpsign=9839;e.mussquare=13234;e.muvsquare=13238;e.muwsquare=13244;e.mvmegasquare=13241;e.mvsquare=13239;e.mwmegasquare=13247;e.mwsquare=13245;e.n=110;e.nabengali=2472;e.nabla=8711;e.nacute=324;e.nadeva=2344;e.nagujarati=2728;e.nagurmukhi=2600;e.nahiragana=12394;e.nakatakana=12490;e.nakatakanahalfwidth=65413;e.napostrophe=329;e.nasquare=13185;e.nbopomofo=12555;e.nbspace=160;e.ncaron=328;e.ncedilla=326;e.ncircle=9437;e.ncircumflexbelow=7755;e.ncommaaccent=326;e.ndotaccent=7749;e.ndotbelow=7751;e.nehiragana=12397;e.nekatakana=12493;e.nekatakanahalfwidth=65416;e.newsheqelsign=8362;e.nfsquare=13195;e.ngabengali=2457;e.ngadeva=2329;e.ngagujarati=2713;e.ngagurmukhi=2585;e.ngonguthai=3591;e.nhiragana=12435;e.nhookleft=626;e.nhookretroflex=627;e.nieunacirclekorean=12911;e.nieunaparenkorean=12815;e.nieuncieuckorean=12597;e.nieuncirclekorean=12897;e.nieunhieuhkorean=12598;e.nieunkorean=12596;e.nieunpansioskorean=12648;e.nieunparenkorean=12801;e.nieunsioskorean=12647;e.nieuntikeutkorean=12646;e.nihiragana=12395;e.nikatakana=12491;e.nikatakanahalfwidth=65414;e.nikhahitleftthai=63641;e.nikhahitthai=3661;e.nine=57;e.ninearabic=1641;e.ninebengali=2543;e.ninecircle=9320;e.ninecircleinversesansserif=10130;e.ninedeva=2415;e.ninegujarati=2799;e.ninegurmukhi=2671;e.ninehackarabic=1641;e.ninehangzhou=12329;e.nineideographicparen=12840;e.nineinferior=8329;e.ninemonospace=65305;e.nineoldstyle=63289;e.nineparen=9340;e.nineperiod=9360;e.ninepersian=1785;e.nineroman=8568;e.ninesuperior=8313;e.nineteencircle=9330;e.nineteenparen=9350;e.nineteenperiod=9370;e.ninethai=3673;e.nj=460;e.njecyrillic=1114;e.nkatakana=12531;e.nkatakanahalfwidth=65437;e.nlegrightlong=414;e.nlinebelow=7753;e.nmonospace=65358;e.nmsquare=13210;e.nnabengali=2467;e.nnadeva=2339;e.nnagujarati=2723;e.nnagurmukhi=2595;e.nnnadeva=2345;e.nohiragana=12398;e.nokatakana=12494;e.nokatakanahalfwidth=65417;e.nonbreakingspace=160;e.nonenthai=3603;e.nonuthai=3609;e.noonarabic=1606;e.noonfinalarabic=65254;e.noonghunnaarabic=1722;e.noonghunnafinalarabic=64415;e.nooninitialarabic=65255;e.noonjeeminitialarabic=64722;e.noonjeemisolatedarabic=64587;e.noonmedialarabic=65256;e.noonmeeminitialarabic=64725;e.noonmeemisolatedarabic=64590;e.noonnoonfinalarabic=64653;e.notcontains=8716;e.notelement=8713;e.notelementof=8713;e.notequal=8800;e.notgreater=8815;e.notgreaternorequal=8817;e.notgreaternorless=8825;e.notidentical=8802;e.notless=8814;e.notlessnorequal=8816;e.notparallel=8742;e.notprecedes=8832;e.notsubset=8836;e.notsucceeds=8833;e.notsuperset=8837;e.nowarmenian=1398;e.nparen=9385;e.nssquare=13233;e.nsuperior=8319;e.ntilde=241;e.nu=957;e.nuhiragana=12396;e.nukatakana=12492;e.nukatakanahalfwidth=65415;e.nuktabengali=2492;e.nuktadeva=2364;e.nuktagujarati=2748;e.nuktagurmukhi=2620;e.numbersign=35;e.numbersignmonospace=65283;e.numbersignsmall=65119;e.numeralsigngreek=884;e.numeralsignlowergreek=885;e.numero=8470;e.nun=1504;e.nundagesh=64320;e.nundageshhebrew=64320;e.nunhebrew=1504;e.nvsquare=13237;e.nwsquare=13243;e.nyabengali=2462;e.nyadeva=2334;e.nyagujarati=2718;e.nyagurmukhi=2590;e.o=111;e.oacute=243;e.oangthai=3629;e.obarred=629;e.obarredcyrillic=1257;e.obarreddieresiscyrillic=1259;e.obengali=2451;e.obopomofo=12571;e.obreve=335;e.ocandradeva=2321;e.ocandragujarati=2705;e.ocandravowelsigndeva=2377;e.ocandravowelsigngujarati=2761;e.ocaron=466;e.ocircle=9438;e.ocircumflex=244;e.ocircumflexacute=7889;e.ocircumflexdotbelow=7897;e.ocircumflexgrave=7891;e.ocircumflexhookabove=7893;e.ocircumflextilde=7895;e.ocyrillic=1086;e.odblacute=337;e.odblgrave=525;e.odeva=2323;e.odieresis=246;e.odieresiscyrillic=1255;e.odotbelow=7885;e.oe=339;e.oekorean=12634;e.ogonek=731;e.ogonekcmb=808;e.ograve=242;e.ogujarati=2707;e.oharmenian=1413;e.ohiragana=12362;e.ohookabove=7887;e.ohorn=417;e.ohornacute=7899;e.ohorndotbelow=7907;e.ohorngrave=7901;e.ohornhookabove=7903;e.ohorntilde=7905;e.ohungarumlaut=337;e.oi=419;e.oinvertedbreve=527;e.okatakana=12458;e.okatakanahalfwidth=65397;e.okorean=12631;e.olehebrew=1451;e.omacron=333;e.omacronacute=7763;e.omacrongrave=7761;e.omdeva=2384;e.omega=969;e.omega1=982;e.omegacyrillic=1121;e.omegalatinclosed=631;e.omegaroundcyrillic=1147;e.omegatitlocyrillic=1149;e.omegatonos=974;e.omgujarati=2768;e.omicron=959;e.omicrontonos=972;e.omonospace=65359;e.one=49;e.onearabic=1633;e.onebengali=2535;e.onecircle=9312;e.onecircleinversesansserif=10122;e.onedeva=2407;e.onedotenleader=8228;e.oneeighth=8539;e.onefitted=63196;e.onegujarati=2791;e.onegurmukhi=2663;e.onehackarabic=1633;e.onehalf=189;e.onehangzhou=12321;e.oneideographicparen=12832;e.oneinferior=8321;e.onemonospace=65297;e.onenumeratorbengali=2548;e.oneoldstyle=63281;e.oneparen=9332;e.oneperiod=9352;e.onepersian=1777;e.onequarter=188;e.oneroman=8560;e.onesuperior=185;e.onethai=3665;e.onethird=8531;e.oogonek=491;e.oogonekmacron=493;e.oogurmukhi=2579;e.oomatragurmukhi=2635;e.oopen=596;e.oparen=9386;e.openbullet=9702;e.option=8997;e.ordfeminine=170;e.ordmasculine=186;e.orthogonal=8735;e.oshortdeva=2322;e.oshortvowelsigndeva=2378;e.oslash=248;e.oslashacute=511;e.osmallhiragana=12361;e.osmallkatakana=12457;e.osmallkatakanahalfwidth=65387;e.ostrokeacute=511;e.osuperior=63216;e.otcyrillic=1151;e.otilde=245;e.otildeacute=7757;e.otildedieresis=7759;e.oubopomofo=12577;e.overline=8254;e.overlinecenterline=65098;e.overlinecmb=773;e.overlinedashed=65097;e.overlinedblwavy=65100;e.overlinewavy=65099;e.overscore=175;e.ovowelsignbengali=2507;e.ovowelsigndeva=2379;e.ovowelsigngujarati=2763;e.p=112;e.paampssquare=13184;e.paasentosquare=13099;e.pabengali=2474;e.pacute=7765;e.padeva=2346;e.pagedown=8671;e.pageup=8670;e.pagujarati=2730;e.pagurmukhi=2602;e.pahiragana=12401;e.paiyannoithai=3631;e.pakatakana=12497;e.palatalizationcyrilliccmb=1156;e.palochkacyrillic=1216;e.pansioskorean=12671;e.paragraph=182;e.parallel=8741;e.parenleft=40;e.parenleftaltonearabic=64830;e.parenleftbt=63725;e.parenleftex=63724;e.parenleftinferior=8333;e.parenleftmonospace=65288;e.parenleftsmall=65113;e.parenleftsuperior=8317;e.parenlefttp=63723;e.parenleftvertical=65077;e.parenright=41;e.parenrightaltonearabic=64831;e.parenrightbt=63736;e.parenrightex=63735;e.parenrightinferior=8334;e.parenrightmonospace=65289;e.parenrightsmall=65114;e.parenrightsuperior=8318;e.parenrighttp=63734;e.parenrightvertical=65078;e.partialdiff=8706;e.paseqhebrew=1472;e.pashtahebrew=1433;e.pasquare=13225;e.patah=1463;e.patah11=1463;e.patah1d=1463;e.patah2a=1463;e.patahhebrew=1463;e.patahnarrowhebrew=1463;e.patahquarterhebrew=1463;e.patahwidehebrew=1463;e.pazerhebrew=1441;e.pbopomofo=12550;e.pcircle=9439;e.pdotaccent=7767;e.pe=1508;e.pecyrillic=1087;e.pedagesh=64324;e.pedageshhebrew=64324;e.peezisquare=13115;e.pefinaldageshhebrew=64323;e.peharabic=1662;e.peharmenian=1402;e.pehebrew=1508;e.pehfinalarabic=64343;e.pehinitialarabic=64344;e.pehiragana=12410;e.pehmedialarabic=64345;e.pekatakana=12506;e.pemiddlehookcyrillic=1191;e.perafehebrew=64334;e.percent=37;e.percentarabic=1642;e.percentmonospace=65285;e.percentsmall=65130;e.period=46;e.periodarmenian=1417;e.periodcentered=183;e.periodhalfwidth=65377;e.periodinferior=63207;e.periodmonospace=65294;e.periodsmall=65106;e.periodsuperior=63208;e.perispomenigreekcmb=834;e.perpendicular=8869;e.perthousand=8240;e.peseta=8359;e.pfsquare=13194;e.phabengali=2475;e.phadeva=2347;e.phagujarati=2731;e.phagurmukhi=2603;e.phi=966;e.phi1=981;e.phieuphacirclekorean=12922;e.phieuphaparenkorean=12826;e.phieuphcirclekorean=12908;e.phieuphkorean=12621;e.phieuphparenkorean=12812;e.philatin=632;e.phinthuthai=3642;e.phisymbolgreek=981;e.phook=421;e.phophanthai=3614;e.phophungthai=3612;e.phosamphaothai=3616;e.pi=960;e.pieupacirclekorean=12915;e.pieupaparenkorean=12819;e.pieupcieuckorean=12662;e.pieupcirclekorean=12901;e.pieupkiyeokkorean=12658;e.pieupkorean=12610;e.pieupparenkorean=12805;e.pieupsioskiyeokkorean=12660;e.pieupsioskorean=12612;e.pieupsiostikeutkorean=12661;e.pieupthieuthkorean=12663;e.pieuptikeutkorean=12659;e.pihiragana=12404;e.pikatakana=12500;e.pisymbolgreek=982;e.piwrarmenian=1411;e.planckover2pi=8463;e.planckover2pi1=8463;e.plus=43;e.plusbelowcmb=799;e.pluscircle=8853;e.plusminus=177;e.plusmod=726;e.plusmonospace=65291;e.plussmall=65122;e.plussuperior=8314;e.pmonospace=65360;e.pmsquare=13272;e.pohiragana=12413;e.pointingindexdownwhite=9759;e.pointingindexleftwhite=9756;e.pointingindexrightwhite=9758;e.pointingindexupwhite=9757;e.pokatakana=12509;e.poplathai=3611;e.postalmark=12306;e.postalmarkface=12320;e.pparen=9387;e.precedes=8826;e.prescription=8478;e.primemod=697;e.primereversed=8245;e.product=8719;e.projective=8965;e.prolongedkana=12540;e.propellor=8984;e.propersubset=8834;e.propersuperset=8835;e.proportion=8759;e.proportional=8733;e.psi=968;e.psicyrillic=1137;e.psilipneumatacyrilliccmb=1158;e.pssquare=13232;e.puhiragana=12407;e.pukatakana=12503;e.pvsquare=13236;e.pwsquare=13242;e.q=113;e.qadeva=2392;e.qadmahebrew=1448;e.qafarabic=1602;e.qaffinalarabic=65238;e.qafinitialarabic=65239;e.qafmedialarabic=65240;e.qamats=1464;e.qamats10=1464;e.qamats1a=1464;e.qamats1c=1464;e.qamats27=1464;e.qamats29=1464;e.qamats33=1464;e.qamatsde=1464;e.qamatshebrew=1464;e.qamatsnarrowhebrew=1464;e.qamatsqatanhebrew=1464;e.qamatsqatannarrowhebrew=1464;e.qamatsqatanquarterhebrew=1464;e.qamatsqatanwidehebrew=1464;e.qamatsquarterhebrew=1464;e.qamatswidehebrew=1464;e.qarneyparahebrew=1439;e.qbopomofo=12561;e.qcircle=9440;e.qhook=672;e.qmonospace=65361;e.qof=1511;e.qofdagesh=64327;e.qofdageshhebrew=64327;e.qofhebrew=1511;e.qparen=9388;e.quarternote=9833;e.qubuts=1467;e.qubuts18=1467;e.qubuts25=1467;e.qubuts31=1467;e.qubutshebrew=1467;e.qubutsnarrowhebrew=1467;e.qubutsquarterhebrew=1467;e.qubutswidehebrew=1467;e.question=63;e.questionarabic=1567;e.questionarmenian=1374;e.questiondown=191;e.questiondownsmall=63423;e.questiongreek=894;e.questionmonospace=65311;e.questionsmall=63295;e.quotedbl=34;e.quotedblbase=8222;e.quotedblleft=8220;e.quotedblmonospace=65282;e.quotedblprime=12318;e.quotedblprimereversed=12317;e.quotedblright=8221;e.quoteleft=8216;e.quoteleftreversed=8219;e.quotereversed=8219;e.quoteright=8217;e.quoterightn=329;e.quotesinglbase=8218;e.quotesingle=39;e.quotesinglemonospace=65287;e.r=114;e.raarmenian=1404;e.rabengali=2480;e.racute=341;e.radeva=2352;e.radical=8730;e.radicalex=63717;e.radoverssquare=13230;e.radoverssquaredsquare=13231;e.radsquare=13229;e.rafe=1471;e.rafehebrew=1471;e.ragujarati=2736;e.ragurmukhi=2608;e.rahiragana=12425;e.rakatakana=12521;e.rakatakanahalfwidth=65431;e.ralowerdiagonalbengali=2545;e.ramiddlediagonalbengali=2544;e.ramshorn=612;e.ratio=8758;e.rbopomofo=12566;e.rcaron=345;e.rcedilla=343;e.rcircle=9441;e.rcommaaccent=343;e.rdblgrave=529;e.rdotaccent=7769;e.rdotbelow=7771;e.rdotbelowmacron=7773;e.referencemark=8251;e.reflexsubset=8838;e.reflexsuperset=8839;e.registered=174;e.registersans=63720;e.registerserif=63194;e.reharabic=1585;e.reharmenian=1408;e.rehfinalarabic=65198;e.rehiragana=12428;e.rekatakana=12524;e.rekatakanahalfwidth=65434;e.resh=1512;e.reshdageshhebrew=64328;e.reshhebrew=1512;e.reversedtilde=8765;e.reviahebrew=1431;e.reviamugrashhebrew=1431;e.revlogicalnot=8976;e.rfishhook=638;e.rfishhookreversed=639;e.rhabengali=2525;e.rhadeva=2397;e.rho=961;e.rhook=637;e.rhookturned=635;e.rhookturnedsuperior=693;e.rhosymbolgreek=1009;e.rhotichookmod=734;e.rieulacirclekorean=12913;e.rieulaparenkorean=12817;e.rieulcirclekorean=12899;e.rieulhieuhkorean=12608;e.rieulkiyeokkorean=12602;e.rieulkiyeoksioskorean=12649;e.rieulkorean=12601;e.rieulmieumkorean=12603;e.rieulpansioskorean=12652;e.rieulparenkorean=12803;e.rieulphieuphkorean=12607;e.rieulpieupkorean=12604;e.rieulpieupsioskorean=12651;e.rieulsioskorean=12605;e.rieulthieuthkorean=12606;e.rieultikeutkorean=12650;e.rieulyeorinhieuhkorean=12653;e.rightangle=8735;e.righttackbelowcmb=793;e.righttriangle=8895;e.rihiragana=12426;e.rikatakana=12522;e.rikatakanahalfwidth=65432;e.ring=730;e.ringbelowcmb=805;e.ringcmb=778;e.ringhalfleft=703;e.ringhalfleftarmenian=1369;e.ringhalfleftbelowcmb=796;e.ringhalfleftcentered=723;e.ringhalfright=702;e.ringhalfrightbelowcmb=825;e.ringhalfrightcentered=722;e.rinvertedbreve=531;e.rittorusquare=13137;e.rlinebelow=7775;e.rlongleg=636;e.rlonglegturned=634;e.rmonospace=65362;e.rohiragana=12429;e.rokatakana=12525;e.rokatakanahalfwidth=65435;e.roruathai=3619;e.rparen=9389;e.rrabengali=2524;e.rradeva=2353;e.rragurmukhi=2652;e.rreharabic=1681;e.rrehfinalarabic=64397;e.rrvocalicbengali=2528;e.rrvocalicdeva=2400;e.rrvocalicgujarati=2784;e.rrvocalicvowelsignbengali=2500;e.rrvocalicvowelsigndeva=2372;e.rrvocalicvowelsigngujarati=2756;e.rsuperior=63217;e.rtblock=9616;e.rturned=633;e.rturnedsuperior=692;e.ruhiragana=12427;e.rukatakana=12523;e.rukatakanahalfwidth=65433;e.rupeemarkbengali=2546;e.rupeesignbengali=2547;e.rupiah=63197;e.ruthai=3620;e.rvocalicbengali=2443;e.rvocalicdeva=2315;e.rvocalicgujarati=2699;e.rvocalicvowelsignbengali=2499;e.rvocalicvowelsigndeva=2371;e.rvocalicvowelsigngujarati=2755;e.s=115;e.sabengali=2488;e.sacute=347;e.sacutedotaccent=7781;e.sadarabic=1589;e.sadeva=2360;e.sadfinalarabic=65210;e.sadinitialarabic=65211;e.sadmedialarabic=65212;e.sagujarati=2744;e.sagurmukhi=2616;e.sahiragana=12373;e.sakatakana=12469;e.sakatakanahalfwidth=65403;e.sallallahoualayhewasallamarabic=65018;e.samekh=1505;e.samekhdagesh=64321;e.samekhdageshhebrew=64321;e.samekhhebrew=1505;e.saraaathai=3634;e.saraaethai=3649;e.saraaimaimalaithai=3652;e.saraaimaimuanthai=3651;e.saraamthai=3635;e.saraathai=3632;e.saraethai=3648;e.saraiileftthai=63622;e.saraiithai=3637;e.saraileftthai=63621;e.saraithai=3636;e.saraothai=3650;e.saraueeleftthai=63624;e.saraueethai=3639;e.saraueleftthai=63623;e.sarauethai=3638;e.sarauthai=3640;e.sarauuthai=3641;e.sbopomofo=12569;e.scaron=353;e.scarondotaccent=7783;e.scedilla=351;e.schwa=601;e.schwacyrillic=1241;e.schwadieresiscyrillic=1243;e.schwahook=602;e.scircle=9442;e.scircumflex=349;e.scommaaccent=537;e.sdotaccent=7777;e.sdotbelow=7779;e.sdotbelowdotaccent=7785;e.seagullbelowcmb=828;e.second=8243;e.secondtonechinese=714;e.section=167;e.seenarabic=1587;e.seenfinalarabic=65202;e.seeninitialarabic=65203;e.seenmedialarabic=65204;e.segol=1462;e.segol13=1462;e.segol1f=1462;e.segol2c=1462;e.segolhebrew=1462;e.segolnarrowhebrew=1462;e.segolquarterhebrew=1462;e.segoltahebrew=1426;e.segolwidehebrew=1462;e.seharmenian=1405;e.sehiragana=12379;e.sekatakana=12475;e.sekatakanahalfwidth=65406;e.semicolon=59;e.semicolonarabic=1563;e.semicolonmonospace=65307;e.semicolonsmall=65108;e.semivoicedmarkkana=12444;e.semivoicedmarkkanahalfwidth=65439;e.sentisquare=13090;e.sentosquare=13091;e.seven=55;e.sevenarabic=1639;e.sevenbengali=2541;e.sevencircle=9318;e.sevencircleinversesansserif=10128;e.sevendeva=2413;e.seveneighths=8542;e.sevengujarati=2797;e.sevengurmukhi=2669;e.sevenhackarabic=1639;e.sevenhangzhou=12327;e.sevenideographicparen=12838;e.seveninferior=8327;e.sevenmonospace=65303;e.sevenoldstyle=63287;e.sevenparen=9338;e.sevenperiod=9358;e.sevenpersian=1783;e.sevenroman=8566;e.sevensuperior=8311;e.seventeencircle=9328;e.seventeenparen=9348;e.seventeenperiod=9368;e.seventhai=3671;e.sfthyphen=173;e.shaarmenian=1399;e.shabengali=2486;e.shacyrillic=1096;e.shaddaarabic=1617;e.shaddadammaarabic=64609;e.shaddadammatanarabic=64606;e.shaddafathaarabic=64608;e.shaddakasraarabic=64610;e.shaddakasratanarabic=64607;e.shade=9618;e.shadedark=9619;e.shadelight=9617;e.shademedium=9618;e.shadeva=2358;e.shagujarati=2742;e.shagurmukhi=2614;e.shalshelethebrew=1427;e.shbopomofo=12565;e.shchacyrillic=1097;e.sheenarabic=1588;e.sheenfinalarabic=65206;e.sheeninitialarabic=65207;e.sheenmedialarabic=65208;e.sheicoptic=995;e.sheqel=8362;e.sheqelhebrew=8362;e.sheva=1456;e.sheva115=1456;e.sheva15=1456;e.sheva22=1456;e.sheva2e=1456;e.shevahebrew=1456;e.shevanarrowhebrew=1456;e.shevaquarterhebrew=1456;e.shevawidehebrew=1456;e.shhacyrillic=1211;e.shimacoptic=1005;e.shin=1513;e.shindagesh=64329;e.shindageshhebrew=64329;e.shindageshshindot=64300;e.shindageshshindothebrew=64300;e.shindageshsindot=64301;e.shindageshsindothebrew=64301;e.shindothebrew=1473;e.shinhebrew=1513;e.shinshindot=64298;e.shinshindothebrew=64298;e.shinsindot=64299;e.shinsindothebrew=64299;e.shook=642;e.sigma=963;e.sigma1=962;e.sigmafinal=962;e.sigmalunatesymbolgreek=1010;e.sihiragana=12375;e.sikatakana=12471;e.sikatakanahalfwidth=65404;e.siluqhebrew=1469;e.siluqlefthebrew=1469;e.similar=8764;e.sindothebrew=1474;e.siosacirclekorean=12916;e.siosaparenkorean=12820;e.sioscieuckorean=12670;e.sioscirclekorean=12902;e.sioskiyeokkorean=12666;e.sioskorean=12613;e.siosnieunkorean=12667;e.siosparenkorean=12806;e.siospieupkorean=12669;e.siostikeutkorean=12668;e.six=54;e.sixarabic=1638;e.sixbengali=2540;e.sixcircle=9317;e.sixcircleinversesansserif=10127;e.sixdeva=2412;e.sixgujarati=2796;e.sixgurmukhi=2668;e.sixhackarabic=1638;e.sixhangzhou=12326;e.sixideographicparen=12837;e.sixinferior=8326;e.sixmonospace=65302;e.sixoldstyle=63286;e.sixparen=9337;e.sixperiod=9357;e.sixpersian=1782;e.sixroman=8565;e.sixsuperior=8310;e.sixteencircle=9327;e.sixteencurrencydenominatorbengali=2553;e.sixteenparen=9347;e.sixteenperiod=9367;e.sixthai=3670;e.slash=47;e.slashmonospace=65295;e.slong=383;e.slongdotaccent=7835;e.smileface=9786;e.smonospace=65363;e.sofpasuqhebrew=1475;e.softhyphen=173;e.softsigncyrillic=1100;e.sohiragana=12381;e.sokatakana=12477;e.sokatakanahalfwidth=65407;e.soliduslongoverlaycmb=824;e.solidusshortoverlaycmb=823;e.sorusithai=3625;e.sosalathai=3624;e.sosothai=3595;e.sosuathai=3626;e.space=32;e.spacehackarabic=32;e.spade=9824;e.spadesuitblack=9824;e.spadesuitwhite=9828;e.sparen=9390;e.squarebelowcmb=827;e.squarecc=13252;e.squarecm=13213;e.squarediagonalcrosshatchfill=9641;e.squarehorizontalfill=9636;e.squarekg=13199;e.squarekm=13214;e.squarekmcapital=13262;e.squareln=13265;e.squarelog=13266;e.squaremg=13198;e.squaremil=13269;e.squaremm=13212;e.squaremsquared=13217;e.squareorthogonalcrosshatchfill=9638;e.squareupperlefttolowerrightfill=9639;e.squareupperrighttolowerleftfill=9640;e.squareverticalfill=9637;e.squarewhitewithsmallblack=9635;e.srsquare=13275;e.ssabengali=2487;e.ssadeva=2359;e.ssagujarati=2743;e.ssangcieuckorean=12617;e.ssanghieuhkorean=12677;e.ssangieungkorean=12672;e.ssangkiyeokkorean=12594;e.ssangnieunkorean=12645;e.ssangpieupkorean=12611;e.ssangsioskorean=12614;e.ssangtikeutkorean=12600;e.ssuperior=63218;e.sterling=163;e.sterlingmonospace=65505;e.strokelongoverlaycmb=822;e.strokeshortoverlaycmb=821;e.subset=8834;e.subsetnotequal=8842;e.subsetorequal=8838;e.succeeds=8827;e.suchthat=8715;e.suhiragana=12377;e.sukatakana=12473;e.sukatakanahalfwidth=65405;e.sukunarabic=1618;e.summation=8721;e.sun=9788;e.superset=8835;e.supersetnotequal=8843;e.supersetorequal=8839;e.svsquare=13276;e.syouwaerasquare=13180;e.t=116;e.tabengali=2468;e.tackdown=8868;e.tackleft=8867;e.tadeva=2340;e.tagujarati=2724;e.tagurmukhi=2596;e.taharabic=1591;e.tahfinalarabic=65218;e.tahinitialarabic=65219;e.tahiragana=12383;e.tahmedialarabic=65220;e.taisyouerasquare=13181;e.takatakana=12479;e.takatakanahalfwidth=65408;e.tatweelarabic=1600;e.tau=964;e.tav=1514;e.tavdages=64330;e.tavdagesh=64330;e.tavdageshhebrew=64330;e.tavhebrew=1514;e.tbar=359;e.tbopomofo=12554;e.tcaron=357;e.tccurl=680;e.tcedilla=355;e.tcheharabic=1670;e.tchehfinalarabic=64379;e.tchehinitialarabic=64380;e.tchehmedialarabic=64381;e.tcircle=9443;e.tcircumflexbelow=7793;e.tcommaaccent=355;e.tdieresis=7831;e.tdotaccent=7787;e.tdotbelow=7789;e.tecyrillic=1090;e.tedescendercyrillic=1197;e.teharabic=1578;e.tehfinalarabic=65174;e.tehhahinitialarabic=64674;e.tehhahisolatedarabic=64524;e.tehinitialarabic=65175;e.tehiragana=12390;e.tehjeeminitialarabic=64673;e.tehjeemisolatedarabic=64523;e.tehmarbutaarabic=1577;e.tehmarbutafinalarabic=65172;e.tehmedialarabic=65176;e.tehmeeminitialarabic=64676;e.tehmeemisolatedarabic=64526;e.tehnoonfinalarabic=64627;e.tekatakana=12486;e.tekatakanahalfwidth=65411;e.telephone=8481;e.telephoneblack=9742;e.telishagedolahebrew=1440;e.telishaqetanahebrew=1449;e.tencircle=9321;e.tenideographicparen=12841;e.tenparen=9341;e.tenperiod=9361;e.tenroman=8569;e.tesh=679;e.tet=1496;e.tetdagesh=64312;e.tetdageshhebrew=64312;e.tethebrew=1496;e.tetsecyrillic=1205;e.tevirhebrew=1435;e.tevirlefthebrew=1435;e.thabengali=2469;e.thadeva=2341;e.thagujarati=2725;e.thagurmukhi=2597;e.thalarabic=1584;e.thalfinalarabic=65196;e.thanthakhatlowleftthai=63640;e.thanthakhatlowrightthai=63639;e.thanthakhatthai=3660;e.thanthakhatupperleftthai=63638;e.theharabic=1579;e.thehfinalarabic=65178;e.thehinitialarabic=65179;e.thehmedialarabic=65180;e.thereexists=8707;e.therefore=8756;e.theta=952;e.theta1=977;e.thetasymbolgreek=977;e.thieuthacirclekorean=12921;e.thieuthaparenkorean=12825;e.thieuthcirclekorean=12907;e.thieuthkorean=12620;e.thieuthparenkorean=12811;e.thirteencircle=9324;e.thirteenparen=9344;e.thirteenperiod=9364;e.thonangmonthothai=3601;e.thook=429;e.thophuthaothai=3602;e.thorn=254;e.thothahanthai=3607;e.thothanthai=3600;e.thothongthai=3608;e.thothungthai=3606;e.thousandcyrillic=1154;e.thousandsseparatorarabic=1644;e.thousandsseparatorpersian=1644;e.three=51;e.threearabic=1635;e.threebengali=2537;e.threecircle=9314;e.threecircleinversesansserif=10124;e.threedeva=2409;e.threeeighths=8540;e.threegujarati=2793;e.threegurmukhi=2665;e.threehackarabic=1635;e.threehangzhou=12323;e.threeideographicparen=12834;e.threeinferior=8323;e.threemonospace=65299;e.threenumeratorbengali=2550;e.threeoldstyle=63283;e.threeparen=9334;e.threeperiod=9354;e.threepersian=1779;e.threequarters=190;e.threequartersemdash=63198;e.threeroman=8562;e.threesuperior=179;e.threethai=3667;e.thzsquare=13204;e.tihiragana=12385;e.tikatakana=12481;e.tikatakanahalfwidth=65409;e.tikeutacirclekorean=12912;e.tikeutaparenkorean=12816;e.tikeutcirclekorean=12898;e.tikeutkorean=12599;e.tikeutparenkorean=12802;e.tilde=732;e.tildebelowcmb=816;e.tildecmb=771;e.tildecomb=771;e.tildedoublecmb=864;e.tildeoperator=8764;e.tildeoverlaycmb=820;e.tildeverticalcmb=830;e.timescircle=8855;e.tipehahebrew=1430;e.tipehalefthebrew=1430;e.tippigurmukhi=2672;e.titlocyrilliccmb=1155;e.tiwnarmenian=1407;e.tlinebelow=7791;e.tmonospace=65364;e.toarmenian=1385;e.tohiragana=12392;e.tokatakana=12488;e.tokatakanahalfwidth=65412;e.tonebarextrahighmod=741;e.tonebarextralowmod=745;e.tonebarhighmod=742;e.tonebarlowmod=744;e.tonebarmidmod=743;e.tonefive=445;e.tonesix=389;e.tonetwo=424;e.tonos=900;e.tonsquare=13095;e.topatakthai=3599;e.tortoiseshellbracketleft=12308;e.tortoiseshellbracketleftsmall=65117;e.tortoiseshellbracketleftvertical=65081;e.tortoiseshellbracketright=12309;e.tortoiseshellbracketrightsmall=65118;e.tortoiseshellbracketrightvertical=65082;e.totaothai=3605;e.tpalatalhook=427;e.tparen=9391;e.trademark=8482;e.trademarksans=63722;e.trademarkserif=63195;e.tretroflexhook=648;e.triagdn=9660;e.triaglf=9668;e.triagrt=9658;e.triagup=9650;e.ts=678;e.tsadi=1510;e.tsadidagesh=64326;e.tsadidageshhebrew=64326;e.tsadihebrew=1510;e.tsecyrillic=1094;e.tsere=1461;e.tsere12=1461;e.tsere1e=1461;e.tsere2b=1461;e.tserehebrew=1461;e.tserenarrowhebrew=1461;e.tserequarterhebrew=1461;e.tserewidehebrew=1461;e.tshecyrillic=1115;e.tsuperior=63219;e.ttabengali=2463;e.ttadeva=2335;e.ttagujarati=2719;e.ttagurmukhi=2591;e.tteharabic=1657;e.ttehfinalarabic=64359;e.ttehinitialarabic=64360;e.ttehmedialarabic=64361;e.tthabengali=2464;e.tthadeva=2336;e.tthagujarati=2720;e.tthagurmukhi=2592;e.tturned=647;e.tuhiragana=12388;e.tukatakana=12484;e.tukatakanahalfwidth=65410;e.tusmallhiragana=12387;e.tusmallkatakana=12483;e.tusmallkatakanahalfwidth=65391;e.twelvecircle=9323;e.twelveparen=9343;e.twelveperiod=9363;e.twelveroman=8571;e.twentycircle=9331;e.twentyhangzhou=21316;e.twentyparen=9351;e.twentyperiod=9371;e.two=50;e.twoarabic=1634;e.twobengali=2536;e.twocircle=9313;e.twocircleinversesansserif=10123;e.twodeva=2408;e.twodotenleader=8229;e.twodotleader=8229;e.twodotleadervertical=65072;e.twogujarati=2792;e.twogurmukhi=2664;e.twohackarabic=1634;e.twohangzhou=12322;e.twoideographicparen=12833;e.twoinferior=8322;e.twomonospace=65298;e.twonumeratorbengali=2549;e.twooldstyle=63282;e.twoparen=9333;e.twoperiod=9353;e.twopersian=1778;e.tworoman=8561;e.twostroke=443;e.twosuperior=178;e.twothai=3666;e.twothirds=8532;e.u=117;e.uacute=250;e.ubar=649;e.ubengali=2441;e.ubopomofo=12584;e.ubreve=365;e.ucaron=468;e.ucircle=9444;e.ucircumflex=251;e.ucircumflexbelow=7799;e.ucyrillic=1091;e.udattadeva=2385;e.udblacute=369;e.udblgrave=533;e.udeva=2313;e.udieresis=252;e.udieresisacute=472;e.udieresisbelow=7795;e.udieresiscaron=474;e.udieresiscyrillic=1265;e.udieresisgrave=476;e.udieresismacron=470;e.udotbelow=7909;e.ugrave=249;e.ugujarati=2697;e.ugurmukhi=2569;e.uhiragana=12358;e.uhookabove=7911;e.uhorn=432;e.uhornacute=7913;e.uhorndotbelow=7921;e.uhorngrave=7915;e.uhornhookabove=7917;e.uhorntilde=7919;e.uhungarumlaut=369;e.uhungarumlautcyrillic=1267;e.uinvertedbreve=535;e.ukatakana=12454;e.ukatakanahalfwidth=65395;e.ukcyrillic=1145;e.ukorean=12636;e.umacron=363;e.umacroncyrillic=1263;e.umacrondieresis=7803;e.umatragurmukhi=2625;e.umonospace=65365;e.underscore=95;e.underscoredbl=8215;e.underscoremonospace=65343;e.underscorevertical=65075;e.underscorewavy=65103;e.union=8746;e.universal=8704;e.uogonek=371;e.uparen=9392;e.upblock=9600;e.upperdothebrew=1476;e.upsilon=965;e.upsilondieresis=971;e.upsilondieresistonos=944;e.upsilonlatin=650;e.upsilontonos=973;e.uptackbelowcmb=797;e.uptackmod=724;e.uragurmukhi=2675;e.uring=367;e.ushortcyrillic=1118;e.usmallhiragana=12357;e.usmallkatakana=12453;e.usmallkatakanahalfwidth=65385;e.ustraightcyrillic=1199;e.ustraightstrokecyrillic=1201;e.utilde=361;e.utildeacute=7801;e.utildebelow=7797;e.uubengali=2442;e.uudeva=2314;e.uugujarati=2698;e.uugurmukhi=2570;e.uumatragurmukhi=2626;e.uuvowelsignbengali=2498;e.uuvowelsigndeva=2370;e.uuvowelsigngujarati=2754;e.uvowelsignbengali=2497;e.uvowelsigndeva=2369;e.uvowelsigngujarati=2753;e.v=118;e.vadeva=2357;e.vagujarati=2741;e.vagurmukhi=2613;e.vakatakana=12535;e.vav=1493;e.vavdagesh=64309;e.vavdagesh65=64309;e.vavdageshhebrew=64309;e.vavhebrew=1493;e.vavholam=64331;e.vavholamhebrew=64331;e.vavvavhebrew=1520;e.vavyodhebrew=1521;e.vcircle=9445;e.vdotbelow=7807;e.vecyrillic=1074;e.veharabic=1700;e.vehfinalarabic=64363;e.vehinitialarabic=64364;e.vehmedialarabic=64365;e.vekatakana=12537;e.venus=9792;e.verticalbar=124;e.verticallineabovecmb=781;e.verticallinebelowcmb=809;e.verticallinelowmod=716;e.verticallinemod=712;e.vewarmenian=1406;e.vhook=651;e.vikatakana=12536;e.viramabengali=2509;e.viramadeva=2381;e.viramagujarati=2765;e.visargabengali=2435;e.visargadeva=2307;e.visargagujarati=2691;e.vmonospace=65366;e.voarmenian=1400;e.voicediterationhiragana=12446;e.voicediterationkatakana=12542;e.voicedmarkkana=12443;e.voicedmarkkanahalfwidth=65438;e.vokatakana=12538;e.vparen=9393;e.vtilde=7805;e.vturned=652;e.vuhiragana=12436;e.vukatakana=12532;e.w=119;e.wacute=7811;e.waekorean=12633;e.wahiragana=12431;e.wakatakana=12527;e.wakatakanahalfwidth=65436;e.wakorean=12632;e.wasmallhiragana=12430;e.wasmallkatakana=12526;e.wattosquare=13143;e.wavedash=12316;e.wavyunderscorevertical=65076;e.wawarabic=1608;e.wawfinalarabic=65262;e.wawhamzaabovearabic=1572;e.wawhamzaabovefinalarabic=65158;e.wbsquare=13277;e.wcircle=9446;e.wcircumflex=373;e.wdieresis=7813;e.wdotaccent=7815;e.wdotbelow=7817;e.wehiragana=12433;e.weierstrass=8472;e.wekatakana=12529;e.wekorean=12638;e.weokorean=12637;e.wgrave=7809;e.whitebullet=9702;e.whitecircle=9675;e.whitecircleinverse=9689;e.whitecornerbracketleft=12302;e.whitecornerbracketleftvertical=65091;e.whitecornerbracketright=12303;e.whitecornerbracketrightvertical=65092;e.whitediamond=9671;e.whitediamondcontainingblacksmalldiamond=9672;e.whitedownpointingsmalltriangle=9663;e.whitedownpointingtriangle=9661;e.whiteleftpointingsmalltriangle=9667;e.whiteleftpointingtriangle=9665;e.whitelenticularbracketleft=12310;e.whitelenticularbracketright=12311;e.whiterightpointingsmalltriangle=9657;e.whiterightpointingtriangle=9655;e.whitesmallsquare=9643;e.whitesmilingface=9786;e.whitesquare=9633;e.whitestar=9734;e.whitetelephone=9743;e.whitetortoiseshellbracketleft=12312;e.whitetortoiseshellbracketright=12313;e.whiteuppointingsmalltriangle=9653;e.whiteuppointingtriangle=9651;e.wihiragana=12432;e.wikatakana=12528;e.wikorean=12639;e.wmonospace=65367;e.wohiragana=12434;e.wokatakana=12530;e.wokatakanahalfwidth=65382;e.won=8361;e.wonmonospace=65510;e.wowaenthai=3623;e.wparen=9394;e.wring=7832;e.wsuperior=695;e.wturned=653;e.wynn=447;e.x=120;e.xabovecmb=829;e.xbopomofo=12562;e.xcircle=9447;e.xdieresis=7821;e.xdotaccent=7819;e.xeharmenian=1389;e.xi=958;e.xmonospace=65368;e.xparen=9395;e.xsuperior=739;e.y=121;e.yaadosquare=13134;e.yabengali=2479;e.yacute=253;e.yadeva=2351;e.yaekorean=12626;e.yagujarati=2735;e.yagurmukhi=2607;e.yahiragana=12420;e.yakatakana=12516;e.yakatakanahalfwidth=65428;e.yakorean=12625;e.yamakkanthai=3662;e.yasmallhiragana=12419;e.yasmallkatakana=12515;e.yasmallkatakanahalfwidth=65388;e.yatcyrillic=1123;e.ycircle=9448;e.ycircumflex=375;e.ydieresis=255;e.ydotaccent=7823;e.ydotbelow=7925;e.yeharabic=1610;e.yehbarreearabic=1746;e.yehbarreefinalarabic=64431;e.yehfinalarabic=65266;e.yehhamzaabovearabic=1574;e.yehhamzaabovefinalarabic=65162;e.yehhamzaaboveinitialarabic=65163;e.yehhamzaabovemedialarabic=65164;e.yehinitialarabic=65267;e.yehmedialarabic=65268;e.yehmeeminitialarabic=64733;e.yehmeemisolatedarabic=64600;e.yehnoonfinalarabic=64660;e.yehthreedotsbelowarabic=1745;e.yekorean=12630;e.yen=165;e.yenmonospace=65509;e.yeokorean=12629;e.yeorinhieuhkorean=12678;e.yerahbenyomohebrew=1450;e.yerahbenyomolefthebrew=1450;e.yericyrillic=1099;e.yerudieresiscyrillic=1273;e.yesieungkorean=12673;e.yesieungpansioskorean=12675;e.yesieungsioskorean=12674;e.yetivhebrew=1434;e.ygrave=7923;e.yhook=436;e.yhookabove=7927;e.yiarmenian=1397;e.yicyrillic=1111;e.yikorean=12642;e.yinyang=9775;e.yiwnarmenian=1410;e.ymonospace=65369;e.yod=1497;e.yoddagesh=64313;e.yoddageshhebrew=64313;e.yodhebrew=1497;e.yodyodhebrew=1522;e.yodyodpatahhebrew=64287;e.yohiragana=12424;e.yoikorean=12681;e.yokatakana=12520;e.yokatakanahalfwidth=65430;e.yokorean=12635;e.yosmallhiragana=12423;e.yosmallkatakana=12519;e.yosmallkatakanahalfwidth=65390;e.yotgreek=1011;e.yoyaekorean=12680;e.yoyakorean=12679;e.yoyakthai=3618;e.yoyingthai=3597;e.yparen=9396;e.ypogegrammeni=890;e.ypogegrammenigreekcmb=837;e.yr=422;e.yring=7833;e.ysuperior=696;e.ytilde=7929;e.yturned=654;e.yuhiragana=12422;e.yuikorean=12684;e.yukatakana=12518;e.yukatakanahalfwidth=65429;e.yukorean=12640;e.yusbigcyrillic=1131;e.yusbigiotifiedcyrillic=1133;e.yuslittlecyrillic=1127;e.yuslittleiotifiedcyrillic=1129;e.yusmallhiragana=12421;e.yusmallkatakana=12517;e.yusmallkatakanahalfwidth=65389;e.yuyekorean=12683;e.yuyeokorean=12682;e.yyabengali=2527;e.yyadeva=2399;e.z=122;e.zaarmenian=1382;e.zacute=378;e.zadeva=2395;e.zagurmukhi=2651;e.zaharabic=1592;e.zahfinalarabic=65222;e.zahinitialarabic=65223;e.zahiragana=12374;e.zahmedialarabic=65224;e.zainarabic=1586;e.zainfinalarabic=65200;e.zakatakana=12470;e.zaqefgadolhebrew=1429;e.zaqefqatanhebrew=1428;e.zarqahebrew=1432;e.zayin=1494;e.zayindagesh=64310;e.zayindageshhebrew=64310;e.zayinhebrew=1494;e.zbopomofo=12567;e.zcaron=382;e.zcircle=9449;e.zcircumflex=7825;e.zcurl=657;e.zdot=380;e.zdotaccent=380;e.zdotbelow=7827;e.zecyrillic=1079;e.zedescendercyrillic=1177;e.zedieresiscyrillic=1247;e.zehiragana=12380;e.zekatakana=12476;e.zero=48;e.zeroarabic=1632;e.zerobengali=2534;e.zerodeva=2406;e.zerogujarati=2790;e.zerogurmukhi=2662;e.zerohackarabic=1632;e.zeroinferior=8320;e.zeromonospace=65296;e.zerooldstyle=63280;e.zeropersian=1776;e.zerosuperior=8304;e.zerothai=3664;e.zerowidthjoiner=65279;e.zerowidthnonjoiner=8204;e.zerowidthspace=8203;e.zeta=950;e.zhbopomofo=12563;e.zhearmenian=1386;e.zhebrevecyrillic=1218;e.zhecyrillic=1078;e.zhedescendercyrillic=1175;e.zhedieresiscyrillic=1245;e.zihiragana=12376;e.zikatakana=12472;e.zinorhebrew=1454;e.zlinebelow=7829;e.zmonospace=65370;e.zohiragana=12382;e.zokatakana=12478;e.zparen=9397;e.zretroflexhook=656;e.zstroke=438;e.zuhiragana=12378;e.zukatakana=12474;e[".notdef"]=0;e.angbracketleftbig=9001;e.angbracketleftBig=9001;e.angbracketleftbigg=9001;e.angbracketleftBigg=9001;e.angbracketrightBig=9002;e.angbracketrightbig=9002;e.angbracketrightBigg=9002;e.angbracketrightbigg=9002;e.arrowhookleft=8618;e.arrowhookright=8617;e.arrowlefttophalf=8636;e.arrowleftbothalf=8637;e.arrownortheast=8599;e.arrownorthwest=8598;e.arrowrighttophalf=8640;e.arrowrightbothalf=8641;e.arrowsoutheast=8600;e.arrowsouthwest=8601;e.backslashbig=8726;e.backslashBig=8726;e.backslashBigg=8726;e.backslashbigg=8726;e.bardbl=8214;e.bracehtipdownleft=65079;e.bracehtipdownright=65079;e.bracehtipupleft=65080;e.bracehtipupright=65080;e.braceleftBig=123;e.braceleftbig=123;e.braceleftbigg=123;e.braceleftBigg=123;e.bracerightBig=125;e.bracerightbig=125;e.bracerightbigg=125;e.bracerightBigg=125;e.bracketleftbig=91;e.bracketleftBig=91;e.bracketleftbigg=91;e.bracketleftBigg=91;e.bracketrightBig=93;e.bracketrightbig=93;e.bracketrightbigg=93;e.bracketrightBigg=93;e.ceilingleftbig=8968;e.ceilingleftBig=8968;e.ceilingleftBigg=8968;e.ceilingleftbigg=8968;e.ceilingrightbig=8969;e.ceilingrightBig=8969;e.ceilingrightbigg=8969;e.ceilingrightBigg=8969;e.circledotdisplay=8857;e.circledottext=8857;e.circlemultiplydisplay=8855;e.circlemultiplytext=8855;e.circleplusdisplay=8853;e.circleplustext=8853;e.contintegraldisplay=8750;e.contintegraltext=8750;e.coproductdisplay=8720;e.coproducttext=8720;e.floorleftBig=8970;e.floorleftbig=8970;e.floorleftbigg=8970;e.floorleftBigg=8970;e.floorrightbig=8971;e.floorrightBig=8971;e.floorrightBigg=8971;e.floorrightbigg=8971;e.hatwide=770;e.hatwider=770;e.hatwidest=770;e.intercal=7488;e.integraldisplay=8747;e.integraltext=8747;e.intersectiondisplay=8898;e.intersectiontext=8898;e.logicalanddisplay=8743;e.logicalandtext=8743;e.logicalordisplay=8744;e.logicalortext=8744;e.parenleftBig=40;e.parenleftbig=40;e.parenleftBigg=40;e.parenleftbigg=40;e.parenrightBig=41;e.parenrightbig=41;e.parenrightBigg=41;e.parenrightbigg=41;e.prime=8242;e.productdisplay=8719;e.producttext=8719;e.radicalbig=8730;e.radicalBig=8730;e.radicalBigg=8730;e.radicalbigg=8730;e.radicalbt=8730;e.radicaltp=8730;e.radicalvertex=8730;e.slashbig=47;e.slashBig=47;e.slashBigg=47;e.slashbigg=47;e.summationdisplay=8721;e.summationtext=8721;e.tildewide=732;e.tildewider=732;e.tildewidest=732;e.uniondisplay=8899;e.unionmultidisplay=8846;e.unionmultitext=8846;e.unionsqdisplay=8852;e.unionsqtext=8852;e.uniontext=8899;e.vextenddouble=8741;e.vextendsingle=8739})),Tr=getLookupTableFactory((function(e){e.space=32;e.a1=9985;e.a2=9986;e.a202=9987;e.a3=9988;e.a4=9742;e.a5=9990;e.a119=9991;e.a118=9992;e.a117=9993;e.a11=9755;e.a12=9758;e.a13=9996;e.a14=9997;e.a15=9998;e.a16=9999;e.a105=1e4;e.a17=10001;e.a18=10002;e.a19=10003;e.a20=10004;e.a21=10005;e.a22=10006;e.a23=10007;e.a24=10008;e.a25=10009;e.a26=10010;e.a27=10011;e.a28=10012;e.a6=10013;e.a7=10014;e.a8=10015;e.a9=10016;e.a10=10017;e.a29=10018;e.a30=10019;e.a31=10020;e.a32=10021;e.a33=10022;e.a34=10023;e.a35=9733;e.a36=10025;e.a37=10026;e.a38=10027;e.a39=10028;e.a40=10029;e.a41=10030;e.a42=10031;e.a43=10032;e.a44=10033;e.a45=10034;e.a46=10035;e.a47=10036;e.a48=10037;e.a49=10038;e.a50=10039;e.a51=10040;e.a52=10041;e.a53=10042;e.a54=10043;e.a55=10044;e.a56=10045;e.a57=10046;e.a58=10047;e.a59=10048;e.a60=10049;e.a61=10050;e.a62=10051;e.a63=10052;e.a64=10053;e.a65=10054;e.a66=10055;e.a67=10056;e.a68=10057;e.a69=10058;e.a70=10059;e.a71=9679;e.a72=10061;e.a73=9632;e.a74=10063;e.a203=10064;e.a75=10065;e.a204=10066;e.a76=9650;e.a77=9660;e.a78=9670;e.a79=10070;e.a81=9687;e.a82=10072;e.a83=10073;e.a84=10074;e.a97=10075;e.a98=10076;e.a99=10077;e.a100=10078;e.a101=10081;e.a102=10082;e.a103=10083;e.a104=10084;e.a106=10085;e.a107=10086;e.a108=10087;e.a112=9827;e.a111=9830;e.a110=9829;e.a109=9824;e.a120=9312;e.a121=9313;e.a122=9314;e.a123=9315;e.a124=9316;e.a125=9317;e.a126=9318;e.a127=9319;e.a128=9320;e.a129=9321;e.a130=10102;e.a131=10103;e.a132=10104;e.a133=10105;e.a134=10106;e.a135=10107;e.a136=10108;e.a137=10109;e.a138=10110;e.a139=10111;e.a140=10112;e.a141=10113;e.a142=10114;e.a143=10115;e.a144=10116;e.a145=10117;e.a146=10118;e.a147=10119;e.a148=10120;e.a149=10121;e.a150=10122;e.a151=10123;e.a152=10124;e.a153=10125;e.a154=10126;e.a155=10127;e.a156=10128;e.a157=10129;e.a158=10130;e.a159=10131;e.a160=10132;e.a161=8594;e.a163=8596;e.a164=8597;e.a196=10136;e.a165=10137;e.a192=10138;e.a166=10139;e.a167=10140;e.a168=10141;e.a169=10142;e.a170=10143;e.a171=10144;e.a172=10145;e.a173=10146;e.a162=10147;e.a174=10148;e.a175=10149;e.a176=10150;e.a177=10151;e.a178=10152;e.a179=10153;e.a193=10154;e.a180=10155;e.a199=10156;e.a181=10157;e.a200=10158;e.a182=10159;e.a201=10161;e.a183=10162;e.a184=10163;e.a197=10164;e.a185=10165;e.a194=10166;e.a198=10167;e.a186=10168;e.a195=10169;e.a187=10170;e.a188=10171;e.a189=10172;e.a190=10173;e.a191=10174;e.a89=10088;e.a90=10089;e.a93=10090;e.a94=10091;e.a91=10092;e.a92=10093;e.a205=10094;e.a85=10095;e.a206=10096;e.a86=10097;e.a87=10098;e.a88=10099;e.a95=10100;e.a96=10101;e[".notdef"]=0})),Mr=getLookupTableFactory((function(e){e[63721]=169;e[63193]=169;e[63720]=174;e[63194]=174;e[63722]=8482;e[63195]=8482;e[63729]=9127;e[63730]=9128;e[63731]=9129;e[63740]=9131;e[63741]=9132;e[63742]=9133;e[63726]=9121;e[63727]=9122;e[63728]=9123;e[63737]=9124;e[63738]=9125;e[63739]=9126;e[63723]=9115;e[63724]=9116;e[63725]=9117;e[63734]=9118;e[63735]=9119;e[63736]=9120}));function getUnicodeForGlyph(e,t){let a=t[e];if(void 0!==a)return a;if(!e)return-1;if("u"===e[0]){const t=e.length;let r;if(7===t&&"n"===e[1]&&"i"===e[2])r=e.substring(3);else{if(!(t>=5&&t<=7))return-1;r=e.substring(1)}if(r===r.toUpperCase()){a=parseInt(r,16);if(a>=0)return a}}return-1}const Dr=[[0,127],[128,255],[256,383],[384,591],[592,687,7424,7551,7552,7615],[688,767,42752,42783],[768,879,7616,7679],[880,1023],[11392,11519],[1024,1279,1280,1327,11744,11775,42560,42655],[1328,1423],[1424,1535],[42240,42559],[1536,1791,1872,1919],[1984,2047],[2304,2431],[2432,2559],[2560,2687],[2688,2815],[2816,2943],[2944,3071],[3072,3199],[3200,3327],[3328,3455],[3584,3711],[3712,3839],[4256,4351,11520,11567],[6912,7039],[4352,4607],[7680,7935,11360,11391,42784,43007],[7936,8191],[8192,8303,11776,11903],[8304,8351],[8352,8399],[8400,8447],[8448,8527],[8528,8591],[8592,8703,10224,10239,10496,10623,11008,11263],[8704,8959,10752,11007,10176,10223,10624,10751],[8960,9215],[9216,9279],[9280,9311],[9312,9471],[9472,9599],[9600,9631],[9632,9727],[9728,9983],[9984,10175],[12288,12351],[12352,12447],[12448,12543,12784,12799],[12544,12591,12704,12735],[12592,12687],[43072,43135],[12800,13055],[13056,13311],[44032,55215],[55296,57343],[67840,67871],[19968,40959,11904,12031,12032,12255,12272,12287,13312,19903,131072,173791,12688,12703],[57344,63743],[12736,12783,63744,64255,194560,195103],[64256,64335],[64336,65023],[65056,65071],[65040,65055],[65104,65135],[65136,65279],[65280,65519],[65520,65535],[3840,4095],[1792,1871],[1920,1983],[3456,3583],[4096,4255],[4608,4991,4992,5023,11648,11743],[5024,5119],[5120,5759],[5760,5791],[5792,5887],[6016,6143],[6144,6319],[10240,10495],[40960,42127],[5888,5919,5920,5951,5952,5983,5984,6015],[66304,66351],[66352,66383],[66560,66639],[118784,119039,119040,119295,119296,119375],[119808,120831],[1044480,1048573],[65024,65039,917760,917999],[917504,917631],[6400,6479],[6480,6527],[6528,6623],[6656,6687],[11264,11359],[11568,11647],[19904,19967],[43008,43055],[65536,65663,65664,65791,65792,65855],[65856,65935],[66432,66463],[66464,66527],[66640,66687],[66688,66735],[67584,67647],[68096,68191],[119552,119647],[73728,74751,74752,74879],[119648,119679],[7040,7103],[7168,7247],[7248,7295],[43136,43231],[43264,43311],[43312,43359],[43520,43615],[65936,65999],[66e3,66047],[66208,66271,66176,66207,67872,67903],[127024,127135,126976,127023]];function getUnicodeRangeFor(e,t=-1){if(-1!==t){const a=Dr[t];for(let r=0,i=a.length;r=a[r]&&e<=a[r+1])return t}for(let t=0,a=Dr.length;t=a[r]&&e<=a[r+1])return t}return-1}const Rr=new RegExp("^(\\s)|(\\p{Mn})|(\\p{Cf})$","u"),Nr=new Map;const Er=!0,Pr=1,Lr=2,_r=4,jr=32,Ur=[".notdef",".null","nonmarkingreturn","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","nonbreakingspace","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron","Lslash","lslash","Scaron","scaron","Zcaron","zcaron","brokenbar","Eth","eth","Yacute","yacute","Thorn","thorn","minus","multiply","onesuperior","twosuperior","threesuperior","onehalf","onequarter","threequarters","franc","Gbreve","gbreve","Idotaccent","Scedilla","scedilla","Cacute","cacute","Ccaron","ccaron","dcroat"];function recoverGlyphName(e,t){if(void 0!==t[e])return e;const a=getUnicodeForGlyph(e,t);if(-1!==a)for(const e in t)if(t[e]===a)return e;info("Unable to recover a standard glyph name for: "+e);return e}function type1FontGlyphMapping(e,t,a){const r=Object.create(null);let i,n,s;const o=!!(e.flags&_r);if(e.isInternalFont){s=t;for(n=0;n=0?i:0}}else if(e.baseEncodingName){s=getEncoding(e.baseEncodingName);for(n=0;n=0?i:0}}else if(o)for(n in t)r[n]=t[n];else{s=Cr;for(n=0;n=0?i:0}}const c=e.differences;let l;if(c)for(n in c){const e=c[n];i=a.indexOf(e);if(-1===i){l||(l=Or());const t=recoverGlyphName(e,l);t!==e&&(i=a.indexOf(t))}r[n]=i>=0?i:0}return r}function normalizeFontName(e){return e.replaceAll(/[,_]/g,"-").replaceAll(/\s/g,"")}const Xr=getLookupTableFactory((e=>{e[8211]=65074;e[8212]=65073;e[8229]=65072;e[8230]=65049;e[12289]=65041;e[12290]=65042;e[12296]=65087;e[12297]=65088;e[12298]=65085;e[12299]=65086;e[12300]=65089;e[12301]=65090;e[12302]=65091;e[12303]=65092;e[12304]=65083;e[12305]=65084;e[12308]=65081;e[12309]=65082;e[12310]=65047;e[12311]=65048;e[65103]=65076;e[65281]=65045;e[65288]=65077;e[65289]=65078;e[65292]=65040;e[65306]=65043;e[65307]=65044;e[65311]=65046;e[65339]=65095;e[65341]=65096;e[65343]=65075;e[65371]=65079;e[65373]=65080}));const qr=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron"],Hr=[".notdef","space","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],Wr=[".notdef","space","dollaroldstyle","dollarsuperior","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","hyphensuperior","colonmonetary","onefitted","rupiah","centoldstyle","figuredash","hypheninferior","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior"],zr=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall","001.000","001.001","001.002","001.003","Black","Bold","Book","Light","Medium","Regular","Roman","Semibold"],$r=391,Gr=[null,{id:"hstem",min:2,stackClearing:!0,stem:!0},null,{id:"vstem",min:2,stackClearing:!0,stem:!0},{id:"vmoveto",min:1,stackClearing:!0},{id:"rlineto",min:2,resetStack:!0},{id:"hlineto",min:1,resetStack:!0},{id:"vlineto",min:1,resetStack:!0},{id:"rrcurveto",min:6,resetStack:!0},null,{id:"callsubr",min:1,undefStack:!0},{id:"return",min:0,undefStack:!0},null,null,{id:"endchar",min:0,stackClearing:!0},null,null,null,{id:"hstemhm",min:2,stackClearing:!0,stem:!0},{id:"hintmask",min:0,stackClearing:!0},{id:"cntrmask",min:0,stackClearing:!0},{id:"rmoveto",min:2,stackClearing:!0},{id:"hmoveto",min:1,stackClearing:!0},{id:"vstemhm",min:2,stackClearing:!0,stem:!0},{id:"rcurveline",min:8,resetStack:!0},{id:"rlinecurve",min:8,resetStack:!0},{id:"vvcurveto",min:4,resetStack:!0},{id:"hhcurveto",min:4,resetStack:!0},null,{id:"callgsubr",min:1,undefStack:!0},{id:"vhcurveto",min:4,resetStack:!0},{id:"hvcurveto",min:4,resetStack:!0}],Vr=[null,null,null,{id:"and",min:2,stackDelta:-1},{id:"or",min:2,stackDelta:-1},{id:"not",min:1,stackDelta:0},null,null,null,{id:"abs",min:1,stackDelta:0},{id:"add",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]+e[t-1]}},{id:"sub",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]-e[t-1]}},{id:"div",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]/e[t-1]}},null,{id:"neg",min:1,stackDelta:0,stackFn(e,t){e[t-1]=-e[t-1]}},{id:"eq",min:2,stackDelta:-1},null,null,{id:"drop",min:1,stackDelta:-1},null,{id:"put",min:2,stackDelta:-2},{id:"get",min:1,stackDelta:0},{id:"ifelse",min:4,stackDelta:-3},{id:"random",min:0,stackDelta:1},{id:"mul",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]*e[t-1]}},null,{id:"sqrt",min:1,stackDelta:0},{id:"dup",min:1,stackDelta:1},{id:"exch",min:2,stackDelta:0},{id:"index",min:2,stackDelta:0},{id:"roll",min:3,stackDelta:-2},null,null,null,{id:"hflex",min:7,resetStack:!0},{id:"flex",min:13,resetStack:!0},{id:"hflex1",min:9,resetStack:!0},{id:"flex1",min:11,resetStack:!0}];class CFFParser{constructor(e,t,a){this.bytes=e.getBytes();this.properties=t;this.seacAnalysisEnabled=!!a}parse(){const e=this.properties,t=new CFF;this.cff=t;const a=this.parseHeader(),r=this.parseIndex(a.endPos),i=this.parseIndex(r.endPos),n=this.parseIndex(i.endPos),s=this.parseIndex(n.endPos),o=this.parseDict(i.obj.get(0)),c=this.createDict(CFFTopDict,o,t.strings);t.header=a.obj;t.names=this.parseNameIndex(r.obj);t.strings=this.parseStringIndex(n.obj);t.topDict=c;t.globalSubrIndex=s.obj;this.parsePrivateDict(t.topDict);t.isCIDFont=c.hasName("ROS");const l=c.getByName("CharStrings"),h=this.parseIndex(l).obj,u=c.getByName("FontMatrix");u&&(e.fontMatrix=u);const d=c.getByName("FontBBox");if(d){e.ascent=Math.max(d[3],d[1]);e.descent=Math.min(d[1],d[3]);e.ascentScaled=!0}let f,g;if(t.isCIDFont){const e=this.parseIndex(c.getByName("FDArray")).obj;for(let a=0,r=e.count;a=t)throw new FormatError("Invalid CFF header");if(0!==a){info("cff data is shifted");e=e.subarray(a);this.bytes=e}const r=e[0],i=e[1],n=e[2],s=e[3];return{obj:new CFFHeader(r,i,n,s),endPos:n}}parseDict(e){let t=0;function parseOperand(){let a=e[t++];if(30===a)return function parseFloatOperand(){let a="";const r=15,i=["0","1","2","3","4","5","6","7","8","9",".","E","E-",null,"-"],n=e.length;for(;t>4,o=15&n;if(s===r)break;a+=i[s];if(o===r)break;a+=i[o]}return parseFloat(a)}();if(28===a){a=readInt16(e,t);t+=2;return a}if(29===a){a=e[t++];a=a<<8|e[t++];a=a<<8|e[t++];a=a<<8|e[t++];return a}if(a>=32&&a<=246)return a-139;if(a>=247&&a<=250)return 256*(a-247)+e[t++]+108;if(a>=251&&a<=254)return-256*(a-251)-e[t++]-108;warn('CFFParser_parseDict: "'+a+'" is a reserved command.');return NaN}let a=[];const r=[];t=0;const i=e.length;for(;t10)return!1;let i=e.stackSize;const n=e.stack;let s=t.length;for(let o=0;o=4){i-=4;if(this.seacAnalysisEnabled){e.seac=n.slice(i,i+4);return!1}}l=Gr[c]}else if(c>=32&&c<=246){n[i]=c-139;i++}else if(c>=247&&c<=254){n[i]=c<251?(c-247<<8)+t[o]+108:-(c-251<<8)-t[o]-108;o++;i++}else if(255===c){n[i]=(t[o]<<24|t[o+1]<<16|t[o+2]<<8|t[o+3])/65536;o+=4;i++}else if(19===c||20===c){e.hints+=i>>1;if(0===e.hints){t.copyWithin(o-1,o,-1);o-=1;s-=1;continue}o+=e.hints+7>>3;i%=2;l=Gr[c]}else{if(10===c||29===c){const t=10===c?a:r;if(!t){l=Gr[c];warn("Missing subrsIndex for "+l.id);return!1}let s=32768;t.count<1240?s=107:t.count<33900&&(s=1131);const o=n[--i]+s;if(o<0||o>=t.count||isNaN(o)){l=Gr[c];warn("Out of bounds subrIndex for "+l.id);return!1}e.stackSize=i;e.callDepth++;if(!this.parseCharString(e,t.get(o),a,r))return!1;e.callDepth--;i=e.stackSize;continue}if(11===c){e.stackSize=i;return!0}if(0===c&&o===t.length){t[o-1]=14;l=Gr[14]}else{if(9===c){t.copyWithin(o-1,o,-1);o-=1;s-=1;continue}l=Gr[c]}}if(l){if(l.stem){e.hints+=i>>1;if(3===c||23===c)e.hasVStems=!0;else if(e.hasVStems&&(1===c||18===c)){warn("CFF stem hints are in wrong order");t[o-1]=1===c?3:23}}if("min"in l&&!e.undefStack&&i=2&&l.stem?i%=2:i>1&&warn("Found too many parameters for stack-clearing command");i>0&&(e.width=n[i-1])}if("stackDelta"in l){"stackFn"in l&&l.stackFn(n,i);i+=l.stackDelta}else if(l.stackClearing)i=0;else if(l.resetStack){i=0;e.undefStack=!1}else if(l.undefStack){i=0;e.undefStack=!0;e.firstStackClearing=!1}}}s=i.length){warn("Invalid fd index for glyph index.");u=!1}if(u){f=i[e].privateDict;d=f.subrsIndex}}else t&&(d=t);u&&(u=this.parseCharString(h,c,d,a));if(null!==h.width){const e=f.getByName("nominalWidthX");o[l]=e+h.width}else{const e=f.getByName("defaultWidthX");o[l]=e}null!==h.seac&&(s[l]=h.seac);u||e.set(l,new Uint8Array([14]))}return{charStrings:e,seacs:s,widths:o}}emptyPrivateDictionary(e){const t=this.createDict(CFFPrivateDict,[],e.strings);e.setByKey(18,[0,0]);e.privateDict=t}parsePrivateDict(e){if(!e.hasName("Private")){this.emptyPrivateDictionary(e);return}const t=e.getByName("Private");if(!Array.isArray(t)||2!==t.length){e.removeByName("Private");return}const a=t[0],r=t[1];if(0===a||r>=this.bytes.length){this.emptyPrivateDictionary(e);return}const i=r+a,n=this.bytes.subarray(r,i),s=this.parseDict(n),o=this.createDict(CFFPrivateDict,s,e.strings);e.privateDict=o;0===o.getByName("ExpansionFactor")&&o.setByName("ExpansionFactor",.06);if(!o.getByName("Subrs"))return;const c=o.getByName("Subrs"),l=r+c;if(0===c||l>=this.bytes.length){this.emptyPrivateDictionary(e);return}const h=this.parseIndex(l);o.subrsIndex=h.obj}parseCharsets(e,t,a,r){if(0===e)return new CFFCharset(!0,Yr.ISO_ADOBE,qr);if(1===e)return new CFFCharset(!0,Yr.EXPERT,Hr);if(2===e)return new CFFCharset(!0,Yr.EXPERT_SUBSET,Wr);const i=this.bytes,n=e,s=i[e++],o=[r?0:".notdef"];let c,l,h;t-=1;switch(s){case 0:for(h=0;h=65535){warn("Not enough space in charstrings to duplicate first glyph.");return}const e=this.charStrings.get(0);this.charStrings.add(e);this.isCIDFont&&this.fdSelect.fdSelect.push(this.fdSelect.fdSelect[0])}hasGlyphId(e){if(e<0||e>=this.charStrings.count)return!1;return this.charStrings.get(e).length>0}}class CFFHeader{constructor(e,t,a,r){this.major=e;this.minor=t;this.hdrSize=a;this.offSize=r}}class CFFStrings{constructor(){this.strings=[]}get(e){return e>=0&&e<=390?zr[e]:e-$r<=this.strings.length?this.strings[e-$r]:zr[0]}getSID(e){let t=zr.indexOf(e);if(-1!==t)return t;t=this.strings.indexOf(e);return-1!==t?t+$r:-1}add(e){this.strings.push(e)}get count(){return this.strings.length}}class CFFIndex{constructor(){this.objects=[];this.length=0}add(e){this.length+=e.length;this.objects.push(e)}set(e,t){this.length+=t.length-this.objects[e].length;this.objects[e]=t}get(e){return this.objects[e]}get count(){return this.objects.length}}class CFFDict{constructor(e,t){this.keyToNameMap=e.keyToNameMap;this.nameToKeyMap=e.nameToKeyMap;this.defaults=e.defaults;this.types=e.types;this.opcodes=e.opcodes;this.order=e.order;this.strings=t;this.values=Object.create(null)}setByKey(e,t){if(!(e in this.keyToNameMap))return!1;if(0===t.length)return!0;for(const a of t)if(isNaN(a)){warn(`Invalid CFFDict value: "${t}" for key "${e}".`);return!0}const a=this.types[e];"num"!==a&&"sid"!==a&&"offset"!==a||(t=t[0]);this.values[e]=t;return!0}setByName(e,t){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name "${e}"`);this.values[this.nameToKeyMap[e]]=t}hasName(e){return this.nameToKeyMap[e]in this.values}getByName(e){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name ${e}"`);const t=this.nameToKeyMap[e];return t in this.values?this.values[t]:this.defaults[t]}removeByName(e){delete this.values[this.nameToKeyMap[e]]}static createTables(e){const t={keyToNameMap:{},nameToKeyMap:{},defaults:{},types:{},opcodes:{},order:[]};for(const a of e){const e=Array.isArray(a[0])?(a[0][0]<<8)+a[0][1]:a[0];t.keyToNameMap[e]=a[1];t.nameToKeyMap[a[1]]=e;t.types[e]=a[2];t.defaults[e]=a[3];t.opcodes[e]=Array.isArray(a[0])?a[0]:[a[0]];t.order.push(e)}return t}}const Kr=[[[12,30],"ROS",["sid","sid","num"],null],[[12,20],"SyntheticBase","num",null],[0,"version","sid",null],[1,"Notice","sid",null],[[12,0],"Copyright","sid",null],[2,"FullName","sid",null],[3,"FamilyName","sid",null],[4,"Weight","sid",null],[[12,1],"isFixedPitch","num",0],[[12,2],"ItalicAngle","num",0],[[12,3],"UnderlinePosition","num",-100],[[12,4],"UnderlineThickness","num",50],[[12,5],"PaintType","num",0],[[12,6],"CharstringType","num",2],[[12,7],"FontMatrix",["num","num","num","num","num","num"],[.001,0,0,.001,0,0]],[13,"UniqueID","num",null],[5,"FontBBox",["num","num","num","num"],[0,0,0,0]],[[12,8],"StrokeWidth","num",0],[14,"XUID","array",null],[15,"charset","offset",0],[16,"Encoding","offset",0],[17,"CharStrings","offset",0],[18,"Private",["offset","offset"],null],[[12,21],"PostScript","sid",null],[[12,22],"BaseFontName","sid",null],[[12,23],"BaseFontBlend","delta",null],[[12,31],"CIDFontVersion","num",0],[[12,32],"CIDFontRevision","num",0],[[12,33],"CIDFontType","num",0],[[12,34],"CIDCount","num",8720],[[12,35],"UIDBase","num",null],[[12,37],"FDSelect","offset",null],[[12,36],"FDArray","offset",null],[[12,38],"FontName","sid",null]];class CFFTopDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(Kr))}constructor(e){super(CFFTopDict.tables,e);this.privateDict=null}}const Jr=[[6,"BlueValues","delta",null],[7,"OtherBlues","delta",null],[8,"FamilyBlues","delta",null],[9,"FamilyOtherBlues","delta",null],[[12,9],"BlueScale","num",.039625],[[12,10],"BlueShift","num",7],[[12,11],"BlueFuzz","num",1],[10,"StdHW","num",null],[11,"StdVW","num",null],[[12,12],"StemSnapH","delta",null],[[12,13],"StemSnapV","delta",null],[[12,14],"ForceBold","num",0],[[12,17],"LanguageGroup","num",0],[[12,18],"ExpansionFactor","num",.06],[[12,19],"initialRandomSeed","num",0],[20,"defaultWidthX","num",0],[21,"nominalWidthX","num",0],[19,"Subrs","offset",null]];class CFFPrivateDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(Jr))}constructor(e){super(CFFPrivateDict.tables,e);this.subrsIndex=null}}const Yr={ISO_ADOBE:0,EXPERT:1,EXPERT_SUBSET:2};class CFFCharset{constructor(e,t,a,r){this.predefined=e;this.format=t;this.charset=a;this.raw=r}}class CFFEncoding{constructor(e,t,a,r){this.predefined=e;this.format=t;this.encoding=a;this.raw=r}}class CFFFDSelect{constructor(e,t){this.format=e;this.fdSelect=t}getFDIndex(e){return e<0||e>=this.fdSelect.length?-1:this.fdSelect[e]}}class CFFOffsetTracker{constructor(){this.offsets=Object.create(null)}isTracking(e){return e in this.offsets}track(e,t){if(e in this.offsets)throw new FormatError(`Already tracking location of ${e}`);this.offsets[e]=t}offset(e){for(const t in this.offsets)this.offsets[t]+=e}setEntryLocation(e,t,a){if(!(e in this.offsets))throw new FormatError(`Not tracking location of ${e}`);const r=a.data,i=this.offsets[e];for(let e=0,a=t.length;e>24&255;r[s]=l>>16&255;r[o]=l>>8&255;r[c]=255&l}}}class CFFCompiler{constructor(e){this.cff=e}compile(){const e=this.cff,t={data:[],length:0,add(e){try{this.data.push(...e)}catch{this.data=this.data.concat(e)}this.length=this.data.length}},a=this.compileHeader(e.header);t.add(a);const r=this.compileNameIndex(e.names);t.add(r);if(e.isCIDFont&&e.topDict.hasName("FontMatrix")){const t=e.topDict.getByName("FontMatrix");e.topDict.removeByName("FontMatrix");for(const a of e.fdArray){let e=t.slice(0);a.hasName("FontMatrix")&&(e=Util.transform(e,a.getByName("FontMatrix")));a.setByName("FontMatrix",e)}}const i=e.topDict.getByName("XUID");i?.length>16&&e.topDict.removeByName("XUID");e.topDict.setByName("charset",0);let n=this.compileTopDicts([e.topDict],t.length,e.isCIDFont);t.add(n.output);const s=n.trackers[0],o=this.compileStringIndex(e.strings.strings);t.add(o);const c=this.compileIndex(e.globalSubrIndex);t.add(c);if(e.encoding&&e.topDict.hasName("Encoding"))if(e.encoding.predefined)s.setEntryLocation("Encoding",[e.encoding.format],t);else{const a=this.compileEncoding(e.encoding);s.setEntryLocation("Encoding",[t.length],t);t.add(a)}const l=this.compileCharset(e.charset,e.charStrings.count,e.strings,e.isCIDFont);s.setEntryLocation("charset",[t.length],t);t.add(l);const h=this.compileCharStrings(e.charStrings);s.setEntryLocation("CharStrings",[t.length],t);t.add(h);if(e.isCIDFont){s.setEntryLocation("FDSelect",[t.length],t);const a=this.compileFDSelect(e.fdSelect);t.add(a);n=this.compileTopDicts(e.fdArray,t.length,!0);s.setEntryLocation("FDArray",[t.length],t);t.add(n.output);const r=n.trackers;this.compilePrivateDicts(e.fdArray,r,t)}this.compilePrivateDicts([e.topDict],[s],t);t.add([0]);return t.data}encodeNumber(e){return Number.isInteger(e)?this.encodeInteger(e):this.encodeFloat(e)}static get EncodeFloatRegExp(){return shadow(this,"EncodeFloatRegExp",/\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/)}encodeFloat(e){let t=e.toString();const a=CFFCompiler.EncodeFloatRegExp.exec(t);if(a){const r=parseFloat("1e"+((a[2]?+a[2]:0)+a[1].length));t=(Math.round(e*r)/r).toString()}let r,i,n="";for(r=0,i=t.length;r=-107&&e<=107?[e+139]:e>=108&&e<=1131?[247+((e-=108)>>8),255&e]:e>=-1131&&e<=-108?[251+((e=-e-108)>>8),255&e]:e>=-32768&&e<=32767?[28,e>>8&255,255&e]:[29,e>>24&255,e>>16&255,e>>8&255,255&e];return t}compileHeader(e){return[e.major,e.minor,4,e.offSize]}compileNameIndex(e){const t=new CFFIndex;for(const a of e){const e=Math.min(a.length,127);let r=new Array(e);for(let t=0;t"~"||"["===e||"]"===e||"("===e||")"===e||"{"===e||"}"===e||"<"===e||">"===e||"/"===e||"%"===e)&&(e="_");r[t]=e}r=r.join("");""===r&&(r="Bad_Font_Name");t.add(stringToBytes(r))}return this.compileIndex(t)}compileTopDicts(e,t,a){const r=[];let i=new CFFIndex;for(const n of e){if(a){n.removeByName("CIDFontVersion");n.removeByName("CIDFontRevision");n.removeByName("CIDFontType");n.removeByName("CIDCount");n.removeByName("UIDBase")}const e=new CFFOffsetTracker,s=this.compileDict(n,e);r.push(e);i.add(s);e.offset(t)}i=this.compileIndex(i,r);return{trackers:r,output:i}}compilePrivateDicts(e,t,a){for(let r=0,i=e.length;r>8&255,255&e])}else{i=new Uint8Array(1+2*n);i[0]=0;let t=0;const r=e.charset.length;let s=!1;for(let n=1;n>8&255;i[n+1]=255&o}}return this.compileTypedArray(i)}compileEncoding(e){return this.compileTypedArray(e.raw)}compileFDSelect(e){const t=e.format;let a,r;switch(t){case 0:a=new Uint8Array(1+e.fdSelect.length);a[0]=t;for(r=0;r>8&255,255&i,n];for(r=1;r>8&255,255&r,t);n=t}}const o=(s.length-3)/3;s[1]=o>>8&255;s[2]=255&o;s.push(r>>8&255,255&r);a=new Uint8Array(s)}return this.compileTypedArray(a)}compileTypedArray(e){return Array.from(e)}compileIndex(e,t=[]){const a=e.objects,r=a.length;if(0===r)return[0,0];const i=[r>>8&255,255&r];let n,s,o=1;for(n=0;n>8&255,255&c):3===s?i.push(c>>16&255,c>>8&255,255&c):i.push(c>>>24&255,c>>16&255,c>>8&255,255&c);a[n]&&(c+=a[n].length)}for(n=0;n=this.firstChar&&e<=this.lastChar?e:-1}amend(e){unreachable("Should not call amend()")}}class CFFFont{constructor(e,t){this.properties=t;const a=new CFFParser(e,t,Er);this.cff=a.parse();this.cff.duplicateFirstGlyph();const r=new CFFCompiler(this.cff);this.seacs=this.cff.seacs;try{this.data=r.compile()}catch{warn("Failed to compile font "+t.loadedName);this.data=e}this._createBuiltInEncoding()}get numGlyphs(){return this.cff.charStrings.count}getCharset(){return this.cff.charset.charset}getGlyphMapping(){const e=this.cff,t=this.properties,{cidToGidMap:a,cMap:r}=t,i=e.charset.charset;let n,s;if(t.composite){let t,o;if(a?.length>0){t=Object.create(null);for(let e=0,r=a.length;e=0){const r=a[t];r&&(i[e]=r)}}i.length>0&&(this.properties.builtInEncoding=i)}}function getFloat214(e,t){return readInt16(e,t)/16384}function getSubroutineBias(e){const t=e.length;let a=32768;t<1240?a=107:t<33900&&(a=1131);return a}function parseCmap(e,t,a){const r=1===readUint16(e,t+2)?readUint32(e,t+8):readUint32(e,t+16),i=readUint16(e,t+r);let n,s,o;if(4===i){readUint16(e,t+r+2);const a=readUint16(e,t+r+6)>>1;s=t+r+14;n=[];for(o=0;o>1;a0;)h.push({flags:n})}for(a=0;a>1;y=!0;break;case 4:s+=i.pop();moveTo(n,s);y=!0;break;case 5:for(;i.length>0;){n+=i.shift();s+=i.shift();lineTo(n,s)}break;case 6:for(;i.length>0;){n+=i.shift();lineTo(n,s);if(0===i.length)break;s+=i.shift();lineTo(n,s)}break;case 7:for(;i.length>0;){s+=i.shift();lineTo(n,s);if(0===i.length)break;n+=i.shift();lineTo(n,s)}break;case 8:for(;i.length>0;){l=n+i.shift();u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+i.shift();bezierCurveTo(l,u,h,d,n,s)}break;case 10:m=i.pop();b=null;if(a.isCFFCIDFont){const e=a.fdSelect.getFDIndex(r);if(e>=0&&eMath.abs(s-t)?n+=i.shift():s+=i.shift();bezierCurveTo(l,u,h,d,n,s);break;default:throw new FormatError(`unknown operator: 12 ${w}`)}break;case 14:if(i.length>=4){const e=i.pop(),r=i.pop();s=i.pop();n=i.pop();t.save();t.translate(n,s);let o=lookupCmap(a.cmap,String.fromCharCode(a.glyphNameMap[Cr[e]]));compileCharString(a.glyphs[o.glyphId],t,a,o.glyphId);t.restore();o=lookupCmap(a.cmap,String.fromCharCode(a.glyphNameMap[Cr[r]]));compileCharString(a.glyphs[o.glyphId],t,a,o.glyphId)}return;case 19:case 20:o+=i.length>>1;c+=o+7>>3;y=!0;break;case 21:s+=i.pop();n+=i.pop();moveTo(n,s);y=!0;break;case 22:n+=i.pop();moveTo(n,s);y=!0;break;case 24:for(;i.length>2;){l=n+i.shift();u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+i.shift();bezierCurveTo(l,u,h,d,n,s)}n+=i.shift();s+=i.shift();lineTo(n,s);break;case 25:for(;i.length>6;){n+=i.shift();s+=i.shift();lineTo(n,s)}l=n+i.shift();u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+i.shift();bezierCurveTo(l,u,h,d,n,s);break;case 26:i.length%2&&(n+=i.shift());for(;i.length>0;){l=n;u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h;s=d+i.shift();bezierCurveTo(l,u,h,d,n,s)}break;case 27:i.length%2&&(s+=i.shift());for(;i.length>0;){l=n+i.shift();u=s;h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d;bezierCurveTo(l,u,h,d,n,s)}break;case 28:i.push(readInt16(e,c));c+=2;break;case 29:m=i.pop()+a.gsubrsBias;b=a.gsubrs[m];b&&parse(b);break;case 30:for(;i.length>0;){l=n;u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s);if(0===i.length)break;l=n+i.shift();u=s;h=l+i.shift();d=u+i.shift();s=d+i.shift();n=h+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s)}break;case 31:for(;i.length>0;){l=n+i.shift();u=s;h=l+i.shift();d=u+i.shift();s=d+i.shift();n=h+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s);if(0===i.length)break;l=n;u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s)}break;default:if(w<32)throw new FormatError(`unknown operator: ${w}`);if(w<247)i.push(w-139);else if(w<251)i.push(256*(w-247)+e[c++]+108);else if(w<255)i.push(256*-(w-251)-e[c++]-108);else{i.push((e[c]<<24|e[c+1]<<16|e[c+2]<<8|e[c+3])/65536);c+=4}}y&&(i.length=0)}}(e)}class Commands{cmds=[];transformStack=[];currentTransform=[1,0,0,1,0,0];add(e,t){if(t){const{currentTransform:a}=this;for(let e=0,r=t.length;e=0&&e2*readUint16(e,t)}const n=[];let s=i(t,0);for(let a=r;ae.getSize()+3&-4)))}write(){const e=this.getSize(),t=new DataView(new ArrayBuffer(e)),a=e>131070,r=a?4:2,i=new DataView(new ArrayBuffer((this.glyphs.length+1)*r));a?i.setUint32(0,0):i.setUint16(0,0);let n=0,s=0;for(const e of this.glyphs){n+=e.write(n,t);n=n+3&-4;s+=r;a?i.setUint32(s,n):i.setUint16(s,n>>1)}return{isLocationLong:a,loca:new Uint8Array(i.buffer),glyf:new Uint8Array(t.buffer)}}scale(e){for(let t=0,a=this.glyphs.length;te.getSize())));return this.header.getSize()+e}write(e,t){if(!this.header)return 0;const a=e;e+=this.header.write(e,t);if(this.simple)e+=this.simple.write(e,t);else for(const a of this.composites)e+=a.write(e,t);return e-a}scale(e){if(!this.header)return;const t=(this.header.xMin+this.header.xMax)/2;this.header.scale(t,e);if(this.simple)this.simple.scale(t,e);else for(const a of this.composites)a.scale(t,e)}}class GlyphHeader{constructor({numberOfContours:e,xMin:t,yMin:a,xMax:r,yMax:i}){this.numberOfContours=e;this.xMin=t;this.yMin=a;this.xMax=r;this.yMax=i}static parse(e,t){return[10,new GlyphHeader({numberOfContours:t.getInt16(e),xMin:t.getInt16(e+2),yMin:t.getInt16(e+4),xMax:t.getInt16(e+6),yMax:t.getInt16(e+8)})]}getSize(){return 10}write(e,t){t.setInt16(e,this.numberOfContours);t.setInt16(e+2,this.xMin);t.setInt16(e+4,this.yMin);t.setInt16(e+6,this.xMax);t.setInt16(e+8,this.yMax);return 10}scale(e,t){this.xMin=Math.round(e+(this.xMin-e)*t);this.xMax=Math.round(e+(this.xMax-e)*t)}}class Contour{constructor({flags:e,xCoordinates:t,yCoordinates:a}){this.xCoordinates=t;this.yCoordinates=a;this.flags=e}}class SimpleGlyph{constructor({contours:e,instructions:t}){this.contours=e;this.instructions=t}static parse(e,t,a){const r=[];for(let i=0;i255?e+=2:o>0&&(e+=1);t=n;o=Math.abs(s-a);o>255?e+=2:o>0&&(e+=1);a=s}}return e}write(e,t){const a=e,r=[],i=[],n=[];let s=0,o=0;for(const a of this.contours){for(let e=0,t=a.xCoordinates.length;e=0?18:2;r.push(e)}else r.push(l)}s=c;const h=a.yCoordinates[e];l=h-o;if(0===l){t|=32;i.push(0)}else{const e=Math.abs(l);if(e<=255){t|=l>=0?36:4;i.push(e)}else i.push(l)}o=h;n.push(t)}t.setUint16(e,r.length-1);e+=2}t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}for(const a of n)t.setUint8(e++,a);for(let a=0,i=r.length;a=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(e+=2):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(e+=2);return e}write(e,t){const a=e;2&this.flags?this.argument1>=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(this.flags|=1):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(this.flags|=1);t.setUint16(e,this.flags);t.setUint16(e+2,this.glyphIndex);e+=4;if(1&this.flags){if(2&this.flags){t.setInt16(e,this.argument1);t.setInt16(e+2,this.argument2)}else{t.setUint16(e,this.argument1);t.setUint16(e+2,this.argument2)}e+=4}else{t.setUint8(e,this.argument1);t.setUint8(e+1,this.argument2);e+=2}if(256&this.flags){t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}}return e-a}scale(e,t){}}function writeInt16(e,t,a){e[t]=a>>8&255;e[t+1]=255&a}function writeInt32(e,t,a){e[t]=a>>24&255;e[t+1]=a>>16&255;e[t+2]=a>>8&255;e[t+3]=255&a}function writeData(e,t,a){if(a instanceof Uint8Array)e.set(a,t);else if("string"==typeof a)for(let r=0,i=a.length;ra;){a<<=1;r++}const i=a*t;return{range:i,entry:r,rangeShift:t*e-i}}toArray(){let e=this.sfnt;const t=this.tables,a=Object.keys(t);a.sort();const r=a.length;let i,n,s,o,c,l=12+16*r;const h=[l];for(i=0;i>>0;h.push(l)}const u=new Uint8Array(l);for(i=0;i>>0}writeInt32(u,l+4,e);writeInt32(u,l+8,h[i]);writeInt32(u,l+12,t[c].length);l+=16}return u}addTable(e,t){if(e in this.tables)throw new Error("Table "+e+" already exists");this.tables[e]=t}}const ci=[4],li=[5],hi=[6],ui=[7],di=[8],fi=[12,35],gi=[14],pi=[21],mi=[22],bi=[30],yi=[31];class Type1CharString{constructor(){this.width=0;this.lsb=0;this.flexing=!1;this.output=[];this.stack=[]}convert(e,t,a){const r=e.length;let i,n,s,o=!1;for(let c=0;cr)return!0;const i=r-e;for(let e=i;e>8&255,255&t);else{t=65536*t|0;this.output.push(255,t>>24&255,t>>16&255,t>>8&255,255&t)}}this.output.push(...t);a?this.stack.splice(i,e):this.stack.length=0;return!1}}function isHexDigit(e){return e>=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102}function decrypt(e,t,a){if(a>=e.length)return new Uint8Array(0);let r,i,n=0|t;for(r=0;r>8;n=52845*(t+n)+22719&65535}return o}function isSpecial(e){return 47===e||91===e||93===e||123===e||125===e||40===e||41===e}class Type1Parser{constructor(e,t,a){if(t){const t=e.getBytes(),a=!((isHexDigit(t[0])||isWhiteSpace(t[0]))&&isHexDigit(t[1])&&isHexDigit(t[2])&&isHexDigit(t[3])&&isHexDigit(t[4])&&isHexDigit(t[5])&&isHexDigit(t[6])&&isHexDigit(t[7]));e=new Stream(a?decrypt(t,55665,4):function decryptAscii(e,t,a){let r=0|t;const i=e.length,n=new Uint8Array(i>>>1);let s,o;for(s=0,o=0;s>8;r=52845*(e+r)+22719&65535}}return n.slice(a,o)}(t,55665,4))}this.seacAnalysisEnabled=!!a;this.stream=e;this.nextChar()}readNumberArray(){this.getToken();const e=[];for(;;){const t=this.getToken();if(null===t||"]"===t||"}"===t)break;e.push(parseFloat(t||0))}return e}readNumber(){const e=this.getToken();return parseFloat(e||0)}readInt(){const e=this.getToken();return 0|parseInt(e||0,10)}readBoolean(){return"true"===this.getToken()?1:0}nextChar(){return this.currentChar=this.stream.getByte()}prevChar(){this.stream.skip(-2);return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(-1===t)return null;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}if(isSpecial(t)){this.nextChar();return String.fromCharCode(t)}let a="";do{a+=String.fromCharCode(t);t=this.nextChar()}while(t>=0&&!isWhiteSpace(t)&&!isSpecial(t));return a}readCharStrings(e,t){return-1===t?e:decrypt(e,4330,t)}extractFontProgram(e){const t=this.stream,a=[],r=[],i=Object.create(null);i.lenIV=4;const n={subrs:[],charstrings:[],properties:{privateData:i}};let s,o,c,l;for(;null!==(s=this.getToken());)if("/"===s){s=this.getToken();switch(s){case"CharStrings":this.getToken();this.getToken();this.getToken();this.getToken();for(;;){s=this.getToken();if(null===s||"end"===s)break;if("/"!==s)continue;const e=this.getToken();o=this.readInt();this.getToken();c=o>0?t.getBytes(o):new Uint8Array(0);l=n.properties.privateData.lenIV;const a=this.readCharStrings(c,l);this.nextChar();s=this.getToken();"noaccess"===s?this.getToken():"/"===s&&this.prevChar();r.push({glyph:e,encoded:a})}break;case"Subrs":this.readInt();this.getToken();for(;"dup"===this.getToken();){const e=this.readInt();o=this.readInt();this.getToken();c=o>0?t.getBytes(o):new Uint8Array(0);l=n.properties.privateData.lenIV;const r=this.readCharStrings(c,l);this.nextChar();s=this.getToken();"noaccess"===s&&this.getToken();a[e]=r}break;case"BlueValues":case"OtherBlues":case"FamilyBlues":case"FamilyOtherBlues":const e=this.readNumberArray();e.length>0&&e.length,0;break;case"StemSnapH":case"StemSnapV":n.properties.privateData[s]=this.readNumberArray();break;case"StdHW":case"StdVW":n.properties.privateData[s]=this.readNumberArray()[0];break;case"BlueShift":case"lenIV":case"BlueFuzz":case"BlueScale":case"LanguageGroup":n.properties.privateData[s]=this.readNumber();break;case"ExpansionFactor":n.properties.privateData[s]=this.readNumber()||.06;break;case"ForceBold":n.properties.privateData[s]=this.readBoolean()}}for(const{encoded:t,glyph:i}of r){const r=new Type1CharString,s=r.convert(t,a,this.seacAnalysisEnabled);let o=r.output;s&&(o=[14]);const c={glyphName:i,charstring:o,width:r.width,lsb:r.lsb,seac:r.seac};".notdef"===i?n.charstrings.unshift(c):n.charstrings.push(c);if(e.builtInEncoding){const t=e.builtInEncoding.indexOf(i);t>-1&&void 0===e.widths[t]&&t>=e.firstChar&&t<=e.lastChar&&(e.widths[t]=r.width)}}return n}extractFontHeader(e){let t;for(;null!==(t=this.getToken());)if("/"===t){t=this.getToken();switch(t){case"FontMatrix":const a=this.readNumberArray();e.fontMatrix=a;break;case"Encoding":const r=this.getToken();let i;if(/^\d+$/.test(r)){i=[];const e=0|parseInt(r,10);this.getToken();for(let a=0;a=i){s+=a;for(;s=0&&(r[e]=i)}}return type1FontGlyphMapping(e,r,a)}hasGlyphId(e){if(e<0||e>=this.numGlyphs)return!1;if(0===e)return!0;return this.charstrings[e-1].charstring.length>0}getSeacs(e){const t=[];for(let a=0,r=e.length;a0;e--)t[e]-=t[e-1];f.setByName(e,t)}n.topDict.privateDict=f;const p=new CFFIndex;for(h=0,u=r.length;h0&&e.toUnicode.amend(t)}class fonts_Glyph{constructor(e,t,a,r,i,n,s,o,c){this.originalCharCode=e;this.fontChar=t;this.unicode=a;this.accent=r;this.width=i;this.vmetric=n;this.operatorListId=s;this.isSpace=o;this.isInFont=c}get category(){return shadow(this,"category",function getCharUnicodeCategory(e){const t=Nr.get(e);if(t)return t;const a=e.match(Rr),r={isWhitespace:!!a?.[1],isZeroWidthDiacritic:!!a?.[2],isInvisibleFormatMark:!!a?.[3]};Nr.set(e,r);return r}(this.unicode),!0)}}function int16(e,t){return(e<<8)+t}function writeSignedInt16(e,t,a){e[t+1]=a;e[t]=a>>>8}function signedInt16(e,t){const a=(e<<8)+t;return 32768&a?a-65536:a}function string16(e){return String.fromCharCode(e>>8&255,255&e)}function safeString16(e){e>32767?e=32767:e<-32768&&(e=-32768);return String.fromCharCode(e>>8&255,255&e)}function isTrueTypeCollectionFile(e){return"ttcf"===bytesToString(e.peekBytes(4))}function getFontFileType(e,{type:t,subtype:a,composite:r}){let i,n;if(function isTrueTypeFile(e){const t=e.peekBytes(4);return 65536===readUint32(t,0)||"true"===bytesToString(t)}(e)||isTrueTypeCollectionFile(e))i=r?"CIDFontType2":"TrueType";else if(function isOpenTypeFile(e){return"OTTO"===bytesToString(e.peekBytes(4))}(e))i=r?"CIDFontType2":"OpenType";else if(function isType1File(e){const t=e.peekBytes(2);return 37===t[0]&&33===t[1]||128===t[0]&&1===t[1]}(e))i=r?"CIDFontType0":"MMType1"===t?"MMType1":"Type1";else if(function isCFFFile(e){const t=e.peekBytes(4);return t[0]>=1&&t[3]>=1&&t[3]<=4}(e))if(r){i="CIDFontType0";n="CIDFontType0C"}else{i="MMType1"===t?"MMType1":"Type1";n="Type1C"}else{warn("getFontFileType: Unable to detect correct font file Type/Subtype.");i=t;n=a}return[i,n]}function applyStandardFontGlyphMap(e,t){for(const a in t)e[+a]=t[a]}function buildToFontChar(e,t,a){const r=[];let i;for(let a=0,n=e.length;ah){c++;if(c>=wi.length){warn("Ran out of space in font private use area.");break}l=wi[c][0];h=wi[c][1]}const p=l++;0===g&&(g=a);let m=r.get(f);if("string"==typeof m)if(1===m.length)m=m.codePointAt(0);else{if(!u){u=new Map;for(let e=64256;e<=64335;e++){const t=String.fromCharCode(e).normalize("NFKD");t.length>1&&u.set(t,e)}}m=u.get(m)||m.codePointAt(0)}if(m&&!(d=m,wi[0][0]<=d&&d<=wi[0][1]||wi[1][0]<=d&&d<=wi[1][1])&&!o.has(g)){n.set(m,g);o.add(g)}i[p]=g;s[f]=p}var d;return{toFontChar:s,charCodeToGlyphId:i,toUnicodeExtraMap:n,nextAvailableFontCharCode:l}}function createCmapTable(e,t,a){const r=function getRanges(e,t,a){const r=[];for(const t in e)e[t]>=a||r.push({fontCharCode:0|t,glyphId:e[t]});if(t)for(const[e,i]of t)i>=a||r.push({fontCharCode:e,glyphId:i});0===r.length&&r.push({fontCharCode:0,glyphId:0});r.sort(((e,t)=>e.fontCharCode-t.fontCharCode));const i=[],n=r.length;for(let e=0;e65535?2:1;let n,s,o,c,l="\0\0"+string16(i)+"\0\0"+string32(4+8*i);for(n=r.length-1;n>=0&&!(r[n][0]<=65535);--n);const h=n+1;r[n][0]<65535&&65535===r[n][1]&&(r[n][1]=65534);const u=r[n][1]<65535?1:0,d=h+u,f=OpenTypeFileBuilder.getSearchParams(d,2);let g,p,m,b,y="",w="",S="",x="",k="",C=0;for(n=0,s=h;n0){w+="ÿÿ";y+="ÿÿ";S+="\0";x+="\0\0"}const v="\0\0"+string16(2*d)+string16(f.range)+string16(f.entry)+string16(f.rangeShift)+w+"\0\0"+y+S+x+k;let F="",O="";if(i>1){l+="\0\0\n"+string32(4+8*i+4+v.length);F="";for(n=0,s=r.length;ne||!o)&&(o=e);c 123 are reserved for internal usage");s|=1<65535&&(c=65535)}else{o=0;c=255}const h=e.bbox||[0,0,0,0],u=a.unitsPerEm||(e.fontMatrix?1/Math.max(...e.fontMatrix.slice(0,4).map(Math.abs)):1e3),d=e.ascentScaled?1:u/Si,f=a.ascent||Math.round(d*(e.ascent||h[3]));let g=a.descent||Math.round(d*(e.descent||h[1]));g>0&&e.descent>0&&h[1]<0&&(g=-g);const p=a.yMax||f,m=-a.yMin||-g;return"\0$ô\0\0\0Š»\0\0\0ŒŠ»\0\0ß\x001\0\0\0\0"+String.fromCharCode(e.fixedPitch?9:0)+"\0\0\0\0\0\0"+string32(r)+string32(i)+string32(n)+string32(s)+"*21*"+string16(e.italicAngle?1:0)+string16(o||e.firstChar)+string16(c||e.lastChar)+string16(f)+string16(g)+"\0d"+string16(p)+string16(m)+"\0\0\0\0\0\0\0\0"+string16(e.xHeight)+string16(e.capHeight)+string16(0)+string16(o||e.firstChar)+"\0"}function createPostTable(e){return"\0\0\0"+string32(Math.floor(65536*e.italicAngle))+"\0\0\0\0"+string32(e.fixedPitch?1:0)+"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}function createPostscriptName(e){return e.replaceAll(/[^\x21-\x7E]|[[\](){}<>/%]/g,"").slice(0,63)}function createNameTable(e,t){t||(t=[[],[]]);const a=[t[0][0]||"Original licence",t[0][1]||e,t[0][2]||"Unknown",t[0][3]||"uniqueID",t[0][4]||e,t[0][5]||"Version 0.11",t[0][6]||createPostscriptName(e),t[0][7]||"Unknown",t[0][8]||"Unknown",t[0][9]||"Unknown"],r=[];let i,n,s,o,c;for(i=0,n=a.length;i0;if((s||o)&&"CIDFontType2"===a&&this.cidEncoding.startsWith("Identity-")){const a=e.cidToGidMap,r=[];applyStandardFontGlyphMap(r,ri());/Arial-?Black/i.test(t)?applyStandardFontGlyphMap(r,ii()):/Calibri/i.test(t)&&applyStandardFontGlyphMap(r,ni());if(a){for(const e in r){const t=r[e];void 0!==a[t]&&(r[+e]=a[t])}a.length!==this.toUnicode.length&&e.hasIncludedToUnicodeMap&&this.toUnicode instanceof IdentityToUnicodeMap&&this.toUnicode.forEach((function(e,t){const i=r[e];void 0===a[i]&&(r[+e]=t)}))}this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach((function(e,t){r[+e]=t}));this.toFontChar=r;this.toUnicode=new ToUnicodeMap(r)}else if(/Symbol/i.test(r))this.toFontChar=buildToFontChar(Fr,Or(),this.differences);else if(/Dingbats/i.test(r))this.toFontChar=buildToFontChar(Ir,Tr(),this.differences);else if(s||o){const e=buildToFontChar(this.defaultEncoding,Or(),this.differences);"CIDFontType2"!==a||this.cidEncoding.startsWith("Identity-")||this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach((function(t,a){e[+t]=a}));this.toFontChar=e}else{const e=Or(),a=[];this.toUnicode.forEach(((t,r)=>{if(!this.composite){const a=getUnicodeForGlyph(this.differences[t]||this.defaultEncoding[t],e);-1!==a&&(r=a)}a[+t]=r}));this.composite&&this.toUnicode instanceof IdentityToUnicodeMap&&/Tahoma|Verdana/i.test(t)&&applyStandardFontGlyphMap(a,ri());this.toFontChar=a}amendFallbackToUnicode(e);this.loadedName=r.split("-",1)[0]}checkAndRepair(e,t,a){const r=["OS/2","cmap","head","hhea","hmtx","maxp","name","post","loca","glyf","fpgm","prep","cvt ","CFF "];function readTables(e,t){const a=Object.create(null);a["OS/2"]=null;a.cmap=null;a.head=null;a.hhea=null;a.hmtx=null;a.maxp=null;a.name=null;a.post=null;for(let i=0;i>>0,r=e.getInt32()>>>0,i=e.getInt32()>>>0,n=e.pos;e.pos=e.start||0;e.skip(r);const s=e.getBytes(i);e.pos=n;if("head"===t){s[8]=s[9]=s[10]=s[11]=0;s[17]|=32}return{tag:t,checksum:a,length:i,offset:r,data:s}}function readOpenTypeHeader(e){return{version:e.getString(4),numTables:e.getUint16(),searchRange:e.getUint16(),entrySelector:e.getUint16(),rangeShift:e.getUint16()}}function sanitizeGlyph(e,t,a,r,i,n){const s={length:0,sizeOfInstructions:0};if(t<0||t>=e.length||a>e.length||a-t<=12)return s;const o=e.subarray(t,a),c=signedInt16(o[2],o[3]),l=signedInt16(o[4],o[5]),h=signedInt16(o[6],o[7]),u=signedInt16(o[8],o[9]);if(c>h){writeSignedInt16(o,2,h);writeSignedInt16(o,6,c)}if(l>u){writeSignedInt16(o,4,u);writeSignedInt16(o,8,l)}const d=signedInt16(o[0],o[1]);if(d<0){if(d<-1)return s;r.set(o,i);s.length=o.length;return s}let f,g=10,p=0;for(f=0;fo.length)return s;if(!n&&b>0){r.set(o.subarray(0,m),i);r.set([0,0],i+m);r.set(o.subarray(y,S),i+m+2);S-=b;o.length-S>3&&(S=S+3&-4);s.length=S;return s}if(o.length-S>3){S=S+3&-4;r.set(o.subarray(0,S),i);s.length=S;return s}r.set(o,i);s.length=o.length;return s}function readNameTable(e){const a=(t.start||0)+e.offset;t.pos=a;const r=[[],[]],i=[],n=e.length,s=a+n;if(0!==t.getUint16()||n<6)return[r,i];const o=t.getUint16(),c=t.getUint16();let l,h;for(l=0;ls)continue;t.pos=n;const o=e.name;if(e.encoding){let a="";for(let r=0,i=e.length;r0&&(l+=e-1)}}else{if(m||y){warn("TT: nested FDEFs not allowed");p=!0}m=!0;u=l;s=d.pop();t.functionsDefined[s]={data:c,i:l}}else if(!m&&!y){s=d.at(-1);if(isNaN(s))info("TT: CALL empty stack (or invalid entry).");else{t.functionsUsed[s]=!0;if(s in t.functionsStackDeltas){const e=d.length+t.functionsStackDeltas[s];if(e<0){warn("TT: CALL invalid functions stack delta.");t.hintsValid=!1;return}d.length=e}else if(s in t.functionsDefined&&!g.includes(s)){f.push({data:c,i:l,stackTop:d.length-1});g.push(s);o=t.functionsDefined[s];if(!o){warn("TT: CALL non-existent function");t.hintsValid=!1;return}c=o.data;l=o.i}}}if(!m&&!y){let t=0;e<=142?t=i[e]:e>=192&&e<=223?t=-1:e>=224&&(t=-2);if(e>=113&&e<=117){r=d.pop();isNaN(r)||(t=2*-r)}for(;t<0&&d.length>0;){d.pop();t++}for(;t>0;){d.push(NaN);t--}}}t.tooComplexToFollowFunctions=p;const w=[c];l>c.length&&w.push(new Uint8Array(l-c.length));if(u>h){warn("TT: complementing a missing function tail");w.push(new Uint8Array([34,45]))}!function foldTTTable(e,t){if(t.length>1){let a,r,i=0;for(a=0,r=t.length;a>>0,n=[];for(let t=0;t>>0);const s={ttcTag:t,majorVersion:a,minorVersion:r,numFonts:i,offsetTable:n};switch(a){case 1:return s;case 2:s.dsigTag=e.getInt32()>>>0;s.dsigLength=e.getInt32()>>>0;s.dsigOffset=e.getInt32()>>>0;return s}throw new FormatError(`Invalid TrueType Collection majorVersion: ${a}.`)}(e),i=t.split("+");let n;for(let s=0;s0||!(a.cMap instanceof IdentityCMap));if("OTTO"===n.version&&!t||!s.head||!s.hhea||!s.maxp||!s.post){c=new Stream(s["CFF "].data);o=new CFFFont(c,a);return this.convert(e,o,a)}delete s.glyf;delete s.loca;delete s.fpgm;delete s.prep;delete s["cvt "];this.isOpenType=!0}if(!s.maxp)throw new FormatError('Required "maxp" table is not found');t.pos=(t.start||0)+s.maxp.offset;let h=t.getInt32();const u=t.getUint16();if(65536!==h&&20480!==h){if(6===s.maxp.length)h=20480;else{if(!(s.maxp.length>=32))throw new FormatError('"maxp" table has a wrong version number');h=65536}!function writeUint32(e,t,a){e[t+3]=255&a;e[t+2]=a>>>8;e[t+1]=a>>>16;e[t]=a>>>24}(s.maxp.data,0,h)}if(a.scaleFactors?.length===u&&l){const{scaleFactors:e}=a,t=int16(s.head.data[50],s.head.data[51]),r=new GlyfTable({glyfTable:s.glyf.data,isGlyphLocationsLong:t,locaTable:s.loca.data,numGlyphs:u});r.scale(e);const{glyf:i,loca:n,isLocationLong:o}=r.write();s.glyf.data=i;s.loca.data=n;if(o!==!!t){s.head.data[50]=0;s.head.data[51]=o?1:0}const c=s.hmtx.data;for(let t=0;t>8&255;c[a+1]=255&r;writeSignedInt16(c,a+2,Math.round(e[t]*signedInt16(c[a+2],c[a+3])))}}let d=u+1,f=!0;if(d>65535){f=!1;d=u;warn("Not enough space in glyfs to duplicate first glyph.")}let g=0,p=0;if(h>=65536&&s.maxp.length>=32){t.pos+=8;if(t.getUint16()>2){s.maxp.data[14]=0;s.maxp.data[15]=2}t.pos+=4;g=t.getUint16();t.pos+=4;p=t.getUint16()}s.maxp.data[4]=d>>8;s.maxp.data[5]=255&d;const m=function sanitizeTTPrograms(e,t,a,r){const i={functionsDefined:[],functionsUsed:[],functionsStackDeltas:[],tooComplexToFollowFunctions:!1,hintsValid:!0};e&&sanitizeTTProgram(e,i);t&&sanitizeTTProgram(t,i);e&&function checkInvalidFunctions(e,t){if(!e.tooComplexToFollowFunctions)if(e.functionsDefined.length>t){warn("TT: more functions defined than expected");e.hintsValid=!1}else for(let a=0,r=e.functionsUsed.length;at){warn("TT: invalid function id: "+a);e.hintsValid=!1;return}if(e.functionsUsed[a]&&!e.functionsDefined[a]){warn("TT: undefined function: "+a);e.hintsValid=!1;return}}}(i,r);if(a&&1&a.length){const e=new Uint8Array(a.length+1);e.set(a.data);a.data=e}return i.hintsValid}(s.fpgm,s.prep,s["cvt "],g);if(!m){delete s.fpgm;delete s.prep;delete s["cvt "]}!function sanitizeMetrics(e,t,a,r,i,n){if(!t){a&&(a.data=null);return}e.pos=(e.start||0)+t.offset;e.pos+=4;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;const s=e.getUint16();e.pos+=8;e.pos+=2;let o=e.getUint16();if(0!==s){if(!(2&int16(r.data[44],r.data[45]))){t.data[22]=0;t.data[23]=0}}if(o>i){info(`The numOfMetrics (${o}) should not be greater than the numGlyphs (${i}).`);o=i;t.data[34]=(65280&o)>>8;t.data[35]=255&o}const c=i-o-(a.length-4*o>>1);if(c>0){const e=new Uint8Array(a.length+2*c);e.set(a.data);if(n){e[a.length]=a.data[2];e[a.length+1]=a.data[3]}a.data=e}}(t,s.hhea,s.hmtx,s.head,d,f);if(!s.head)throw new FormatError('Required "head" table is not found');!function sanitizeHead(e,t,a){const r=e.data,i=function int32(e,t,a,r){return(e<<24)+(t<<16)+(a<<8)+r}(r[0],r[1],r[2],r[3]);if(i>>16!=1){info("Attempting to fix invalid version in head table: "+i);r[0]=0;r[1]=1;r[2]=0;r[3]=0}const n=int16(r[50],r[51]);if(n<0||n>1){info("Attempting to fix invalid indexToLocFormat in head table: "+n);const e=t+1;if(a===e<<1){r[50]=0;r[51]=0}else{if(a!==e<<2)throw new FormatError("Could not fix indexToLocFormat: "+n);r[50]=0;r[51]=1}}}(s.head,u,l?s.loca.length:0);let b=Object.create(null);if(l){const e=int16(s.head.data[50],s.head.data[51]),t=function sanitizeGlyphLocations(e,t,a,r,i,n,s){let o,c,l;if(r){o=4;c=function fontItemDecodeLong(e,t){return e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3]};l=function fontItemEncodeLong(e,t,a){e[t]=a>>>24&255;e[t+1]=a>>16&255;e[t+2]=a>>8&255;e[t+3]=255&a}}else{o=2;c=function fontItemDecode(e,t){return e[t]<<9|e[t+1]<<1};l=function fontItemEncode(e,t,a){e[t]=a>>9&255;e[t+1]=a>>1&255}}const h=n?a+1:a,u=o*(1+h),d=new Uint8Array(u);d.set(e.data.subarray(0,u));e.data=d;const f=t.data,g=f.length,p=new Uint8Array(g);let m,b;const y=[];for(m=0,b=0;mg&&(e=g);y.push({index:m,offset:e,endOffset:0})}y.sort(((e,t)=>e.offset-t.offset));for(m=0;me.index-t.index));for(m=0;ms&&(s=e.sizeOfInstructions);x+=t;l(d,b,x)}if(0===x){const e=new Uint8Array([0,1,0,0,0,0,0,0,0,0,0,0,0,0,49,0]);for(m=0,b=o;ma+x)t.data=p.subarray(0,a+x);else{t.data=new Uint8Array(a+x);t.data.set(p.subarray(0,x))}t.data.set(p.subarray(0,a),x);l(e.data,d.length-o,x+a)}else t.data=p.subarray(0,x);return{missingGlyphs:S,maxSizeOfInstructions:s}}(s.loca,s.glyf,u,e,m,f,p);b=t.missingGlyphs;if(h>=65536&&s.maxp.length>=32){s.maxp.data[26]=t.maxSizeOfInstructions>>8;s.maxp.data[27]=255&t.maxSizeOfInstructions}}if(!s.hhea)throw new FormatError('Required "hhea" table is not found');if(0===s.hhea.data[10]&&0===s.hhea.data[11]){s.hhea.data[10]=255;s.hhea.data[11]=255}const y={unitsPerEm:int16(s.head.data[18],s.head.data[19]),yMax:signedInt16(s.head.data[42],s.head.data[43]),yMin:signedInt16(s.head.data[38],s.head.data[39]),ascent:signedInt16(s.hhea.data[4],s.hhea.data[5]),descent:signedInt16(s.hhea.data[6],s.hhea.data[7]),lineGap:signedInt16(s.hhea.data[8],s.hhea.data[9])};this.ascent=y.ascent/y.unitsPerEm;this.descent=y.descent/y.unitsPerEm;this.lineGap=y.lineGap/y.unitsPerEm;if(this.cssFontInfo?.lineHeight){this.lineHeight=this.cssFontInfo.metrics.lineHeight;this.lineGap=this.cssFontInfo.metrics.lineGap}else this.lineHeight=this.ascent-this.descent+this.lineGap;s.post&&function readPostScriptTable(e,a,r){const i=(t.start||0)+e.offset;t.pos=i;const n=i+e.length,s=t.getInt32();t.skip(28);let o,c,l=!0;switch(s){case 65536:o=Ur;break;case 131072:const e=t.getUint16();if(e!==r){l=!1;break}const i=[];for(c=0;c=32768){l=!1;break}i.push(e)}if(!l)break;const h=[],u=[];for(;t.pos65535)throw new FormatError("Max size of CID is 65,535");let i=-1;t?i=r:void 0!==e[r]&&(i=e[r]);i>=0&&i>>0;let h=!1;if(o?.platformId!==i||o?.encodingId!==n){if(0!==i||0!==n&&1!==n&&3!==n)if(1===i&&0===n)h=!0;else if(3!==i||1!==n||!r&&o){if(a&&3===i&&0===n){h=!0;let a=!0;if(e>3;e.push(r);a=Math.max(r,a)}const r=[];for(let e=0;e<=a;e++)r.push({firstCode:t.getUint16(),entryCount:t.getUint16(),idDelta:signedInt16(t.getByte(),t.getByte()),idRangePos:t.pos+t.getUint16()});for(let a=0;a<256;a++)if(0===e[a]){t.pos=r[0].idRangePos+2*a;f=t.getUint16();u.push({charCode:a,glyphId:f})}else{const i=r[e[a]];for(d=0;d>1;t.skip(6);const a=[];let r;for(r=0;r>1)-(e-r);i.offsetIndex=s;o=Math.max(o,s+i.end-i.start+1)}else i.offsetIndex=-1}const c=[];for(d=0;d>>0;for(d=0;d>>0,a=t.getInt32()>>>0;let r=t.getInt32()>>>0;for(let t=e;t<=a;t++)u.push({charCode:t,glyphId:r++})}}}u.sort(((e,t)=>e.charCode-t.charCode));const g=[],p=new Set;for(const e of u){const{charCode:t}=e;if(!p.has(t)){p.add(t);g.push(e)}}return{platformId:o.platformId,encodingId:o.encodingId,mappings:g,hasShortCmap:h}}(s.cmap,t,this.isSymbolicFont,a.hasEncoding),r=e.platformId,i=e.encodingId,n=e.mappings;let o=[],c=!1;!a.hasEncoding||"MacRomanEncoding"!==a.baseEncodingName&&"WinAnsiEncoding"!==a.baseEncodingName||(o=getEncoding(a.baseEncodingName));if(a.hasEncoding&&!this.isSymbolicFont&&(3===r&&1===i||1===r&&0===i)){const e=Or();for(let t=0;t<256;t++){let s;s=void 0!==this.differences[t]?this.differences[t]:o.length&&""!==o[t]?o[t]:Cr[t];if(!s)continue;const c=recoverGlyphName(s,e);let l;3===r&&1===i?l=e[c]:1===r&&0===i&&(l=kr.indexOf(c));if(void 0===l){if(!a.glyphNames&&a.hasIncludedToUnicodeMap&&!(this.toUnicode instanceof IdentityToUnicodeMap)){const e=this.toUnicode.get(t);e&&(l=e.codePointAt(0))}if(void 0===l)continue}for(const e of n)if(e.charCode===l){w[t]=e.glyphId;break}}}else if(0===r){for(const e of n)w[e.charCode]=e.glyphId;c=!0}else if(3===r&&0===i)for(const e of n){let t=e.charCode;t>=61440&&t<=61695&&(t&=255);w[t]=e.glyphId}else for(const e of n)w[e.charCode]=e.glyphId;if(a.glyphNames&&(o.length||this.differences.length))for(let e=0;e<256;++e){if(!c&&void 0!==w[e])continue;const t=this.differences[e]||o[e];if(!t)continue;const r=a.glyphNames.indexOf(t);r>0&&hasGlyph(r)&&(w[e]=r)}}0===w.length&&(w[0]=0);let S=d-1;f||(S=0);if(!a.cssFontInfo){const e=adjustMapping(w,hasGlyph,S,this.toUnicode);this.toFontChar=e.toFontChar;s.cmap={tag:"cmap",data:createCmapTable(e.charCodeToGlyphId,e.toUnicodeExtraMap,d)};s["OS/2"]&&function validateOS2Table(e,t){t.pos=(t.start||0)+e.offset;const a=t.getUint16();t.skip(60);const r=t.getUint16();if(a<4&&768&r)return!1;if(t.getUint16()>t.getUint16())return!1;t.skip(6);if(0===t.getUint16())return!1;e.data[8]=e.data[9]=0;return!0}(s["OS/2"],t)||(s["OS/2"]={tag:"OS/2",data:createOS2Table(a,e.charCodeToGlyphId,y)})}if(!l)try{c=new Stream(s["CFF "].data);o=new CFFParser(c,a,Er).parse();o.duplicateFirstGlyph();const e=new CFFCompiler(o);s["CFF "].data=e.compile()}catch{warn("Failed to compile font "+a.loadedName)}if(s.name){const[t,r]=readNameTable(s.name);s.name.data=createNameTable(e,t);this.psName=t[0][6]||null;a.composite||function adjustTrueTypeToUnicode(e,t,a){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(e.hasEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;if(!t)return;if(0===a.length)return;if(e.defaultEncoding===vr)return;for(const e of a)if(!isWinNameRecord(e))return;const r=vr,i=[],n=Or();for(const e in r){const t=r[e];if(""===t)continue;const a=n[t];void 0!==a&&(i[e]=String.fromCharCode(a))}i.length>0&&e.toUnicode.amend(i)}(a,this.isSymbolicFont,r)}else s.name={tag:"name",data:createNameTable(this.name)};const x=new OpenTypeFileBuilder(n.version);for(const e in s)x.addTable(e,s[e].data);return x.toArray()}convert(e,a,r){r.fixedPitch=!1;r.builtInEncoding&&function adjustType1ToUnicode(e,t){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(t===e.defaultEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;const a=[],r=Or();for(const i in t){if(e.hasEncoding&&(e.baseEncodingName||void 0!==e.differences[i]))continue;const n=getUnicodeForGlyph(t[i],r);-1!==n&&(a[i]=String.fromCharCode(n))}a.length>0&&e.toUnicode.amend(a)}(r,r.builtInEncoding);let i=1;a instanceof CFFFont&&(i=a.numGlyphs-1);const n=a.getGlyphMapping(r);let s=null,o=n,c=null;if(!r.cssFontInfo){s=adjustMapping(n,a.hasGlyphId.bind(a),i,this.toUnicode);this.toFontChar=s.toFontChar;o=s.charCodeToGlyphId;c=s.toUnicodeExtraMap}const l=a.numGlyphs;function getCharCodes(e,t){let a=null;for(const r in e)t===e[r]&&(a||=[]).push(0|r);return a}function createCharCode(e,t){for(const a in e)if(t===e[a])return 0|a;s.charCodeToGlyphId[s.nextAvailableFontCharCode]=t;return s.nextAvailableFontCharCode++}const h=a.seacs;if(s&&h?.length){const e=r.fontMatrix||t,i=a.getCharset(),o=Object.create(null);for(let t in h){t|=0;const a=h[t],r=Cr[a[2]],c=Cr[a[3]],l=i.indexOf(r),u=i.indexOf(c);if(l<0||u<0)continue;const d={x:a[0]*e[0]+a[1]*e[2]+e[4],y:a[0]*e[1]+a[1]*e[3]+e[5]},f=getCharCodes(n,t);if(f)for(const e of f){const t=s.charCodeToGlyphId,a=createCharCode(t,l),r=createCharCode(t,u);o[e]={baseFontCharCode:a,accentFontCharCode:r,accentOffset:d}}}r.seacMap=o}const u=r.fontMatrix?1/Math.max(...r.fontMatrix.slice(0,4).map(Math.abs)):1e3,d=new OpenTypeFileBuilder("OTTO");d.addTable("CFF ",a.data);d.addTable("OS/2",createOS2Table(r,o));d.addTable("cmap",createCmapTable(o,c,l));d.addTable("head","\0\0\0\0\0\0\0\0\0\0_<õ\0\0"+safeString16(u)+"\0\0\0\0ž\v~'\0\0\0\0ž\v~'\0\0"+safeString16(r.descent)+"ÿ"+safeString16(r.ascent)+string16(r.italicAngle?2:0)+"\0\0\0\0\0\0\0");d.addTable("hhea","\0\0\0"+safeString16(r.ascent)+safeString16(r.descent)+"\0\0ÿÿ\0\0\0\0\0\0"+safeString16(r.capHeight)+safeString16(Math.tan(r.italicAngle)*r.xHeight)+"\0\0\0\0\0\0\0\0\0\0\0\0"+string16(l));d.addTable("hmtx",function fontFieldsHmtx(){const e=a.charstrings,t=a.cff?a.cff.widths:null;let r="\0\0\0\0";for(let a=1,i=l;a=65520&&e<=65535?0:e>=62976&&e<=63743?Mr()[e]||e:173===e?45:e}(a)}this.isType3Font&&(i=a);let h=null;if(this.seacMap?.[e]){l=!0;const t=this.seacMap[e];a=t.baseFontCharCode;h={fontChar:String.fromCodePoint(t.accentFontCharCode),offset:t.accentOffset}}let u="";"number"==typeof a&&(a<=1114111?u=String.fromCodePoint(a):warn(`charToGlyph - invalid fontCharCode: ${a}`));if(this.missingFile&&this.vertical&&1===u.length){const e=Xr()[u.charCodeAt(0)];e&&(u=c=String.fromCharCode(e))}n=new fonts_Glyph(e,u,c,h,r,o,i,t,l);return this._glyphCache[e]=n}charsToGlyphs(e){let t=this._charsCache[e];if(t)return t;t=[];if(this.cMap){const a=Object.create(null),r=e.length;let i=0;for(;it.length%2==1,r=this.toUnicode instanceof IdentityToUnicodeMap?e=>this.toUnicode.charCodeOf(e):e=>this.toUnicode.charCodeOf(String.fromCodePoint(e));for(let i=0,n=e.length;i55295&&(n<57344||n>65533)&&i++;if(this.toUnicode){const e=r(n);if(-1!==e){if(hasCurrentBufErrors()){t.push(a.join(""));a.length=0}for(let t=(this.cMap?this.cMap.getCharCodeLength(e):1)-1;t>=0;t--)a.push(String.fromCharCode(e>>8*t&255));continue}}if(!hasCurrentBufErrors()){t.push(a.join(""));a.length=0}a.push(String.fromCodePoint(n))}t.push(a.join(""));return t}}class ErrorFont{constructor(e){this.error=e;this.loadedName="g_font_error";this.missingFile=!0}charsToGlyphs(){return[]}encodeString(e){return[e]}exportData(){return{error:this.error}}}const ki=2,Ci=3,vi=4,Fi=5,Ii=6,Oi=7;class Pattern{constructor(){unreachable("Cannot initialize Pattern.")}static parseShading(e,t,a,r,i,n){const s=e instanceof BaseStream?e.dict:e,o=s.get("ShadingType");try{switch(o){case ki:case Ci:return new RadialAxialShading(s,t,a,r,i,n);case vi:case Fi:case Ii:case Oi:return new MeshShading(e,t,a,r,i,n);default:throw new FormatError("Unsupported ShadingType: "+o)}}catch(e){if(e instanceof MissingDataException)throw e;warn(e);return new DummyShading}}}class BaseShading{static SMALL_NUMBER=1e-6;getIR(){unreachable("Abstract method `getIR` called.")}}class RadialAxialShading extends BaseShading{constructor(e,t,a,r,i,n){super();this.shadingType=e.get("ShadingType");let s=0;this.shadingType===ki?s=4:this.shadingType===Ci&&(s=6);this.coordsArr=e.getArray("Coords");if(!isNumberArray(this.coordsArr,s))throw new FormatError("RadialAxialShading: Invalid /Coords array.");const o=ColorSpaceUtils.parse({cs:e.getRaw("CS")||e.getRaw("ColorSpace"),xref:t,resources:a,pdfFunctionFactory:r,globalColorSpaceCache:i,localColorSpaceCache:n});this.bbox=lookupNormalRect(e.getArray("BBox"),null);let c=0,l=1;const h=e.getArray("Domain");isNumberArray(h,2)&&([c,l]=h);let u=!1,d=!1;const f=e.getArray("Extend");(function isBooleanArray(e,t){return Array.isArray(e)&&(null===t||e.length===t)&&e.every((e=>"boolean"==typeof e))})(f,2)&&([u,d]=f);if(!(this.shadingType!==Ci||u&&d)){const[e,t,a,r,i,n]=this.coordsArr,s=Math.hypot(e-r,t-i);a<=n+s&&n<=a+s&&warn("Unsupported radial gradient.")}this.extendStart=u;this.extendEnd=d;const g=e.getRaw("Function"),p=r.create(g,!0),m=(l-c)/840,b=this.colorStops=[];if(c>=l||m<=0){info("Bad shading domain.");return}const y=new Float32Array(o.numComps),w=new Float32Array(1);let S,x=0;w[0]=c;p(w,0,y,0);let k=o.getRgb(y,0);const C=Util.makeHexColor(k[0],k[1],k[2]);b.push([0,C]);let v=1;w[0]=c+m;p(w,0,y,0);let F=o.getRgb(y,0),O=F[0]-k[0]+1,T=F[1]-k[1]+1,M=F[2]-k[2]+1,D=F[0]-k[0]-1,R=F[1]-k[1]-1,N=F[2]-k[2]-1;for(let e=2;e<840;e++){w[0]=c+e*m;p(w,0,y,0);S=o.getRgb(y,0);const t=e-x;O=Math.min(O,(S[0]-k[0]+1)/t);T=Math.min(T,(S[1]-k[1]+1)/t);M=Math.min(M,(S[2]-k[2]+1)/t);D=Math.max(D,(S[0]-k[0]-1)/t);R=Math.max(R,(S[1]-k[1]-1)/t);N=Math.max(N,(S[2]-k[2]-1)/t);if(!(D<=O&&R<=T&&N<=M)){const e=Util.makeHexColor(F[0],F[1],F[2]);b.push([v/840,e]);O=S[0]-F[0]+1;T=S[1]-F[1]+1;M=S[2]-F[2]+1;D=S[0]-F[0]-1;R=S[1]-F[1]-1;N=S[2]-F[2]-1;x=v;k=F}v=e;F=S}const E=Util.makeHexColor(F[0],F[1],F[2]);b.push([1,E]);let L="transparent";if(e.has("Background")){S=o.getRgb(e.get("Background"),0);L=Util.makeHexColor(S[0],S[1],S[2])}if(!u){b.unshift([0,L]);b[1][0]+=BaseShading.SMALL_NUMBER}if(!d){b.at(-1)[0]-=BaseShading.SMALL_NUMBER;b.push([1,L])}this.colorStops=b}getIR(){const{coordsArr:e,shadingType:t}=this;let a,r,i,n,s;if(t===ki){r=[e[0],e[1]];i=[e[2],e[3]];n=null;s=null;a="axial"}else if(t===Ci){r=[e[0],e[1]];i=[e[3],e[4]];n=e[2];s=e[5];a="radial"}else unreachable(`getPattern type unknown: ${t}`);return["RadialAxial",a,this.bbox,this.colorStops,r,i,n,s]}}class MeshStreamReader{constructor(e,t){this.stream=e;this.context=t;this.buffer=0;this.bufferLength=0;const a=t.numComps;this.tmpCompsBuf=new Float32Array(a);const r=t.colorSpace.numComps;this.tmpCsCompsBuf=t.colorFn?new Float32Array(r):this.tmpCompsBuf}get hasData(){if(this.stream.end)return this.stream.pos0)return!0;const e=this.stream.getByte();if(e<0)return!1;this.buffer=e;this.bufferLength=8;return!0}readBits(e){const{stream:t}=this;let{buffer:a,bufferLength:r}=this;if(32===e){if(0===r)return t.getInt32()>>>0;a=a<<24|t.getByte()<<16|t.getByte()<<8|t.getByte();const e=t.getByte();this.buffer=e&(1<>r)>>>0}if(8===e&&0===r)return t.getByte();for(;r>r}align(){this.buffer=0;this.bufferLength=0}readFlag(){return this.readBits(this.context.bitsPerFlag)}readCoordinate(){const{bitsPerCoordinate:e,decode:t}=this.context,a=this.readBits(e),r=this.readBits(e),i=e<32?1/((1<n?n:e;t=t>s?s:t;a=ae*i[t])):a;let s,o=-2;const c=[];for(const[e,t]of r.map(((e,t)=>[e,t])).sort((([e],[t])=>e-t)))if(-1!==e)if(e===o+1){s.push(n[t]);o+=1}else{o=e;s=[n[t]];c.push(e,s)}return c}(e),a=new Dict(null);a.set("BaseFont",Name.get(e));a.set("Type",Name.get("Font"));a.set("Subtype",Name.get("CIDFontType2"));a.set("Encoding",Name.get("Identity-H"));a.set("CIDToGIDMap",Name.get("Identity"));a.set("W",t);a.set("FirstChar",t[0]);a.set("LastChar",t.at(-2)+t.at(-1).length-1);const r=new Dict(null);a.set("FontDescriptor",r);const i=new Dict(null);i.set("Ordering","Identity");i.set("Registry","Adobe");i.set("Supplement",0);a.set("CIDSystemInfo",i);return a}class PostScriptParser{constructor(e){this.lexer=e;this.operators=[];this.token=null;this.prev=null}nextToken(){this.prev=this.token;this.token=this.lexer.getToken()}accept(e){if(this.token.type===e){this.nextToken();return!0}return!1}expect(e){if(this.accept(e))return!0;throw new FormatError(`Unexpected symbol: found ${this.token.type} expected ${e}.`)}parse(){this.nextToken();this.expect(Sn.LBRACE);this.parseBlock();this.expect(Sn.RBRACE);return this.operators}parseBlock(){for(;;)if(this.accept(Sn.NUMBER))this.operators.push(this.prev.value);else if(this.accept(Sn.OPERATOR))this.operators.push(this.prev.value);else{if(!this.accept(Sn.LBRACE))return;this.parseCondition()}}parseCondition(){const e=this.operators.length;this.operators.push(null,null);this.parseBlock();this.expect(Sn.RBRACE);if(this.accept(Sn.IF)){this.operators[e]=this.operators.length;this.operators[e+1]="jz"}else{if(!this.accept(Sn.LBRACE))throw new FormatError("PS Function: error parsing conditional.");{const t=this.operators.length;this.operators.push(null,null);const a=this.operators.length;this.parseBlock();this.expect(Sn.RBRACE);this.expect(Sn.IFELSE);this.operators[t]=this.operators.length;this.operators[t+1]="j";this.operators[e]=a;this.operators[e+1]="jz"}}}}const Sn={LBRACE:0,RBRACE:1,NUMBER:2,OPERATOR:3,IF:4,IFELSE:5};class PostScriptToken{static get opCache(){return shadow(this,"opCache",Object.create(null))}constructor(e,t){this.type=e;this.value=t}static getOperator(e){return PostScriptToken.opCache[e]||=new PostScriptToken(Sn.OPERATOR,e)}static get LBRACE(){return shadow(this,"LBRACE",new PostScriptToken(Sn.LBRACE,"{"))}static get RBRACE(){return shadow(this,"RBRACE",new PostScriptToken(Sn.RBRACE,"}"))}static get IF(){return shadow(this,"IF",new PostScriptToken(Sn.IF,"IF"))}static get IFELSE(){return shadow(this,"IFELSE",new PostScriptToken(Sn.IFELSE,"IFELSE"))}}class PostScriptLexer{constructor(e){this.stream=e;this.nextChar();this.strBuf=[]}nextChar(){return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(t<0)return wa;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return new PostScriptToken(Sn.NUMBER,this.getNumber());case 123:this.nextChar();return PostScriptToken.LBRACE;case 125:this.nextChar();return PostScriptToken.RBRACE}const a=this.strBuf;a.length=0;a[0]=String.fromCharCode(t);for(;(t=this.nextChar())>=0&&(t>=65&&t<=90||t>=97&&t<=122);)a.push(String.fromCharCode(t));const r=a.join("");switch(r.toLowerCase()){case"if":return PostScriptToken.IF;case"ifelse":return PostScriptToken.IFELSE;default:return PostScriptToken.getOperator(r)}}getNumber(){let e=this.currentChar;const t=this.strBuf;t.length=0;t[0]=String.fromCharCode(e);for(;(e=this.nextChar())>=0&&(e>=48&&e<=57||45===e||46===e);)t.push(String.fromCharCode(e));const a=parseFloat(t.join(""));if(isNaN(a))throw new FormatError(`Invalid floating point number: ${a}`);return a}}class BaseLocalCache{constructor(e){this._onlyRefs=!0===e?.onlyRefs;if(!this._onlyRefs){this._nameRefMap=new Map;this._imageMap=new Map}this._imageCache=new RefSetCache}getByName(e){this._onlyRefs&&unreachable("Should not call `getByName` method.");const t=this._nameRefMap.get(e);return t?this.getByRef(t):this._imageMap.get(e)||null}getByRef(e){return this._imageCache.get(e)||null}set(e,t,a){unreachable("Abstract method `set` called.")}}class LocalImageCache extends BaseLocalCache{set(e,t=null,a){if("string"!=typeof e)throw new Error('LocalImageCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalColorSpaceCache extends BaseLocalCache{set(e=null,t=null,a){if("string"!=typeof e&&!t)throw new Error('LocalColorSpaceCache.set - expected "name" and/or "ref" argument.');if(t){if(this._imageCache.has(t))return;null!==e&&this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalFunctionCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('LocalFunctionCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class LocalGStateCache extends BaseLocalCache{set(e,t=null,a){if("string"!=typeof e)throw new Error('LocalGStateCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalTilingPatternCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('LocalTilingPatternCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class RegionalImageCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('RegionalImageCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class GlobalColorSpaceCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('GlobalColorSpaceCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}clear(){this._imageCache.clear()}}class GlobalImageCache{static NUM_PAGES_THRESHOLD=2;static MIN_IMAGES_TO_CACHE=10;static MAX_BYTE_SIZE=5e7;#X=new RefSet;constructor(){this._refCache=new RefSetCache;this._imageCache=new RefSetCache}get#q(){let e=0;for(const t of this._imageCache)e+=t.byteSize;return e}get#H(){return!(this._imageCache.size+e)):null}class PDFFunction{static getSampleArray(e,t,a,r){let i,n,s=1;for(i=0,n=e.length;i>c)*h;l&=(1<0&&(d=n[u-1]);let f=a[1];u>1,c=r.length>>1,l=new PostScriptEvaluator(s),h=Object.create(null);let u=8192;const d=new Float32Array(c);return function constructPostScriptFn(e,t,a,r){let n,s,f="";const g=d;for(n=0;ne&&(s=e)}m[n]=s}if(u>0){u--;h[f]=m}a.set(m,r)}}}function isPDFFunction(e){let t;if(e instanceof Dict)t=e;else{if(!(e instanceof BaseStream))return!1;t=e.dict}return t.has("FunctionType")}class PostScriptStack{static MAX_STACK_SIZE=100;constructor(e){this.stack=e?Array.from(e):[]}push(e){if(this.stack.length>=PostScriptStack.MAX_STACK_SIZE)throw new Error("PostScript function stack overflow.");this.stack.push(e)}pop(){if(this.stack.length<=0)throw new Error("PostScript function stack underflow.");return this.stack.pop()}copy(e){if(this.stack.length+e>=PostScriptStack.MAX_STACK_SIZE)throw new Error("PostScript function stack overflow.");const t=this.stack;for(let a=t.length-e,r=e-1;r>=0;r--,a++)t.push(t[a])}index(e){this.push(this.stack[this.stack.length-e-1])}roll(e,t){const a=this.stack,r=a.length-e,i=a.length-1,n=r+(t-Math.floor(t/e)*e);for(let e=r,t=i;e0?t.push(s<>o);break;case"ceiling":s=t.pop();t.push(Math.ceil(s));break;case"copy":s=t.pop();t.copy(s);break;case"cos":s=t.pop();t.push(Math.cos(s%360/180*Math.PI));break;case"cvi":s=0|t.pop();t.push(s);break;case"cvr":break;case"div":o=t.pop();s=t.pop();t.push(s/o);break;case"dup":t.copy(1);break;case"eq":o=t.pop();s=t.pop();t.push(s===o);break;case"exch":t.roll(2,1);break;case"exp":o=t.pop();s=t.pop();t.push(s**o);break;case"false":t.push(!1);break;case"floor":s=t.pop();t.push(Math.floor(s));break;case"ge":o=t.pop();s=t.pop();t.push(s>=o);break;case"gt":o=t.pop();s=t.pop();t.push(s>o);break;case"idiv":o=t.pop();s=t.pop();t.push(s/o|0);break;case"index":s=t.pop();t.index(s);break;case"le":o=t.pop();s=t.pop();t.push(s<=o);break;case"ln":s=t.pop();t.push(Math.log(s));break;case"log":s=t.pop();t.push(Math.log10(s));break;case"lt":o=t.pop();s=t.pop();t.push(s=t?new AstLiteral(t):e.max<=t?e:new AstMin(e,t)}class PostScriptCompiler{compile(e,t,a){const r=[],i=[],n=t.length>>1,s=a.length>>1;let o,c,l,h,u,d,f,g,p=0;for(let e=0;et.min){o.unshift("Math.max(",n,", ");o.push(")")}if(s4){r=!0;t=0}else{r=!1;t=1}const c=[];for(n=0;n=0&&"ET"===Cn[e];--e)Cn[e]="EN";for(let e=n+1;e0&&(t=Cn[n-1]);let a=u;e+1g&&isOdd(g)&&(m=g)}for(g=p;g>=m;--g){let e=-1;for(n=0,s=c.length;n=0){reverseValues(kn,e,n);e=-1}}else e<0&&(e=n);e>=0&&reverseValues(kn,e,c.length)}for(n=0,s=kn.length;n"!==e||(kn[n]="")}return createBidiText(kn.join(""),r)}const vn={style:"normal",weight:"normal"},Fn={style:"normal",weight:"bold"},In={style:"italic",weight:"normal"},On={style:"italic",weight:"bold"},Tn=new Map([["Times-Roman",{local:["Times New Roman","Times-Roman","Times","Liberation Serif","Nimbus Roman","Nimbus Roman L","Tinos","Thorndale","TeX Gyre Termes","FreeSerif","Linux Libertine O","Libertinus Serif","DejaVu Serif","Bitstream Vera Serif","Ubuntu"],style:vn,ultimate:"serif"}],["Times-Bold",{alias:"Times-Roman",style:Fn,ultimate:"serif"}],["Times-Italic",{alias:"Times-Roman",style:In,ultimate:"serif"}],["Times-BoldItalic",{alias:"Times-Roman",style:On,ultimate:"serif"}],["Helvetica",{local:["Helvetica","Helvetica Neue","Arial","Arial Nova","Liberation Sans","Arimo","Nimbus Sans","Nimbus Sans L","A030","TeX Gyre Heros","FreeSans","DejaVu Sans","Albany","Bitstream Vera Sans","Arial Unicode MS","Microsoft Sans Serif","Apple Symbols","Cantarell"],path:"LiberationSans-Regular.ttf",style:vn,ultimate:"sans-serif"}],["Helvetica-Bold",{alias:"Helvetica",path:"LiberationSans-Bold.ttf",style:Fn,ultimate:"sans-serif"}],["Helvetica-Oblique",{alias:"Helvetica",path:"LiberationSans-Italic.ttf",style:In,ultimate:"sans-serif"}],["Helvetica-BoldOblique",{alias:"Helvetica",path:"LiberationSans-BoldItalic.ttf",style:On,ultimate:"sans-serif"}],["Courier",{local:["Courier","Courier New","Liberation Mono","Nimbus Mono","Nimbus Mono L","Cousine","Cumberland","TeX Gyre Cursor","FreeMono","Linux Libertine Mono O","Libertinus Mono"],style:vn,ultimate:"monospace"}],["Courier-Bold",{alias:"Courier",style:Fn,ultimate:"monospace"}],["Courier-Oblique",{alias:"Courier",style:In,ultimate:"monospace"}],["Courier-BoldOblique",{alias:"Courier",style:On,ultimate:"monospace"}],["ArialBlack",{local:["Arial Black"],style:{style:"normal",weight:"900"},fallback:"Helvetica-Bold"}],["ArialBlack-Bold",{alias:"ArialBlack"}],["ArialBlack-Italic",{alias:"ArialBlack",style:{style:"italic",weight:"900"},fallback:"Helvetica-BoldOblique"}],["ArialBlack-BoldItalic",{alias:"ArialBlack-Italic"}],["ArialNarrow",{local:["Arial Narrow","Liberation Sans Narrow","Helvetica Condensed","Nimbus Sans Narrow","TeX Gyre Heros Cn"],style:vn,fallback:"Helvetica"}],["ArialNarrow-Bold",{alias:"ArialNarrow",style:Fn,fallback:"Helvetica-Bold"}],["ArialNarrow-Italic",{alias:"ArialNarrow",style:In,fallback:"Helvetica-Oblique"}],["ArialNarrow-BoldItalic",{alias:"ArialNarrow",style:On,fallback:"Helvetica-BoldOblique"}],["Calibri",{local:["Calibri","Carlito"],style:vn,fallback:"Helvetica"}],["Calibri-Bold",{alias:"Calibri",style:Fn,fallback:"Helvetica-Bold"}],["Calibri-Italic",{alias:"Calibri",style:In,fallback:"Helvetica-Oblique"}],["Calibri-BoldItalic",{alias:"Calibri",style:On,fallback:"Helvetica-BoldOblique"}],["Wingdings",{local:["Wingdings","URW Dingbats"],style:vn}],["Wingdings-Regular",{alias:"Wingdings"}],["Wingdings-Bold",{alias:"Wingdings"}]]),Mn=new Map([["Arial-Black","ArialBlack"]]);function getFamilyName(e){const t=new Set(["thin","extralight","ultralight","demilight","semilight","light","book","regular","normal","medium","demibold","semibold","bold","extrabold","ultrabold","black","heavy","extrablack","ultrablack","roman","italic","oblique","ultracondensed","extracondensed","condensed","semicondensed","normal","semiexpanded","expanded","extraexpanded","ultraexpanded","bolditalic"]);return e.split(/[- ,+]+/g).filter((e=>!t.has(e.toLowerCase()))).join(" ")}function generateFont({alias:e,local:t,path:a,fallback:r,style:i,ultimate:n},s,o,c=!0,l=!0,h=""){const u={style:null,ultimate:null};if(t){const e=h?` ${h}`:"";for(const a of t)s.push(`local(${a}${e})`)}if(e){const t=Tn.get(e),n=h||function getStyleToAppend(e){switch(e){case Fn:return"Bold";case In:return"Italic";case On:return"Bold Italic";default:if("bold"===e?.weight)return"Bold";if("italic"===e?.style)return"Italic"}return""}(i);Object.assign(u,generateFont(t,s,o,c&&!r,l&&!a,n))}i&&(u.style=i);n&&(u.ultimate=n);if(c&&r){const e=Tn.get(r),{ultimate:t}=generateFont(e,s,o,c,l&&!a,h);u.ultimate||=t}l&&a&&o&&s.push(`url(${o}${a})`);return u}function getFontSubstitution(e,t,a,r,i,n){if(r.startsWith("InvalidPDFjsFont_"))return null;"TrueType"!==n&&"Type1"!==n||!/^[A-Z]{6}\+/.test(r)||(r=r.slice(7));const s=r=normalizeFontName(r);let o=e.get(s);if(o)return o;let c=Tn.get(r);if(!c)for(const[e,t]of Mn)if(r.startsWith(e)){r=`${t}${r.substring(e.length)}`;c=Tn.get(r);break}let l=!1;if(!c){c=Tn.get(i);l=!0}const h=`${t.getDocId()}_s${t.createFontId()}`;if(!c){if(!validateFontName(r)){warn(`Cannot substitute the font because of its name: ${r}`);e.set(s,null);return null}const t=/bold/gi.test(r),a=/oblique|italic/gi.test(r),i=t&&a&&On||t&&Fn||a&&In||vn;o={css:`"${getFamilyName(r)}",${h}`,guessFallback:!0,loadedName:h,baseFontName:r,src:`local(${r})`,style:i};e.set(s,o);return o}const u=[];l&&validateFontName(r)&&u.push(`local(${r})`);const{style:d,ultimate:f}=generateFont(c,u,a),g=null===f,p=g?"":`,${f}`;o={css:`"${getFamilyName(r)}",${h}${p}`,guessFallback:g,loadedName:h,baseFontName:r,src:u.join(","),style:d};e.set(s,o);return o}const Dn=3285377520,Bn=4294901760,Rn=65535;class MurmurHash3_64{constructor(e){this.h1=e?4294967295&e:Dn;this.h2=e?4294967295&e:Dn}update(e){let t,a;if("string"==typeof e){t=new Uint8Array(2*e.length);a=0;for(let r=0,i=e.length;r>>8;t[a++]=255&i}}}else{if(!ArrayBuffer.isView(e))throw new Error("Invalid data format, must be a string or TypedArray.");t=e.slice();a=t.byteLength}const r=a>>2,i=a-4*r,n=new Uint32Array(t.buffer,0,r);let s=0,o=0,c=this.h1,l=this.h2;const h=3432918353,u=461845907,d=11601,f=13715;for(let e=0;e>>17;s=s*u&Bn|s*f&Rn;c^=s;c=c<<13|c>>>19;c=5*c+3864292196}else{o=n[e];o=o*h&Bn|o*d&Rn;o=o<<15|o>>>17;o=o*u&Bn|o*f&Rn;l^=o;l=l<<13|l>>>19;l=5*l+3864292196}s=0;switch(i){case 3:s^=t[4*r+2]<<16;case 2:s^=t[4*r+1]<<8;case 1:s^=t[4*r];s=s*h&Bn|s*d&Rn;s=s<<15|s>>>17;s=s*u&Bn|s*f&Rn;1&r?c^=s:l^=s}this.h1=c;this.h2=l}hexdigest(){let e=this.h1,t=this.h2;e^=t>>>1;e=3981806797*e&Bn|36045*e&Rn;t=4283543511*t&Bn|(2950163797*(t<<16|e>>>16)&Bn)>>>16;e^=t>>>1;e=444984403*e&Bn|60499*e&Rn;t=3301882366*t&Bn|(3120437893*(t<<16|e>>>16)&Bn)>>>16;e^=t>>>1;return(e>>>0).toString(16).padStart(8,"0")+(t>>>0).toString(16).padStart(8,"0")}}function resizeImageMask(e,t,a,r,i,n){const s=i*n;let o;o=t<=8?new Uint8Array(s):t<=16?new Uint16Array(s):new Uint32Array(s);const c=a/i,l=r/n;let h,u,d,f,g=0;const p=new Uint16Array(i),m=a;for(h=0;h0&&Number.isInteger(a.height)&&a.height>0&&(a.width!==f||a.height!==g)){warn("PDFImage - using the Width/Height of the image data, rather than the image dictionary.");f=a.width;g=a.height}else{const e="number"==typeof f&&f>0,t="number"==typeof g&&g>0;if(!e||!t){if(!a.fallbackDims)throw new FormatError(`Invalid image width: ${f} or height: ${g}`);warn("PDFImage - using the Width/Height of the parent image, for SMask/Mask data.");e||(f=a.fallbackDims.width);t||(g=a.fallbackDims.height)}}this.width=f;this.height=g;this.interpolate=h.get("I","Interpolate");this.imageMask=h.get("IM","ImageMask")||!1;this.matte=h.get("Matte")||!1;let p=a.bitsPerComponent;if(!p){p=h.get("BPC","BitsPerComponent");if(!p){if(!this.imageMask)throw new FormatError(`Bits per component missing in image: ${this.imageMask}`);p=1}}this.bpc=p;if(!this.imageMask){let i=h.getRaw("CS")||h.getRaw("ColorSpace");const n=!!i;if(n)this.jpxDecoderOptions?.smaskInData&&(i=Name.get("DeviceRGBA"));else if(this.jpxDecoderOptions)i=Name.get("DeviceRGBA");else switch(a.numComps){case 1:i=Name.get("DeviceGray");break;case 3:i=Name.get("DeviceRGB");break;case 4:i=Name.get("DeviceCMYK");break;default:throw new Error(`Images with ${a.numComps} color components not supported.`)}this.colorSpace=ColorSpaceUtils.parse({cs:i,xref:e,resources:r?t:null,pdfFunctionFactory:o,globalColorSpaceCache:c,localColorSpaceCache:l});this.numComps=this.colorSpace.numComps;if(this.jpxDecoderOptions){this.jpxDecoderOptions.numComponents=n?this.numComps:0;this.jpxDecoderOptions.isIndexedColormap="Indexed"===this.colorSpace.name}}this.decode=h.getArray("D","Decode");this.needsDecode=!1;if(this.decode&&(this.colorSpace&&!this.colorSpace.isDefaultDecode(this.decode,p)||s&&!ColorSpace.isDefaultDecode(this.decode,1))){this.needsDecode=!0;const e=(1<0,c=(r+7>>3)*i,l=e.getBytes(c),h=1===r&&1===i&&o===(0===l.length||!!(128&l[0]));if(h)return{isSingleOpaquePixel:h};if(t){if(ImageResizer.needsToBeResized(r,i)){const e=new Uint8ClampedArray(r*i*4);convertBlackAndWhiteToRGBA({src:l,dest:e,width:r,height:i,nonBlackColor:0,inverseDecode:o});return ImageResizer.createImage({kind:v,data:e,width:r,height:i,interpolate:n})}const e=new OffscreenCanvas(r,i),t=e.getContext("2d"),a=t.createImageData(r,i);convertBlackAndWhiteToRGBA({src:l,dest:a.data,width:r,height:i,nonBlackColor:0,inverseDecode:o});t.putImageData(a,0,0);return{data:null,width:r,height:i,interpolate:n,bitmap:e.transferToImageBitmap()}}const u=l.byteLength;let d;if(e instanceof DecodeStream&&(!o||c===u))d=l;else if(o){d=new Uint8Array(c);d.set(l);d.fill(255,u)}else d=new Uint8Array(l);if(o)for(let e=0;e>7&1;s[d+1]=u>>6&1;s[d+2]=u>>5&1;s[d+3]=u>>4&1;s[d+4]=u>>3&1;s[d+5]=u>>2&1;s[d+6]=u>>1&1;s[d+7]=1&u;d+=8}if(d>=1}}}}else{let a=0;u=0;for(d=0,h=n;d>r;i<0?i=0:i>l&&(i=l);s[d]=i;u&=(1<s[r+1]){t=255;break}}o[h]=t}}}if(o)for(h=0,d=3,u=t*r;h>3,h=t&&ImageResizer.needsToBeResized(a,r);if(!this.smask&&!this.mask&&"DeviceRGBA"===this.colorSpace.name){i.kind=v;const e=i.data=await this.getImageBytes(o*s*4,{});return t?h?ImageResizer.createImage(i,!1):this.createBitmap(v,a,r,e):i}if(!e){let e;"DeviceGray"===this.colorSpace.name&&1===c?e=k:"DeviceRGB"!==this.colorSpace.name||8!==c||this.needsDecode||(e=C);if(e&&!this.smask&&!this.mask&&a===s&&r===o){const n=await this.#W(s,o);if(n)return n;const c=await this.getImageBytes(o*l,{});if(t)return h?ImageResizer.createImage({data:c,kind:e,width:a,height:r,interpolate:this.interpolate},this.needsDecode):this.createBitmap(e,s,o,c);i.kind=e;i.data=c;if(this.needsDecode){assert(e===k,"PDFImage.createImageData: The image must be grayscale.");const t=i.data;for(let e=0,a=t.length;e>3,s=await this.getImageBytes(r*n,{internal:!0}),o=this.getComponents(s);let c,l;if(1===i){l=a*r;if(this.needsDecode)for(c=0;c0&&r[0].count++}class TimeSlotManager{static TIME_SLOT_DURATION_MS=20;static CHECK_TIME_EVERY=100;constructor(){this.reset()}check(){if(++this.checkedo){const e="Image exceeded maximum allowed size and was removed.";if(!c)throw new Error(e);warn(e);return}let g;h.has("OC")&&(g=await this.parseMarkedContentProps(h.get("OC"),e));let p,m,b;if(h.get("IM","ImageMask")||!1){p=await PDFImage.createMask({image:t,isOffscreenCanvasSupported:l&&!this.parsingType3Font});if(p.isSingleOpaquePixel){m=ta;b=[];r.addImageOps(m,b,g);if(i){const e={fn:m,args:b,optionalContent:g};n.set(i,u,e);u&&this._regionalImageCache.set(null,u,e)}return}if(this.parsingType3Font){b=function compileType3Glyph({data:e,width:t,height:a}){if(t>1e3||a>1e3)return null;const r=new Uint8Array([0,2,4,0,1,0,5,4,8,10,0,8,0,2,1,0]),i=t+1,n=new Uint8Array(i*(a+1));let s,o,c;const l=t+7&-8,h=new Uint8Array(l*a);let u=0;for(const t of e){let e=128;for(;e>0;){h[u++]=t&e?0:255;e>>=1}}let d=0;u=0;if(0!==h[u]){n[0]=1;++d}for(o=1;o>2)+(h[u+1]?4:0)+(h[u-l+1]?8:0);if(r[e]){n[c+o]=r[e];++d}u++}if(h[u-l]!==h[u]){n[c+o]=h[u]?2:4;++d}if(d>1e3)return null}u=l*(a-1);c=s*i;if(0!==h[u]){n[c]=8;++d}for(o=1;o1e3)return null;const f=new Int32Array([0,i,-1,0,-i,0,0,0,1]),g=[],{a:p,b:m,c:b,d:y,e:w,f:S}=(new DOMMatrix).scaleSelf(1/t,-1/a).translateSelf(0,-a);for(s=0;d&&s<=a;s++){let e=s*i;const a=e+t;for(;e>4;n[e]&=l>>2|l<<2}r=e%i;o=e/i|0;g.push(oa,p*r+b*o+w,m*r+y*o+S);n[e]||--d}while(c!==e);--s}return[na,[new Float32Array(g)],new Float32Array([0,0,t,a])]}(p);if(b){r.addImageOps(aa,b,g);return}warn("Cannot compile Type3 glyph.");r.addImageOps(Vt,[p],g);return}const e=`mask_${this.idFactory.createObjId()}`;r.addDependency(e);p.dataLen=p.bitmap?p.width*p.height*4:p.data.length;this._sendImgData(e,p);m=Vt;b=[{data:e,width:p.width,height:p.height,interpolate:p.interpolate,count:1}];r.addImageOps(m,b,g);if(i){const t={objId:e,fn:m,args:b,optionalContent:g};n.set(i,u,t);u&&this._regionalImageCache.set(null,u,t)}return}const y=h.has("SMask")||h.has("Mask");if(a&&d+f<200&&!y){try{const i=new PDFImage({xref:this.xref,res:e,image:t,isInline:a,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:s});p=await i.createImageData(!0,!1);r.addImageOps(Yt,[p],g)}catch(e){const t=`Unable to decode inline image: "${e}".`;if(!c)throw new Error(t);warn(t)}return}let w=`img_${this.idFactory.createObjId()}`,S=!1,x=null;if(this.parsingType3Font)w=`${this.idFactory.getDocId()}_type3_${w}`;else if(i&&u){S=this.globalImageCache.shouldCache(u,this.pageIndex);if(S){assert(!a,"Cannot cache an inline image globally.");w=`${this.idFactory.getDocId()}_${w}`}}r.addDependency(w);m=Jt;b=[w,d,f];r.addImageOps(m,b,g,y);if(S){x={objId:w,fn:m,args:b,optionalContent:g,hasMask:y,byteSize:0};if(this.globalImageCache.hasDecodeFailed(u)){this.globalImageCache.setData(u,x);this._sendImgData(w,null,S);return}if(d*f>25e4||y){const e=await this.handler.sendWithPromise("commonobj",[w,"CopyLocalImage",{imageRef:u}]);if(e){this.globalImageCache.setData(u,x);this.globalImageCache.addByteSize(u,e);return}}}PDFImage.buildImage({xref:this.xref,res:e,image:t,isInline:a,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:s}).then((async e=>{p=await e.createImageData(!1,l);p.dataLen=p.bitmap?p.width*p.height*4:p.data.length;p.ref=u;S&&this.globalImageCache.addByteSize(u,p.dataLen);return this._sendImgData(w,p,S)})).catch((e=>{warn(`Unable to decode image "${w}": "${e}".`);u&&this.globalImageCache.addDecodeFailed(u);return this._sendImgData(w,null,S)}));if(i){const e={objId:w,fn:m,args:b,optionalContent:g,hasMask:y};n.set(i,u,e);if(u){this._regionalImageCache.set(null,u,e);if(S){assert(x,"The global cache-data must be available.");this.globalImageCache.setData(u,x)}}}}handleSMask(e,t,a,r,i,n,s){const o=e.get("G"),c={subtype:e.get("S").name,backdrop:e.get("BC")},l=e.get("TR");if(isPDFFunction(l)){const e=this._pdfFunctionFactory.create(l),t=new Uint8Array(256),a=new Float32Array(1);for(let r=0;r<256;r++){a[0]=r/255;e(a,0,a,0);t[r]=255*a[0]|0}c.transferMap=t}return this.buildFormXObject(t,o,c,a,r,i.state.clone({newPath:!0}),n,s)}handleTransferFunction(e){let t;if(Array.isArray(e))t=e;else{if(!isPDFFunction(e))return null;t=[e]}const a=[];let r=0,i=0;for(const e of t){const t=this.xref.fetchIfRef(e);r++;if(isName(t,"Identity")){a.push(null);continue}if(!isPDFFunction(t))return null;const n=this._pdfFunctionFactory.create(t),s=new Uint8Array(256),o=new Float32Array(1);for(let e=0;e<256;e++){o[0]=e/255;n(o,0,o,0);s[e]=255*o[0]|0}a.push(s);i++}return 1!==r&&4!==r||0===i?null:a}handleTilingType(e,t,a,r,i,n,s,o){const c=new OperatorList,l=Dict.merge({xref:this.xref,dictArray:[i.get("Resources"),a]});return this.getOperatorList({stream:r,task:s,resources:l,operatorList:c}).then((function(){const a=c.getIR(),r=getTilingPatternIR(a,i,t);n.addDependencies(c.dependencies);n.addOp(e,r);i.objId&&o.set(null,i.objId,{operatorListIR:a,dict:i})})).catch((e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`handleTilingType - ignoring pattern: "${e}".`)}}))}async handleSetFont(e,t,a,r,i,n,s=null,o=null){const c=t?.[0]instanceof Name?t[0].name:null,l=await this.loadFont(c,a,e,i,s,o);l.font.isType3Font&&r.addDependencies(l.type3Dependencies);n.font=l.font;l.send(this.handler);return l.loadedName}handleText(e,t){const a=t.font,r=a.charsToGlyphs(e);if(a.data){(!!(t.textRenderingMode&x)||"Pattern"===t.fillColorSpace.name||a.disableFontFace)&&PartialEvaluator.buildFontPaths(a,r,this.handler,this.options)}return r}ensureStateFont(e){if(e.font)return;const t=new FormatError("Missing setFont (Tf) operator before text rendering operator.");if(!this.options.ignoreErrors)throw t;warn(`ensureStateFont: "${t}".`)}async setGState({resources:e,gState:t,operatorList:a,cacheKey:r,task:i,stateManager:n,localGStateCache:s,localColorSpaceCache:o,seenRefs:c}){const l=t.objId;let h=!0;const u=[];let d=Promise.resolve();for(const[r,s]of t)switch(r){case"Type":break;case"LW":if("number"!=typeof s){warn(`Invalid LW (line width): ${s}`);break}u.push([r,Math.abs(s)]);break;case"LC":case"LJ":case"ML":case"D":case"RI":case"FL":case"CA":case"ca":u.push([r,s]);break;case"Font":h=!1;d=d.then((()=>this.handleSetFont(e,null,s[0],a,i,n.state).then((function(e){a.addDependency(e);u.push([r,[e,s[1]]])}))));break;case"BM":u.push([r,normalizeBlendMode(s)]);break;case"SMask":if(isName(s,"None")){u.push([r,!1]);break}if(s instanceof Dict){h=!1;d=d.then((()=>this.handleSMask(s,e,a,i,n,o,c)));u.push([r,!0])}else warn("Unsupported SMask type");break;case"TR":const t=this.handleTransferFunction(s);u.push([r,t]);break;case"OP":case"op":case"OPM":case"BG":case"BG2":case"UCR":case"UCR2":case"TR2":case"HT":case"SM":case"SA":case"AIS":case"TK":info("graphic state operator "+r);break;default:info("Unknown graphic state operator "+r)}await d;u.length>0&&a.addOp(De,[u]);h&&s.set(r,l,u)}loadFont(e,t,a,r,i=null,n=null){const errorFont=async()=>new TranslatedFont({loadedName:"g_font_error",font:new ErrorFont(`Font "${e}" is not available.`),dict:t});let s;if(t)t instanceof Ref&&(s=t);else{const t=a.get("Font");t&&(s=t.getRaw(e))}if(s){if(this.type3FontRefs?.has(s))return errorFont();if(this.fontCache.has(s))return this.fontCache.get(s);try{t=this.xref.fetchIfRef(s)}catch(e){warn(`loadFont - lookup failed: "${e}".`)}}if(!(t instanceof Dict)){if(!this.options.ignoreErrors&&!this.parsingType3Font){warn(`Font "${e}" is not available.`);return errorFont()}warn(`Font "${e}" is not available -- attempting to fallback to a default font.`);t=i||PartialEvaluator.fallbackFontDict}if(t.cacheKey&&this.fontCache.has(t.cacheKey))return this.fontCache.get(t.cacheKey);const{promise:o,resolve:c}=Promise.withResolvers();let l;try{l=this.preEvaluateFont(t);l.cssFontInfo=n}catch(e){warn(`loadFont - preEvaluateFont failed: "${e}".`);return errorFont()}const{descriptor:h,hash:u}=l,d=s instanceof Ref;let f;if(u&&h instanceof Dict){const e=h.fontAliases||=Object.create(null);if(e[u]){const t=e[u].aliasRef;if(d&&t&&this.fontCache.has(t)){this.fontCache.putAlias(s,t);return this.fontCache.get(s)}}else e[u]={fontID:this.idFactory.createFontId()};d&&(e[u].aliasRef=s);f=e[u].fontID}else f=this.idFactory.createFontId();assert(f?.startsWith("f"),'The "fontID" must be (correctly) defined.');if(d)this.fontCache.put(s,o);else{t.cacheKey=`cacheKey_${f}`;this.fontCache.put(t.cacheKey,o)}t.loadedName=`${this.idFactory.getDocId()}_${f}`;this.translateFont(l).then((async e=>{const i=new TranslatedFont({loadedName:t.loadedName,font:e,dict:t});if(e.isType3Font)try{await i.loadType3Data(this,a,r)}catch(e){throw new Error(`Type3 font load error: ${e}`)}c(i)})).catch((e=>{warn(`loadFont - translateFont failed: "${e}".`);c(new TranslatedFont({loadedName:t.loadedName,font:new ErrorFont(e?.message),dict:t}))}));return o}buildPath(e,t,a){const{pathMinMax:r,pathBuffer:i}=a;switch(0|e){case Xe:{const e=a.currentPointX=t[0],n=a.currentPointY=t[1],s=t[2],o=t[3],c=e+s,l=n+o;0===s||0===o?i.push(sa,e,n,oa,c,l,la):i.push(sa,e,n,oa,c,n,oa,c,l,oa,e,l,la);Util.rectBoundingBox(e,n,c,l,r);break}case Ee:{const e=a.currentPointX=t[0],n=a.currentPointY=t[1];i.push(sa,e,n);Util.pointBoundingBox(e,n,r);break}case Pe:{const e=a.currentPointX=t[0],n=a.currentPointY=t[1];i.push(oa,e,n);Util.pointBoundingBox(e,n,r);break}case Le:{const e=a.currentPointX,n=a.currentPointY,[s,o,c,l,h,u]=t;a.currentPointX=h;a.currentPointY=u;i.push(ca,s,o,c,l,h,u);Util.bezierBoundingBox(e,n,s,o,c,l,h,u,r);break}case _e:{const e=a.currentPointX,n=a.currentPointY,[s,o,c,l]=t;a.currentPointX=c;a.currentPointY=l;i.push(ca,e,n,s,o,c,l);Util.bezierBoundingBox(e,n,e,n,s,o,c,l,r);break}case je:{const e=a.currentPointX,n=a.currentPointY,[s,o,c,l]=t;a.currentPointX=c;a.currentPointY=l;i.push(ca,s,o,c,l,c,l);Util.bezierBoundingBox(e,n,s,o,c,l,c,l,r);break}case Ue:i.push(la)}}_getColorSpace(e,t,a){return ColorSpaceUtils.parse({cs:e,xref:this.xref,resources:t,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:a,asyncIfNotCached:!0})}async _handleColorSpace(e){try{return await e}catch(e){if(e instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`_handleColorSpace - ignoring ColorSpace: "${e}".`);return null}throw e}}parseShading({shading:e,resources:t,localColorSpaceCache:a,localShadingPatternCache:r}){let i,n=r.get(e);if(n)return n;try{i=Pattern.parseShading(e,this.xref,t,this._pdfFunctionFactory,this.globalColorSpaceCache,a).getIR()}catch(t){if(t instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`parseShading - ignoring shading: "${t}".`);r.set(e,null);return null}throw t}n=`pattern_${this.idFactory.createObjId()}`;this.parsingType3Font&&(n=`${this.idFactory.getDocId()}_type3_${n}`);r.set(e,n);this.parsingType3Font?this.handler.send("commonobj",[n,"Pattern",i]):this.handler.send("obj",[n,this.pageIndex,"Pattern",i]);return n}handleColorN(e,t,a,r,i,n,s,o,c,l){const h=a.pop();if(h instanceof Name){const u=i.getRaw(h.name),d=u instanceof Ref&&c.getByRef(u);if(d)try{const i=r.base?r.base.getRgb(a,0):null,n=getTilingPatternIR(d.operatorListIR,d.dict,i);e.addOp(t,n);return}catch{}const f=this.xref.fetchIfRef(u);if(f){const i=f instanceof BaseStream?f.dict:f,h=i.get("PatternType");if(h===En){const o=r.base?r.base.getRgb(a,0):null;return this.handleTilingType(t,o,n,f,i,e,s,c)}if(h===Pn){const a=i.get("Shading"),r=this.parseShading({shading:a,resources:n,localColorSpaceCache:o,localShadingPatternCache:l});if(r){const a=lookupMatrix(i.getArray("Matrix"),null);e.addOp(t,["Shading",r,a])}return}throw new FormatError(`Unknown PatternType: ${h}`)}}throw new FormatError(`Unknown PatternName: ${h}`)}_parseVisibilityExpression(e,t,a){if(++t>10){warn("Visibility expression is too deeply nested");return}const r=e.length,i=this.xref.fetchIfRef(e[0]);if(!(r<2)&&i instanceof Name){switch(i.name){case"And":case"Or":case"Not":a.push(i.name);break;default:warn(`Invalid operator ${i.name} in visibility expression`);return}for(let i=1;i0)return{type:"OCMD",expression:t}}const t=a.get("OCGs");if(Array.isArray(t)||t instanceof Dict){const e=[];if(Array.isArray(t))for(const a of t)e.push(a.toString());else e.push(t.objId);return{type:r,ids:e,policy:a.get("P")instanceof Name?a.get("P").name:null,expression:null}}if(t instanceof Ref)return{type:r,id:t.toString()}}return null}getOperatorList({stream:e,task:t,resources:a,operatorList:r,initialState:i=null,fallbackFontDict:n=null,prevRefs:s=null}){const o=e.dict?.objId,c=new RefSet(s);if(o){if(s?.has(o))throw new Error(`getOperatorList - ignoring circular reference: ${o}`);c.put(o)}a||=Dict.empty;i||=new EvalState;if(!r)throw new Error('getOperatorList: missing "operatorList" parameter');const l=this,h=this.xref,u=new LocalImageCache,d=new LocalColorSpaceCache,f=new LocalGStateCache,g=new LocalTilingPatternCache,p=new Map,m=a.get("XObject")||Dict.empty,b=a.get("Pattern")||Dict.empty,y=new StateManager(i),w=new EvaluatorPreprocessor(e,h,y),S=new TimeSlotManager;function closePendingRestoreOPS(e){for(let e=0,t=w.savedStatesDepth;e{y.state.fillColorSpace=e||ColorSpaceUtils.gray})));return}case yt:{const t=l._getColorSpace(e[0],a,d);if(t instanceof ColorSpace){y.state.strokeColorSpace=t;continue}next(l._handleColorSpace(t).then((e=>{y.state.strokeColorSpace=e||ColorSpaceUtils.gray})));return}case At:C=y.state.fillColorSpace;e=C.getRgb(e,0);i=It;break;case St:C=y.state.strokeColorSpace;e=C.getRgb(e,0);i=Ft;break;case vt:y.state.fillColorSpace=ColorSpaceUtils.gray;e=ColorSpaceUtils.gray.getRgb(e,0);i=It;break;case Ct:y.state.strokeColorSpace=ColorSpaceUtils.gray;e=ColorSpaceUtils.gray.getRgb(e,0);i=Ft;break;case Tt:y.state.fillColorSpace=ColorSpaceUtils.cmyk;e=ColorSpaceUtils.cmyk.getRgb(e,0);i=It;break;case Ot:y.state.strokeColorSpace=ColorSpaceUtils.cmyk;e=ColorSpaceUtils.cmyk.getRgb(e,0);i=Ft;break;case It:y.state.fillColorSpace=ColorSpaceUtils.rgb;e=ColorSpaceUtils.rgb.getRgb(e,0);break;case Ft:y.state.strokeColorSpace=ColorSpaceUtils.rgb;e=ColorSpaceUtils.rgb.getRgb(e,0);break;case kt:C=y.state.patternFillColorSpace;if(!C){if(isNumberArray(e,null)){e=ColorSpaceUtils.gray.getRgb(e,0);i=It;break}e=[];i=ia;break}if("Pattern"===C.name){next(l.handleColorN(r,kt,e,C,b,a,t,d,g,p));return}e=C.getRgb(e,0);i=It;break;case xt:C=y.state.patternStrokeColorSpace;if(!C){if(isNumberArray(e,null)){e=ColorSpaceUtils.gray.getRgb(e,0);i=Ft;break}e=[];i=ra;break}if("Pattern"===C.name){next(l.handleColorN(r,xt,e,C,b,a,t,d,g,p));return}e=C.getRgb(e,0);i=Ft;break;case Mt:let O;try{const t=a.get("Shading");if(!t)throw new FormatError("No shading resource found");O=t.get(e[0].name);if(!O)throw new FormatError("No shading object found")}catch(e){if(e instanceof AbortException)continue;if(l.options.ignoreErrors){warn(`getOperatorList - ignoring Shading: "${e}".`);continue}throw e}const T=l.parseShading({shading:O,resources:a,localColorSpaceCache:d,localShadingPatternCache:p});if(!T)continue;e=[T];i=Mt;break;case De:F=e[0]instanceof Name;v=e[0].name;if(F){const t=f.getByName(v);if(t){t.length>0&&r.addOp(De,[t]);e=null;continue}}next(new Promise((function(e,i){if(!F)throw new FormatError("GState must be referred to by name.");const n=a.get("ExtGState");if(!(n instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const s=n.get(v);if(!(s instanceof Dict))throw new FormatError("GState should be a dictionary.");l.setGState({resources:a,gState:s,operatorList:r,cacheKey:v,task:t,stateManager:y,localGStateCache:f,localColorSpaceCache:d,seenRefs:c}).then(e,i)})).catch((function(e){if(!(e instanceof AbortException)){if(!l.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring ExtGState: "${e}".`)}})));return;case Ce:{const[t]=e;if("number"!=typeof t){warn(`Invalid setLineWidth: ${t}`);continue}e[0]=Math.abs(t);break}case Ee:case Pe:case Le:case _e:case je:case Ue:case Xe:l.buildPath(i,e,y.state);continue;case qe:case He:case We:case ze:case $e:case Ge:case Ve:case Ke:case Je:{const{state:{pathBuffer:e,pathMinMax:t}}=y;i!==He&&i!==Ve&&i!==Ke||e.push(la);if(0===e.length)r.addOp(aa,[i,[null],null]);else{r.addOp(aa,[i,[new Float32Array(e)],t.slice()]);e.length=0;t.set([1/0,1/0,-1/0,-1/0],0)}continue}case ht:r.addOp(i,[new Float32Array(e)]);continue;case Et:case Pt:case Ut:case Xt:continue;case _t:if(!(e[0]instanceof Name)){warn(`Expected name for beginMarkedContentProps arg0=${e[0]}`);r.addOp(_t,["OC",null]);continue}if("OC"===e[0].name){next(l.parseMarkedContentProps(e[1],a).then((e=>{r.addOp(_t,["OC",e])})).catch((e=>{if(!(e instanceof AbortException)){if(!l.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring beginMarkedContentProps: "${e}".`);r.addOp(_t,["OC",null])}})));return}e=[e[0].name,e[1]instanceof Dict?e[1].get("MCID"):null];break;default:if(null!==e){for(x=0,k=e.length;x{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring errors during "${t.name}" task: "${e}".`);closePendingRestoreOPS()}}))}getTextContent({stream:e,task:a,resources:r,stateManager:i=null,includeMarkedContent:n=!1,sink:s,seenStyles:o=new Set,viewBox:c,lang:l=null,markedContentData:h=null,disableNormalization:u=!1,keepWhiteSpace:d=!1,prevRefs:f=null}){const g=e.dict?.objId,p=new RefSet(f);if(g){if(f?.has(g))throw new Error(`getTextContent - ignoring circular reference: ${g}`);p.put(g)}r||=Dict.empty;i||=new StateManager(new TextState);n&&(h||={level:0});const m={items:[],styles:Object.create(null),lang:l},b={initialized:!1,str:[],totalWidth:0,totalHeight:0,width:0,height:0,vertical:!1,prevTransform:null,textAdvanceScale:0,spaceInFlowMin:0,spaceInFlowMax:0,trackingSpaceMin:1/0,negativeSpaceMax:-1/0,notASpace:-1/0,transform:null,fontName:null,hasEOL:!1},y=[" "," "];let w=0;function saveLastChar(e){const t=(w+1)%2,a=" "!==y[w]&&" "===y[t];y[w]=e;w=t;return!d&&a}function shouldAddWhitepsace(){return!d&&" "!==y[w]&&" "===y[(w+1)%2]}function resetLastChars(){y[0]=y[1]=" ";w=0}const S=this,x=this.xref,k=[];let C=null;const v=new LocalImageCache,F=new LocalGStateCache,O=new EvaluatorPreprocessor(e,x,i);let T;function pushWhitespace({width:e=0,height:t=0,transform:a=b.prevTransform,fontName:r=b.fontName}){m.items.push({str:" ",dir:"ltr",width:e,height:t,transform:a,fontName:r,hasEOL:!1})}function getCurrentTextTransform(){const e=T.font,a=[T.fontSize*T.textHScale,0,0,T.fontSize,0,T.textRise];if(e.isType3Font&&(T.fontSize<=1||e.isCharBBox)&&!isArrayEqual(T.fontMatrix,t)){const t=e.bbox[3]-e.bbox[1];t>0&&(a[3]*=t*T.fontMatrix[3])}return Util.transform(T.ctm,Util.transform(T.textMatrix,a))}function ensureTextContentItem(){if(b.initialized)return b;const{font:e,loadedName:t}=T;if(!o.has(t)){o.add(t);m.styles[t]={fontFamily:e.fallbackName,ascent:e.ascent,descent:e.descent,vertical:e.vertical};if(S.options.fontExtraProperties&&e.systemFontInfo){const a=m.styles[t];a.fontSubstitution=e.systemFontInfo.css;a.fontSubstitutionLoadedName=e.systemFontInfo.loadedName}}b.fontName=t;const a=b.transform=getCurrentTextTransform();if(e.vertical){b.width=b.totalWidth=Math.hypot(a[0],a[1]);b.height=b.totalHeight=0;b.vertical=!0}else{b.width=b.totalWidth=0;b.height=b.totalHeight=Math.hypot(a[2],a[3]);b.vertical=!1}const r=Math.hypot(T.textLineMatrix[0],T.textLineMatrix[1]),i=Math.hypot(T.ctm[0],T.ctm[1]);b.textAdvanceScale=i*r;const{fontSize:n}=T;b.trackingSpaceMin=.102*n;b.notASpace=.03*n;b.negativeSpaceMax=-.2*n;b.spaceInFlowMin=.102*n;b.spaceInFlowMax=.6*n;b.hasEOL=!1;b.initialized=!0;return b}function updateAdvanceScale(){if(!b.initialized)return;const e=Math.hypot(T.textLineMatrix[0],T.textLineMatrix[1]),t=Math.hypot(T.ctm[0],T.ctm[1])*e;if(t!==b.textAdvanceScale){if(b.vertical){b.totalHeight+=b.height*b.textAdvanceScale;b.height=0}else{b.totalWidth+=b.width*b.textAdvanceScale;b.width=0}b.textAdvanceScale=t}}function runBidiTransform(e){let t=e.str.join("");u||(t=function normalizeUnicode(e){if(!ma){ma=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;ba=new Map([["ſt","ſt"]])}return e.replaceAll(ma,((e,t,a)=>t?t.normalize("NFKC"):ba.get(a)))}(t));const a=bidi(t,-1,e.vertical);return{str:a.str,dir:a.dir,width:Math.abs(e.totalWidth),height:Math.abs(e.totalHeight),transform:e.transform,fontName:e.fontName,hasEOL:e.hasEOL}}async function handleSetFont(e,i){const n=await S.loadFont(e,i,r,a);T.loadedName=n.loadedName;T.font=n.font;T.fontMatrix=n.font.fontMatrix||t}function applyInverseRotation(e,t,a){const r=Math.hypot(a[0],a[1]);return[(a[0]*e+a[1]*t)/r,(a[2]*e+a[3]*t)/r]}function compareWithLastPosition(e){const t=getCurrentTextTransform();let a=t[4],r=t[5];if(T.font?.vertical){if(ac[2]||r+ec[3])return!1}else if(a+ec[2]||rc[3])return!1;if(!T.font||!b.prevTransform)return!0;let i=b.prevTransform[4],n=b.prevTransform[5];if(i===a&&n===r)return!0;let s=-1;t[0]&&0===t[1]&&0===t[2]?s=t[0]>0?0:180:t[1]&&0===t[0]&&0===t[3]&&(s=t[1]>0?90:270);switch(s){case 0:break;case 90:[a,r]=[r,a];[i,n]=[n,i];break;case 180:[a,r,i,n]=[-a,-r,-i,-n];break;case 270:[a,r]=[-r,-a];[i,n]=[-n,-i];break;default:[a,r]=applyInverseRotation(a,r,t);[i,n]=applyInverseRotation(i,n,b.prevTransform)}if(T.font.vertical){const e=(n-r)/b.textAdvanceScale,t=a-i,s=Math.sign(b.height);if(e.5*b.width){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(t)>b.width){appendEOL();return!0}e<=s*b.notASpace&&resetLastChars();if(e<=s*b.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({height:Math.abs(e)})}else b.height+=e;else if(!addFakeSpaces(e,b.prevTransform,s))if(0===b.str.length){resetLastChars();pushWhitespace({height:Math.abs(e)})}else b.height+=e;Math.abs(t)>.25*b.width&&flushTextContentItem();return!0}const o=(a-i)/b.textAdvanceScale,l=r-n,h=Math.sign(b.width);if(o.5*b.height){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(l)>b.height){appendEOL();return!0}o<=h*b.notASpace&&resetLastChars();if(o<=h*b.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({width:Math.abs(o)})}else b.width+=o;else if(!addFakeSpaces(o,b.prevTransform,h))if(0===b.str.length){resetLastChars();pushWhitespace({width:Math.abs(o)})}else b.width+=o;Math.abs(l)>.25*b.height&&flushTextContentItem();return!0}function buildTextContentItem({chars:e,extraSpacing:t}){const a=T.font;if(!e){const e=T.charSpacing+t;e&&(a.vertical?T.translateTextMatrix(0,-e):T.translateTextMatrix(e*T.textHScale,0));d&&compareWithLastPosition(0);return}const r=a.charsToGlyphs(e),i=T.fontMatrix[0]*T.fontSize;for(let e=0,n=r.length;e0){const e=k.join("");k.length=0;buildTextContentItem({chars:e,extraSpacing:0})}break;case dt:if(!i.state.font){S.ensureStateFont(i.state);continue}buildTextContentItem({chars:w[0],extraSpacing:0});break;case gt:if(!i.state.font){S.ensureStateFont(i.state);continue}T.carriageReturn();buildTextContentItem({chars:w[0],extraSpacing:0});break;case pt:if(!i.state.font){S.ensureStateFont(i.state);continue}T.wordSpacing=w[0];T.charSpacing=w[1];T.carriageReturn();buildTextContentItem({chars:w[2],extraSpacing:0});break;case Nt:flushTextContentItem();C??=r.get("XObject")||Dict.empty;y=w[0]instanceof Name;b=w[0].name;if(y&&v.getByName(b))break;next(new Promise((function(e,t){if(!y)throw new FormatError("XObject must be referred to by name.");let f=C.getRaw(b);if(f instanceof Ref){if(v.getByRef(f)){e();return}if(S.globalImageCache.getData(f,S.pageIndex)){e();return}f=x.fetch(f)}if(!(f instanceof BaseStream))throw new FormatError("XObject should be a stream");const{dict:g}=f,m=g.get("Subtype");if(!(m instanceof Name))throw new FormatError("XObject should have a Name subtype");if("Form"!==m.name){v.set(b,g.objId,!0);e();return}const w=i.state.clone(),k=new StateManager(w),F=lookupMatrix(g.getArray("Matrix"),null);F&&k.transform(F);const O=g.get("Resources");enqueueChunk();const T={enqueueInvoked:!1,enqueue(e,t){this.enqueueInvoked=!0;s.enqueue(e,t)},get desiredSize(){return s.desiredSize},get ready(){return s.ready}};S.getTextContent({stream:f,task:a,resources:O instanceof Dict?O:r,stateManager:k,includeMarkedContent:n,sink:T,seenStyles:o,viewBox:c,lang:l,markedContentData:h,disableNormalization:u,keepWhiteSpace:d,prevRefs:p}).then((function(){T.enqueueInvoked||v.set(b,g.objId,!0);e()}),t)})).catch((function(e){if(!(e instanceof AbortException)){if(!S.options.ignoreErrors)throw e;warn(`getTextContent - ignoring XObject: "${e}".`)}})));return;case De:y=w[0]instanceof Name;b=w[0].name;if(y&&F.getByName(b))break;next(new Promise((function(e,t){if(!y)throw new FormatError("GState must be referred to by name.");const a=r.get("ExtGState");if(!(a instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const i=a.get(b);if(!(i instanceof Dict))throw new FormatError("GState should be a dictionary.");const n=i.get("Font");if(n){flushTextContentItem();T.fontName=null;T.fontSize=n[1];handleSetFont(null,n[0]).then(e,t)}else{F.set(b,i.objId,!0);e()}})).catch((function(e){if(!(e instanceof AbortException)){if(!S.options.ignoreErrors)throw e;warn(`getTextContent - ignoring ExtGState: "${e}".`)}})));return;case Lt:flushTextContentItem();if(n){h.level++;m.items.push({type:"beginMarkedContent",tag:w[0]instanceof Name?w[0].name:null})}break;case _t:flushTextContentItem();if(n){h.level++;let e=null;w[1]instanceof Dict&&(e=w[1].get("MCID"));m.items.push({type:"beginMarkedContentProps",id:Number.isInteger(e)?`${S.idFactory.getPageObjId()}_mc${e}`:null,tag:w[0]instanceof Name?w[0].name:null})}break;case jt:flushTextContentItem();if(n){if(0===h.level)break;h.level--;m.items.push({type:"endMarkedContent"})}break;case Re:!e||e.font===T.font&&e.fontSize===T.fontSize&&e.fontName===T.fontName||flushTextContentItem()}if(m.items.length>=s.desiredSize){g=!0;break}}if(g)next(Ln);else{flushTextContentItem();enqueueChunk();e()}})).catch((e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getTextContent - ignoring errors during "${a.name}" task: "${e}".`);flushTextContentItem();enqueueChunk()}}))}async extractDataStructures(e,t){const a=this.xref;let r;const i=this.readToUnicode(t.toUnicode);if(t.composite){const a=e.get("CIDSystemInfo");a instanceof Dict&&(t.cidSystemInfo={registry:stringToPDFString(a.get("Registry")),ordering:stringToPDFString(a.get("Ordering")),supplement:a.get("Supplement")});try{const t=e.get("CIDToGIDMap");t instanceof BaseStream&&(r=t.getBytes())}catch(e){if(!this.options.ignoreErrors)throw e;warn(`extractDataStructures - ignoring CIDToGIDMap data: "${e}".`)}}const n=[];let s,o=null;if(e.has("Encoding")){s=e.get("Encoding");if(s instanceof Dict){o=s.get("BaseEncoding");o=o instanceof Name?o.name:null;if(s.has("Differences")){const e=s.get("Differences");let t=0;for(const r of e){const e=a.fetchIfRef(r);if("number"==typeof e)t=e;else{if(!(e instanceof Name))throw new FormatError(`Invalid entry in 'Differences' array: ${e}`);n[t++]=e.name}}}}else if(s instanceof Name)o=s.name;else{const e="Encoding is not a Name nor a Dict";if(!this.options.ignoreErrors)throw new FormatError(e);warn(e)}"MacRomanEncoding"!==o&&"MacExpertEncoding"!==o&&"WinAnsiEncoding"!==o&&(o=null)}const c=!t.file||t.isInternalFont,l=ai()[t.name];o&&c&&l&&(o=null);if(o)t.defaultEncoding=getEncoding(o);else{const e=!!(t.flags&_r),a=!!(t.flags&jr);s=Cr;"TrueType"!==t.type||a||(s=vr);if(e||l){s=kr;c&&(/Symbol/i.test(t.name)?s=Fr:/Dingbats/i.test(t.name)?s=Ir:/Wingdings/i.test(t.name)&&(s=vr))}t.defaultEncoding=s}t.differences=n;t.baseEncodingName=o;t.hasEncoding=!!o||n.length>0;t.dict=e;t.toUnicode=await i;const h=await this.buildToUnicode(t);t.toUnicode=h;r&&(t.cidToGidMap=this.readCidToGidMap(r,h));return t}_simpleFontToUnicode(e,t=!1){assert(!e.composite,"Must be a simple font.");const a=[],r=e.defaultEncoding.slice(),i=e.baseEncodingName,n=e.differences;for(const e in n){const t=n[e];".notdef"!==t&&(r[e]=t)}const s=Or();for(const n in r){let o=r[n];if(""===o)continue;let c=s[o];if(void 0!==c){a[n]=String.fromCharCode(c);continue}let l=0;switch(o[0]){case"G":3===o.length&&(l=parseInt(o.substring(1),16));break;case"g":5===o.length&&(l=parseInt(o.substring(1),16));break;case"C":case"c":if(o.length>=3&&o.length<=4){const a=o.substring(1);if(t){l=parseInt(a,16);break}l=+a;if(Number.isNaN(l)&&Number.isInteger(parseInt(a,16)))return this._simpleFontToUnicode(e,!0)}break;case"u":c=getUnicodeForGlyph(o,s);-1!==c&&(l=c);break;default:switch(o){case"f_h":case"f_t":case"T_h":a[n]=o.replaceAll("_","");continue}}if(l>0&&l<=1114111&&Number.isInteger(l)){if(i&&l===+n){const e=getEncoding(i);if(e&&(o=e[n])){a[n]=String.fromCharCode(s[o]);continue}}a[n]=String.fromCodePoint(l)}}return a}async buildToUnicode(e){e.hasIncludedToUnicodeMap=e.toUnicode?.length>0;if(e.hasIncludedToUnicodeMap){!e.composite&&e.hasEncoding&&(e.fallbackToUnicode=this._simpleFontToUnicode(e));return e.toUnicode}if(!e.composite)return new ToUnicodeMap(this._simpleFontToUnicode(e));if(e.composite&&(e.cMap.builtInCMap&&!(e.cMap instanceof IdentityCMap)||"Adobe"===e.cidSystemInfo?.registry&&("GB1"===e.cidSystemInfo.ordering||"CNS1"===e.cidSystemInfo.ordering||"Japan1"===e.cidSystemInfo.ordering||"Korea1"===e.cidSystemInfo.ordering))){const{registry:t,ordering:a}=e.cidSystemInfo,r=Name.get(`${t}-${a}-UCS2`),i=await CMapFactory.create({encoding:r,fetchBuiltInCMap:this._fetchBuiltInCMapBound,useCMap:null}),n=[],s=[];e.cMap.forEach((function(e,t){if(t>65535)throw new FormatError("Max size of CID is 65,535");const a=i.lookup(t);if(a){s.length=0;for(let e=0,t=a.length;e>1;(0!==i||t.has(n))&&(a[n]=i)}return a}extractWidths(e,t,a){const r=this.xref;let i=[],n=0;const s=[];let o;if(a.composite){const t=e.get("DW");n="number"==typeof t?Math.ceil(t):1e3;const c=e.get("W");if(Array.isArray(c))for(let e=0,t=c.length;e{const t=c.get(e),r=new OperatorList;return n.getOperatorList({stream:t,task:a,resources:l,operatorList:r}).then((()=>{switch(r.fnArray[0]){case bt:this.#G(r,g);break;case mt:g||this.#V(r)}h[e]=r.getIR();for(const e of r.dependencies)i.add(e)})).catch((function(t){warn(`Type3 font resource "${e}" is not available.`);const a=new OperatorList;h[e]=a.getIR()}))}));this.#$=o.then((()=>{r.charProcOperatorList=h;if(this._bbox){r.isCharBBox=!0;r.bbox=this._bbox}}));return this.#$}#G(e,t=NaN){const a=Util.normalizeRect(e.argsArray[0].slice(2)),r=a[2]-a[0],i=a[3]-a[1],n=Math.hypot(r,i);if(0===r||0===i){e.fnArray.splice(0,1);e.argsArray.splice(0,1)}else if(0===t||Math.round(n/t)>=10){this._bbox??=[1/0,1/0,-1/0,-1/0];Util.rectBoundingBox(...a,this._bbox)}let s=0,o=e.length;for(;s=Ee&&n<=Je;if(i.variableArgs)o>s&&info(`Command ${r}: expected [0, ${s}] args, but received ${o} args.`);else{if(o!==s){const e=this.nonProcessedArgs;for(;o>s;){e.push(t.shift());o--}for(;oEvaluatorPreprocessor.MAX_INVALID_PATH_OPS)throw new FormatError(`Invalid ${e}`);warn(`Skipping ${e}`);null!==t&&(t.length=0);continue}}this.preprocessCommand(n,t);e.fn=n;e.args=t;return!0}if(a===wa)return!1;if(null!==a){null===t&&(t=[]);t.push(a);if(t.length>33)throw new FormatError("Too many arguments")}}}preprocessCommand(e,t){switch(0|e){case Be:this.stateManager.save();break;case Re:this.stateManager.restore();break;case Ne:this.stateManager.transform(t)}}}class DefaultAppearanceEvaluator extends EvaluatorPreprocessor{constructor(e){super(new StringStream(e))}parse(){const e={fn:0,args:[]},t={fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3)};try{for(;;){e.args.length=0;if(!this.read(e))break;if(0!==this.savedStatesDepth)continue;const{fn:a,args:r}=e;switch(0|a){case nt:const[e,a]=r;e instanceof Name&&(t.fontName=e.name);"number"==typeof a&&a>0&&(t.fontSize=a);break;case It:ColorSpaceUtils.rgb.getRgbItem(r,0,t.fontColor,0);break;case vt:ColorSpaceUtils.gray.getRgbItem(r,0,t.fontColor,0);break;case Tt:ColorSpaceUtils.cmyk.getRgbItem(r,0,t.fontColor,0)}}}catch(e){warn(`parseDefaultAppearance - ignoring errors: "${e}".`)}return t}}function parseDefaultAppearance(e){return new DefaultAppearanceEvaluator(e).parse()}class AppearanceStreamEvaluator extends EvaluatorPreprocessor{constructor(e,t,a,r){super(e);this.stream=e;this.evaluatorOptions=t;this.xref=a;this.globalColorSpaceCache=r;this.resources=e.dict?.get("Resources")}parse(){const e={fn:0,args:[]};let t={scaleFactor:1,fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3),fillColorSpace:ColorSpaceUtils.gray},a=!1;const r=[];try{for(;;){e.args.length=0;if(a||!this.read(e))break;const{fn:i,args:n}=e;switch(0|i){case Be:r.push({scaleFactor:t.scaleFactor,fontSize:t.fontSize,fontName:t.fontName,fontColor:t.fontColor.slice(),fillColorSpace:t.fillColorSpace});break;case Re:t=r.pop()||t;break;case ht:t.scaleFactor*=Math.hypot(n[0],n[1]);break;case nt:const[e,i]=n;e instanceof Name&&(t.fontName=e.name);"number"==typeof i&&i>0&&(t.fontSize=i*t.scaleFactor);break;case wt:t.fillColorSpace=ColorSpaceUtils.parse({cs:n[0],xref:this.xref,resources:this.resources,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:this._localColorSpaceCache});break;case At:t.fillColorSpace.getRgbItem(n,0,t.fontColor,0);break;case It:ColorSpaceUtils.rgb.getRgbItem(n,0,t.fontColor,0);break;case vt:ColorSpaceUtils.gray.getRgbItem(n,0,t.fontColor,0);break;case Tt:ColorSpaceUtils.cmyk.getRgbItem(n,0,t.fontColor,0);break;case dt:case ft:case gt:case pt:a=!0}}}catch(e){warn(`parseAppearanceStream - ignoring errors: "${e}".`)}this.stream.reset();delete t.scaleFactor;delete t.fillColorSpace;return t}get _localColorSpaceCache(){return shadow(this,"_localColorSpaceCache",new LocalColorSpaceCache)}get _pdfFunctionFactory(){return shadow(this,"_pdfFunctionFactory",new PDFFunctionFactory({xref:this.xref,isEvalSupported:this.evaluatorOptions.isEvalSupported}))}}function getPdfColor(e,t){if(e[0]===e[1]&&e[1]===e[2]){return`${numberToString(e[0]/255)} ${t?"g":"G"}`}return Array.from(e,(e=>numberToString(e/255))).join(" ")+" "+(t?"rg":"RG")}class FakeUnicodeFont{constructor(e,t){this.xref=e;this.widths=null;this.firstChar=1/0;this.lastChar=-1/0;this.fontFamily=t;const a=new OffscreenCanvas(1,1);this.ctxMeasure=a.getContext("2d",{willReadFrequently:!0});FakeUnicodeFont._fontNameId||(FakeUnicodeFont._fontNameId=1);this.fontName=Name.get(`InvalidPDFjsFont_${t}_${FakeUnicodeFont._fontNameId++}`)}get fontDescriptorRef(){if(!FakeUnicodeFont._fontDescriptorRef){const e=new Dict(this.xref);e.set("Type",Name.get("FontDescriptor"));e.set("FontName",this.fontName);e.set("FontFamily","MyriadPro Regular");e.set("FontBBox",[0,0,0,0]);e.set("FontStretch",Name.get("Normal"));e.set("FontWeight",400);e.set("ItalicAngle",0);FakeUnicodeFont._fontDescriptorRef=this.xref.getNewPersistentRef(e)}return FakeUnicodeFont._fontDescriptorRef}get descendantFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("CIDFontType0"));e.set("CIDToGIDMap",Name.get("Identity"));e.set("FirstChar",this.firstChar);e.set("LastChar",this.lastChar);e.set("FontDescriptor",this.fontDescriptorRef);e.set("DW",1e3);const t=[],a=[...this.widths.entries()].sort();let r=null,i=null;for(const[e,n]of a)if(r)if(e===r+i.length)i.push(n);else{t.push(r,i);r=e;i=[n]}else{r=e;i=[n]}r&&t.push(r,i);e.set("W",t);const n=new Dict(this.xref);n.set("Ordering","Identity");n.set("Registry","Adobe");n.set("Supplement",0);e.set("CIDSystemInfo",n);return this.xref.getNewPersistentRef(e)}get baseFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("Type0"));e.set("Encoding",Name.get("Identity-H"));e.set("DescendantFonts",[this.descendantFontRef]);e.set("ToUnicode",Name.get("Identity-H"));return this.xref.getNewPersistentRef(e)}get resources(){const e=new Dict(this.xref),t=new Dict(this.xref);t.set(this.fontName.name,this.baseFontRef);e.set("Font",t);return e}_createContext(){this.widths=new Map;this.ctxMeasure.font=`1000px ${this.fontFamily}`;return this.ctxMeasure}createFontResources(e){const t=this._createContext();for(const a of e.split(/\r\n?|\n/))for(const e of a.split("")){const a=e.charCodeAt(0);if(this.widths.has(a))continue;const r=t.measureText(e),i=Math.ceil(r.width);this.widths.set(a,i);this.firstChar=Math.min(a,this.firstChar);this.lastChar=Math.max(a,this.lastChar)}return this.resources}static getFirstPositionInfo(e,t,i){const[n,s,o,c]=e;let l=o-n,h=c-s;t%180!=0&&([l,h]=[h,l]);const u=a*i;return{coords:[0,h+r*i-u],bbox:[0,0,l,h],matrix:0!==t?getRotationMatrix(t,h,u):void 0}}createAppearance(e,t,i,n,s,o){const c=this._createContext(),l=[];let h=-1/0;for(const t of e.split(/\r\n?|\n/)){l.push(t);const e=c.measureText(t).width;h=Math.max(h,e);for(const e of codePointIter(t)){const t=String.fromCodePoint(e);let a=this.widths.get(e);if(void 0===a){const r=c.measureText(t);a=Math.ceil(r.width);this.widths.set(e,a);this.firstChar=Math.min(e,this.firstChar);this.lastChar=Math.max(e,this.lastChar)}}}h*=n/1e3;const[u,d,f,g]=t;let p=f-u,m=g-d;i%180!=0&&([p,m]=[m,p]);let b=1;h>p&&(b=p/h);let y=1;const w=a*n,S=r*n,x=w*l.length;x>m&&(y=m/x);const k=n*Math.min(b,y),C=["q",`0 0 ${numberToString(p)} ${numberToString(m)} re W n`,"BT",`1 0 0 1 0 ${numberToString(m+S)} Tm 0 Tc ${getPdfColor(s,!0)}`,`/${this.fontName.name} ${numberToString(k)} Tf`],{resources:v}=this;if(1!==(o="number"==typeof o&&o>=0&&o<=1?o:1)){C.push("/R0 gs");const e=new Dict(this.xref),t=new Dict(this.xref);t.set("ca",o);t.set("CA",o);t.set("Type",Name.get("ExtGState"));e.set("R0",t);v.set("ExtGState",e)}const F=numberToString(w);for(const e of l)C.push(`0 -${F} Td <${stringToUTF16HexString(e)}> Tj`);C.push("ET","Q");const O=C.join("\n"),T=new Dict(this.xref);T.set("Subtype",Name.get("Form"));T.set("Type",Name.get("XObject"));T.set("BBox",[0,0,p,m]);T.set("Length",O.length);T.set("Resources",v);if(i){const e=getRotationMatrix(i,p,m);T.set("Matrix",e)}const M=new StringStream(O);M.dict=T;return M}}class NameOrNumberTree{constructor(e,t,a){this.root=e;this.xref=t;this._type=a}getAll(){const e=new Map;if(!this.root)return e;const t=this.xref,a=new RefSet;a.put(this.root);const r=[this.root];for(;r.length>0;){const i=t.fetchIfRef(r.shift());if(!(i instanceof Dict))continue;if(i.has("Kids")){const e=i.get("Kids");if(!Array.isArray(e))continue;for(const t of e){if(a.has(t))throw new FormatError(`Duplicate entry in "${this._type}" tree.`);r.push(t);a.put(t)}continue}const n=i.get(this._type);if(Array.isArray(n))for(let a=0,r=n.length;a10){warn(`Search depth limit reached for "${this._type}" tree.`);return null}const i=a.get("Kids");if(!Array.isArray(i))return null;let n=0,s=i.length-1;for(;n<=s;){const r=n+s>>1,o=t.fetchIfRef(i[r]),c=o.get("Limits");if(et.fetchIfRef(c[1]))){a=o;break}n=r+1}}if(n>s)return null}const i=a.get(this._type);if(Array.isArray(i)){let a=0,r=i.length-2;for(;a<=r;){const n=a+r>>1,s=n+(1&n),o=t.fetchIfRef(i[s]);if(eo))return i[s+1];a=s+2}}}return null}get(e){return this.xref.fetchIfRef(this.getRaw(e))}}class NameTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Names")}}class NumberTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Nums")}}function clearGlobalCaches(){!function clearPatternCaches(){Ti=Object.create(null)}();!function clearPrimitiveCaches(){Sa=Object.create(null);xa=Object.create(null);Aa=Object.create(null)}();!function clearUnicodeCaches(){Nr.clear()}();JpxImage.cleanup()}function pickPlatformItem(e){return e instanceof Dict?e.has("UF")?e.get("UF"):e.has("F")?e.get("F"):e.has("Unix")?e.get("Unix"):e.has("Mac")?e.get("Mac"):e.has("DOS")?e.get("DOS"):null:null}class FileSpec{#K=!1;constructor(e,t,a=!1){if(e instanceof Dict){this.xref=t;this.root=e;e.has("FS")&&(this.fs=e.get("FS"));e.has("RF")&&warn("Related file specifications are not supported");a||(e.has("EF")?this.#K=!0:warn("Non-embedded file specifications are not supported"))}}get filename(){let e="";const t=pickPlatformItem(this.root);t&&"string"==typeof t&&(e=stringToPDFString(t).replaceAll("\\\\","\\").replaceAll("\\/","/").replaceAll("\\","/"));return shadow(this,"filename",e||"unnamed")}get content(){if(!this.#K)return null;this._contentRef||=pickPlatformItem(this.root?.get("EF"));let e=null;if(this._contentRef){const t=this.xref.fetchIfRef(this._contentRef);t instanceof BaseStream?e=t.getBytes():warn("Embedded file specification points to non-existing/invalid content")}else warn("Embedded file specification does not have any content");return e}get description(){let e="";const t=this.root?.get("Desc");t&&"string"==typeof t&&(e=stringToPDFString(t));return shadow(this,"description",e)}get serializable(){return{rawFilename:this.filename,filename:(e=this.filename,e.substring(e.lastIndexOf("/")+1)),content:this.content,description:this.description};var e}}const _n=0,jn=-2,Un=-3,Xn=-4,qn=-5,Hn=-6,Wn=-9;function isWhitespace(e,t){const a=e[t];return" "===a||"\n"===a||"\r"===a||"\t"===a}class XMLParserBase{_resolveEntities(e){return e.replaceAll(/&([^;]+);/g,((e,t)=>{if("#x"===t.substring(0,2))return String.fromCodePoint(parseInt(t.substring(2),16));if("#"===t.substring(0,1))return String.fromCodePoint(parseInt(t.substring(1),10));switch(t){case"lt":return"<";case"gt":return">";case"amp":return"&";case"quot":return'"';case"apos":return"'"}return this.onResolveEntity(t)}))}_parseContent(e,t){const a=[];let r=t;function skipWs(){for(;r"!==e[r]&&"/"!==e[r];)++r;const i=e.substring(t,r);skipWs();for(;r"!==e[r]&&"/"!==e[r]&&"?"!==e[r];){skipWs();let t="",i="";for(;r"!==e[a]&&"?"!==e[a]&&"/"!==e[a];)++a;const r=e.substring(t,a);!function skipWs(){for(;a"!==e[a+1]);)++a;return{name:r,value:e.substring(i,a),parsed:a-t}}parseXml(e){let t=0;for(;t",a);if(t<0){this.onError(Wn);return}this.onEndElement(e.substring(a,t));a=t+1;break;case"?":++a;const r=this._parseProcessingInstruction(e,a);if("?>"!==e.substring(a+r.parsed,a+r.parsed+2)){this.onError(Un);return}this.onPi(r.name,r.value);a+=r.parsed+2;break;case"!":if("--"===e.substring(a+1,a+3)){t=e.indexOf("--\x3e",a+3);if(t<0){this.onError(qn);return}this.onComment(e.substring(a+3,t));a=t+3}else if("[CDATA["===e.substring(a+1,a+8)){t=e.indexOf("]]>",a+8);if(t<0){this.onError(jn);return}this.onCdata(e.substring(a+8,t));a=t+3}else{if("DOCTYPE"!==e.substring(a+1,a+8)){this.onError(Hn);return}{const r=e.indexOf("[",a+8);let i=!1;t=e.indexOf(">",a+8);if(t<0){this.onError(Xn);return}if(r>0&&t>r){t=e.indexOf("]>",a+8);if(t<0){this.onError(Xn);return}i=!0}const n=e.substring(a+8,t+(i?1:0));this.onDoctype(n);a=t+(i?2:1)}}break;default:const i=this._parseContent(e,a);if(null===i){this.onError(Hn);return}let n=!1;if("/>"===e.substring(a+i.parsed,a+i.parsed+2))n=!0;else if(">"!==e.substring(a+i.parsed,a+i.parsed+1)){this.onError(Wn);return}this.onBeginElement(i.name,i.attributes,n);a+=i.parsed+(n?2:1)}}else{for(;ae.textContent)).join(""):this.nodeValue||""}get children(){return this.childNodes||[]}hasChildNodes(){return this.childNodes?.length>0}searchNode(e,t){if(t>=e.length)return this;const a=e[t];if(a.name.startsWith("#")&&t0){r.push([i,0]);i=i.childNodes[0]}else{if(0===r.length)return null;for(;0!==r.length;){const[e,t]=r.pop(),a=t+1;if(a");for(const t of this.childNodes)t.dump(e);e.push(``)}else this.nodeValue?e.push(`>${encodeToXmlString(this.nodeValue)}`):e.push("/>")}else e.push(encodeToXmlString(this.nodeValue))}}class SimpleXMLParser extends XMLParserBase{constructor({hasAttributes:e=!1,lowerCaseName:t=!1}){super();this._currentFragment=null;this._stack=null;this._errorCode=_n;this._hasAttributes=e;this._lowerCaseName=t}parseFromString(e){this._currentFragment=[];this._stack=[];this._errorCode=_n;this.parseXml(e);if(this._errorCode!==_n)return;const[t]=this._currentFragment;return t?{documentElement:t}:void 0}onText(e){if(function isWhitespaceString(e){for(let t=0,a=e.length;t\\376\\377([^<]+)/g,(function(e,t){const a=t.replaceAll(/\\([0-3])([0-7])([0-7])/g,(function(e,t,a,r){return String.fromCharCode(64*t+8*a+1*r)})).replaceAll(/&(amp|apos|gt|lt|quot);/g,(function(e,t){switch(t){case"amp":return"&";case"apos":return"'";case"gt":return">";case"lt":return"<";case"quot":return'"'}throw new Error(`_repair: ${t} isn't defined.`)})),r=[">"];for(let e=0,t=a.length;e=32&&t<127&&60!==t&&62!==t&&38!==t?r.push(String.fromCharCode(t)):r.push("&#x"+(65536+t).toString(16).substring(1)+";")}return r.join("")}))}_getSequence(e){const t=e.nodeName;return"rdf:bag"!==t&&"rdf:seq"!==t&&"rdf:alt"!==t?null:e.childNodes.filter((e=>"rdf:li"===e.nodeName))}_parseArray(e){if(!e.hasChildNodes())return;const[t]=e.childNodes,a=this._getSequence(t)||[];this._metadataMap.set(e.nodeName,a.map((e=>e.textContent.trim())))}_parse(e){let t=e.documentElement;if("rdf:rdf"!==t.nodeName){t=t.firstChild;for(;t&&"rdf:rdf"!==t.nodeName;)t=t.nextSibling}if(t&&"rdf:rdf"===t.nodeName&&t.hasChildNodes())for(const e of t.childNodes)if("rdf:description"===e.nodeName)for(const t of e.childNodes){const e=t.nodeName;switch(e){case"#text":continue;case"dc:creator":case"dc:subject":this._parseArray(t);continue}this._metadataMap.set(e,t.textContent.trim())}}get serializable(){return{parsedData:this._metadataMap,rawData:this._data}}}const zn=1,$n=2,Gn=3,Vn=4,Kn=5;class StructTreeRoot{constructor(e,t,a){this.xref=e;this.dict=t;this.ref=a instanceof Ref?a:null;this.roleMap=new Map;this.structParentIds=null}init(){this.readRoleMap()}#J(e,t,a){if(!(e instanceof Ref)||t<0)return;this.structParentIds||=new RefSetCache;let r=this.structParentIds.get(e);if(!r){r=[];this.structParentIds.put(e,r)}r.push([t,a])}addAnnotationIdToPage(e,t){this.#J(e,t,Vn)}readRoleMap(){const e=this.dict.get("RoleMap");if(e instanceof Dict)for(const[t,a]of e)a instanceof Name&&this.roleMap.set(t,a.name)}static async canCreateStructureTree({catalogRef:e,pdfManager:t,newAnnotationsByPage:a}){if(!(e instanceof Ref)){warn("Cannot save the struct tree: no catalog reference.");return!1}let r=0,i=!0;for(const[e,n]of a){const{ref:a}=await t.getPage(e);if(!(a instanceof Ref)){warn(`Cannot save the struct tree: page ${e} has no ref.`);i=!0;break}for(const e of n)if(e.accessibilityData?.type){e.parentTreeId=r++;i=!1}}if(i){for(const e of a.values())for(const t of e)delete t.parentTreeId;return!1}return!0}static async createStructureTree({newAnnotationsByPage:e,xref:t,catalogRef:a,pdfManager:r,changes:i}){const n=r.catalog.cloneDict(),s=new RefSetCache;s.put(a,n);const o=t.getNewTemporaryRef();n.set("StructTreeRoot",o);const c=new Dict(t);c.set("Type",Name.get("StructTreeRoot"));const l=t.getNewTemporaryRef();c.set("ParentTree",l);const h=[];c.set("K",h);s.put(o,c);const u=new Dict(t),d=[];u.set("Nums",d);const f=await this.#Y({newAnnotationsByPage:e,structTreeRootRef:o,structTreeRoot:null,kids:h,nums:d,xref:t,pdfManager:r,changes:i,cache:s});c.set("ParentTreeNextKey",f);s.put(l,u);for(const[e,t]of s.items())i.put(e,{data:t})}async canUpdateStructTree({pdfManager:e,newAnnotationsByPage:t}){if(!this.ref){warn("Cannot update the struct tree: no root reference.");return!1}let a=this.dict.get("ParentTreeNextKey");if(!Number.isInteger(a)||a<0){warn("Cannot update the struct tree: invalid next key.");return!1}const r=this.dict.get("ParentTree");if(!(r instanceof Dict)){warn("Cannot update the struct tree: ParentTree isn't a dict.");return!1}const i=r.get("Nums");if(!Array.isArray(i)){warn("Cannot update the struct tree: nums isn't an array.");return!1}const n=new NumberTree(r,this.xref);for(const a of t.keys()){const{pageDict:t}=await e.getPage(a);if(!t.has("StructParents"))continue;const r=t.get("StructParents");if(!Number.isInteger(r)||!Array.isArray(n.get(r))){warn(`Cannot save the struct tree: page ${a} has a wrong id.`);return!1}}let s=!0;for(const[r,i]of t){const{pageDict:t}=await e.getPage(r);StructTreeRoot.#Z({elements:i,xref:this.xref,pageDict:t,numberTree:n});for(const e of i)if(e.accessibilityData?.type){e.accessibilityData.structParent>=0||(e.parentTreeId=a++);s=!1}}if(s){for(const e of t.values())for(const t of e){delete t.parentTreeId;delete t.structTreeParent}return!1}return!0}async updateStructureTree({newAnnotationsByPage:e,pdfManager:t,changes:a}){const{ref:r,xref:i}=this,n=this.dict.clone(),s=new RefSetCache;s.put(r,n);let o,c=n.getRaw("ParentTree");if(c instanceof Ref)o=i.fetch(c);else{o=c;c=i.getNewTemporaryRef();n.set("ParentTree",c)}o=o.clone();s.put(c,o);let l=o.getRaw("Nums"),h=null;if(l instanceof Ref){h=l;l=i.fetch(h)}l=l.slice();h||o.set("Nums",l);const u=await StructTreeRoot.#Y({newAnnotationsByPage:e,structTreeRootRef:r,structTreeRoot:this,kids:null,nums:l,xref:i,pdfManager:t,changes:a,cache:s});if(-1!==u){n.set("ParentTreeNextKey",u);h&&s.put(h,l);for(const[e,t]of s.items())a.put(e,{data:t})}}static async#Y({newAnnotationsByPage:e,structTreeRootRef:t,structTreeRoot:a,kids:r,nums:i,xref:n,pdfManager:s,changes:o,cache:c}){const l=Name.get("OBJR");let h,u=-1;for(const[d,f]of e){const e=await s.getPage(d),{ref:g}=e,p=g instanceof Ref;for(const{accessibilityData:s,ref:m,parentTreeId:b,structTreeParent:y}of f){if(!s?.type)continue;const{structParent:f}=s;if(a&&Number.isInteger(f)&&f>=0){let t=(h||=new Map).get(d);if(void 0===t){t=new StructTreePage(a,e.pageDict).collectObjects(g);h.set(d,t)}const r=t?.get(f);if(r){const e=n.fetch(r).clone();StructTreeRoot.#Q(e,s);o.put(r,{data:e});continue}}u=Math.max(u,b);const w=n.getNewTemporaryRef(),S=new Dict(n);StructTreeRoot.#Q(S,s);await this.#ee({structTreeParent:y,tagDict:S,newTagRef:w,structTreeRootRef:t,fallbackKids:r,xref:n,cache:c});const x=new Dict(n);S.set("K",x);x.set("Type",l);p&&x.set("Pg",g);x.set("Obj",m);c.put(w,S);i.push(b,w)}}return u+1}static#Q(e,{type:t,title:a,lang:r,alt:i,expanded:n,actualText:s}){e.set("S",Name.get(t));a&&e.set("T",stringToAsciiOrUTF16BE(a));r&&e.set("Lang",stringToAsciiOrUTF16BE(r));i&&e.set("Alt",stringToAsciiOrUTF16BE(i));n&&e.set("E",stringToAsciiOrUTF16BE(n));s&&e.set("ActualText",stringToAsciiOrUTF16BE(s))}static#Z({elements:e,xref:t,pageDict:a,numberTree:r}){const i=new Map;for(const t of e)if(t.structTreeParentId){const e=parseInt(t.structTreeParentId.split("_mc")[1],10);let a=i.get(e);if(!a){a=[];i.set(e,a)}a.push(t)}const n=a.get("StructParents");if(!Number.isInteger(n))return;const s=r.get(n),updateElement=(e,a,r)=>{const n=i.get(e);if(n){const e=a.getRaw("P"),i=t.fetchIfRef(e);if(e instanceof Ref&&i instanceof Dict){const e={ref:r,dict:a};for(const t of n)t.structTreeParent=e}return!0}return!1};for(const e of s){if(!(e instanceof Ref))continue;const a=t.fetch(e),r=a.get("K");if(Number.isInteger(r))updateElement(r,a,e);else if(Array.isArray(r))for(let i of r){i=t.fetchIfRef(i);if(Number.isInteger(i)&&updateElement(i,a,e))break;if(!(i instanceof Dict))continue;if(!isName(i.get("Type"),"MCR"))break;const r=i.get("MCID");if(Number.isInteger(r)&&updateElement(r,a,e))break}}}static async#ee({structTreeParent:e,tagDict:t,newTagRef:a,structTreeRootRef:r,fallbackKids:i,xref:n,cache:s}){let o,c=null;if(e){({ref:c}=e);o=e.dict.getRaw("P")||r}else o=r;t.set("P",o);const l=n.fetchIfRef(o);if(!l){i.push(a);return}let h=s.get(o);if(!h){h=l.clone();s.put(o,h)}const u=h.getRaw("K");let d=u instanceof Ref?s.get(u):null;if(!d){d=n.fetchIfRef(u);d=Array.isArray(d)?d.slice():[u];const e=n.getNewTemporaryRef();h.set("K",e);s.put(e,d)}const f=d.indexOf(c);d.splice(f>=0?f+1:d.length,0,a)}}class StructElementNode{constructor(e,t){this.tree=e;this.xref=e.xref;this.dict=t;this.kids=[];this.parseKids()}get role(){const e=this.dict.get("S"),t=e instanceof Name?e.name:"",{root:a}=this.tree;return a.roleMap.get(t)??t}parseKids(){let e=null;const t=this.dict.getRaw("Pg");t instanceof Ref&&(e=t.toString());const a=this.dict.get("K");if(Array.isArray(a))for(const t of a){const a=this.parseKid(e,this.xref.fetchIfRef(t));a&&this.kids.push(a)}else{const t=this.parseKid(e,a);t&&this.kids.push(t)}}parseKid(e,t){if(Number.isInteger(t))return this.tree.pageDict.objId!==e?null:new StructElement({type:zn,mcid:t,pageObjId:e});if(!(t instanceof Dict))return null;const a=t.getRaw("Pg");a instanceof Ref&&(e=a.toString());const r=t.get("Type")instanceof Name?t.get("Type").name:null;if("MCR"===r){if(this.tree.pageDict.objId!==e)return null;const a=t.getRaw("Stm");return new StructElement({type:$n,refObjId:a instanceof Ref?a.toString():null,pageObjId:e,mcid:t.get("MCID")})}if("OBJR"===r){if(this.tree.pageDict.objId!==e)return null;const a=t.getRaw("Obj");return new StructElement({type:Gn,refObjId:a instanceof Ref?a.toString():null,pageObjId:e})}return new StructElement({type:Kn,dict:t})}}class StructElement{constructor({type:e,dict:t=null,mcid:a=null,pageObjId:r=null,refObjId:i=null}){this.type=e;this.dict=t;this.mcid=a;this.pageObjId=r;this.refObjId=i;this.parentNode=null}}class StructTreePage{constructor(e,t){this.root=e;this.xref=e?.xref??null;this.rootDict=e?.dict??null;this.pageDict=t;this.nodes=[]}collectObjects(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return null;const t=this.rootDict.get("ParentTree");if(!t)return null;const a=this.root.structParentIds?.get(e);if(!a)return null;const r=new Map,i=new NumberTree(t,this.xref);for(const[e]of a){const t=i.getRaw(e);t instanceof Ref&&r.set(e,t)}return r}parse(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return;const t=this.rootDict.get("ParentTree");if(!t)return;const a=this.pageDict.get("StructParents"),r=this.root.structParentIds?.get(e);if(!Number.isInteger(a)&&!r)return;const i=new Map,n=new NumberTree(t,this.xref);if(Number.isInteger(a)){const e=n.get(a);if(Array.isArray(e))for(const t of e)t instanceof Ref&&this.addNode(this.xref.fetch(t),i)}if(r)for(const[e,t]of r){const a=n.get(e);if(a){const e=this.addNode(this.xref.fetchIfRef(a),i);1===e?.kids?.length&&e.kids[0].type===Gn&&(e.kids[0].type=t)}}}addNode(e,t,a=0){if(a>40){warn("StructTree MAX_DEPTH reached.");return null}if(!(e instanceof Dict))return null;if(t.has(e))return t.get(e);const r=new StructElementNode(this,e);t.set(e,r);const i=e.get("P");if(!(i instanceof Dict)||isName(i.get("Type"),"StructTreeRoot")){this.addTopLevelNode(e,r)||t.delete(e);return r}const n=this.addNode(i,t,a+1);if(!n)return r;let s=!1;for(const t of n.kids)if(t.type===Kn&&t.dict===e){t.parentNode=r;s=!0}s||t.delete(e);return r}addTopLevelNode(e,t){const a=this.rootDict.get("K");if(!a)return!1;if(a instanceof Dict){if(a.objId!==e.objId)return!1;this.nodes[0]=t;return!0}if(!Array.isArray(a))return!0;let r=!1;for(let i=0;i40){warn("StructTree too deep to be fully serialized.");return}const r=Object.create(null);r.role=e.role;r.children=[];t.children.push(r);let i=e.dict.get("Alt");"string"!=typeof i&&(i=e.dict.get("ActualText"));"string"==typeof i&&(r.alt=stringToPDFString(i));const n=e.dict.get("A");if(n instanceof Dict){const e=lookupNormalRect(n.getArray("BBox"),null);if(e)r.bbox=e;else{const e=n.get("Width"),t=n.get("Height");"number"==typeof e&&e>0&&"number"==typeof t&&t>0&&(r.bbox=[0,0,e,t])}}const s=e.dict.get("Lang");"string"==typeof s&&(r.lang=stringToPDFString(s));for(const t of e.kids){const e=t.type===Kn?t.parentNode:null;e?nodeToSerializable(e,r,a+1):t.type===zn||t.type===$n?r.children.push({type:"content",id:`p${t.pageObjId}_mc${t.mcid}`}):t.type===Gn?r.children.push({type:"object",id:t.refObjId}):t.type===Vn&&r.children.push({type:"annotation",id:`pdfjs_internal_id_${t.refObjId}`})}}const e=Object.create(null);e.children=[];e.role="Root";for(const t of this.nodes)t&&nodeToSerializable(t,e);return e}}const Jn=function _isValidExplicitDest(e,t,a){if(!Array.isArray(a)||a.length<2)return!1;const[r,i,...n]=a;if(!e(r)&&!Number.isInteger(r))return!1;if(!t(i))return!1;const s=n.length;let o=!0;switch(i.name){case"XYZ":if(s<2||s>3)return!1;break;case"Fit":case"FitB":return 0===s;case"FitH":case"FitBH":case"FitV":case"FitBV":if(s>1)return!1;break;case"FitR":if(4!==s)return!1;o=!1;break;default:return!1}for(const e of n)if(!("number"==typeof e||o&&null===e))return!1;return!0}.bind(null,(e=>e instanceof Ref),isName);function fetchDest(e){e instanceof Dict&&(e=e.get("D"));return Jn(e)?e:null}function fetchRemoteDest(e){let t=e.get("D");if(t){t instanceof Name&&(t=t.name);if("string"==typeof t)return stringToPDFString(t);if(Jn(t))return JSON.stringify(t)}return null}class Catalog{constructor(e,t){this.pdfManager=e;this.xref=t;this._catDict=t.getCatalogObj();if(!(this._catDict instanceof Dict))throw new FormatError("Catalog object is not a dictionary.");this.toplevelPagesDict;this._actualNumPages=null;this.fontCache=new RefSetCache;this.builtInCMapCache=new Map;this.standardFontDataCache=new Map;this.globalColorSpaceCache=new GlobalColorSpaceCache;this.globalImageCache=new GlobalImageCache;this.pageKidsCountCache=new RefSetCache;this.pageIndexCache=new RefSetCache;this.pageDictCache=new RefSetCache;this.nonBlendModesSet=new RefSet;this.systemFontCache=new Map}cloneDict(){return this._catDict.clone()}get version(){const e=this._catDict.get("Version");if(e instanceof Name){if(Ca.test(e.name))return shadow(this,"version",e.name);warn(`Invalid PDF catalog version: ${e.name}`)}return shadow(this,"version",null)}get lang(){const e=this._catDict.get("Lang");return shadow(this,"lang",e&&"string"==typeof e?stringToPDFString(e):null)}get needsRendering(){const e=this._catDict.get("NeedsRendering");return shadow(this,"needsRendering","boolean"==typeof e&&e)}get collection(){let e=null;try{const t=this._catDict.get("Collection");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch Collection entry; assuming no collection is present.")}return shadow(this,"collection",e)}get acroForm(){let e=null;try{const t=this._catDict.get("AcroForm");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch AcroForm entry; assuming no forms are present.")}return shadow(this,"acroForm",e)}get acroFormRef(){const e=this._catDict.getRaw("AcroForm");return shadow(this,"acroFormRef",e instanceof Ref?e:null)}get metadata(){const e=this._catDict.getRaw("Metadata");if(!(e instanceof Ref))return shadow(this,"metadata",null);let t=null;try{const a=this.xref.fetch(e,!this.xref.encrypt?.encryptMetadata);if(a instanceof BaseStream&&a.dict instanceof Dict){const e=a.dict.get("Type"),r=a.dict.get("Subtype");if(isName(e,"Metadata")&&isName(r,"XML")){const e=stringToUTF8String(a.getString());e&&(t=new MetadataParser(e).serializable)}}}catch(e){if(e instanceof MissingDataException)throw e;info(`Skipping invalid Metadata: "${e}".`)}return shadow(this,"metadata",t)}get markInfo(){let e=null;try{e=this._readMarkInfo()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read mark info.")}return shadow(this,"markInfo",e)}_readMarkInfo(){const e=this._catDict.get("MarkInfo");if(!(e instanceof Dict))return null;const t={Marked:!1,UserProperties:!1,Suspects:!1};for(const a in t){const r=e.get(a);"boolean"==typeof r&&(t[a]=r)}return t}get structTreeRoot(){let e=null;try{e=this.#te()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable read to structTreeRoot info.")}return shadow(this,"structTreeRoot",e)}#te(){const e=this._catDict.getRaw("StructTreeRoot"),t=this.xref.fetchIfRef(e);if(!(t instanceof Dict))return null;const a=new StructTreeRoot(this.xref,t,e);a.init();return a}get toplevelPagesDict(){const e=this._catDict.get("Pages");if(!(e instanceof Dict))throw new FormatError("Invalid top-level pages dictionary.");return shadow(this,"toplevelPagesDict",e)}get documentOutline(){let e=null;try{e=this._readDocumentOutline()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read document outline.")}return shadow(this,"documentOutline",e)}_readDocumentOutline(){let e=this._catDict.get("Outlines");if(!(e instanceof Dict))return null;e=e.getRaw("First");if(!(e instanceof Ref))return null;const t={items:[]},a=[{obj:e,parent:t}],r=new RefSet;r.put(e);const i=this.xref,n=new Uint8ClampedArray(3);for(;a.length>0;){const t=a.shift(),s=i.fetchIfRef(t.obj);if(null===s)continue;s.has("Title")||warn("Invalid outline item encountered.");const o={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:s,resultObj:o,docBaseUrl:this.baseUrl,docAttachments:this.attachments});const c=s.get("Title"),l=s.get("F")||0,h=s.getArray("C"),u=s.get("Count");let d=n;!isNumberArray(h,3)||0===h[0]&&0===h[1]&&0===h[2]||(d=ColorSpaceUtils.rgb.getRgb(h,0));const f={action:o.action,attachment:o.attachment,dest:o.dest,url:o.url,unsafeUrl:o.unsafeUrl,newWindow:o.newWindow,setOCGState:o.setOCGState,title:"string"==typeof c?stringToPDFString(c):"",color:d,count:Number.isInteger(u)?u:void 0,bold:!!(2&l),italic:!!(1&l),items:[]};t.parent.items.push(f);e=s.getRaw("First");if(e instanceof Ref&&!r.has(e)){a.push({obj:e,parent:f});r.put(e)}e=s.getRaw("Next");if(e instanceof Ref&&!r.has(e)){a.push({obj:e,parent:t.parent});r.put(e)}}return t.items.length>0?t.items:null}get permissions(){let e=null;try{e=this._readPermissions()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read permissions.")}return shadow(this,"permissions",e)}_readPermissions(){const e=this.xref.trailer.get("Encrypt");if(!(e instanceof Dict))return null;let t=e.get("P");if("number"!=typeof t)return null;t+=2**32;const a=[];for(const e in w){const r=w[e];t&r&&a.push(r)}return a}get optionalContentConfig(){let e=null;try{const t=this._catDict.get("OCProperties");if(!t)return shadow(this,"optionalContentConfig",null);const a=t.get("D");if(!a)return shadow(this,"optionalContentConfig",null);const r=t.get("OCGs");if(!Array.isArray(r))return shadow(this,"optionalContentConfig",null);const i=new RefSetCache;for(const e of r)e instanceof Ref&&!i.has(e)&&i.put(e,this.#ae(e));e=this.#re(a,i)}catch(e){if(e instanceof MissingDataException)throw e;warn(`Unable to read optional content config: ${e}`)}return shadow(this,"optionalContentConfig",e)}#ae(e){const t=this.xref.fetch(e),a={id:e.toString(),name:null,intent:null,usage:{print:null,view:null},rbGroups:[]},r=t.get("Name");"string"==typeof r&&(a.name=stringToPDFString(r));let i=t.getArray("Intent");Array.isArray(i)||(i=[i]);i.every((e=>e instanceof Name))&&(a.intent=i.map((e=>e.name)));const n=t.get("Usage");if(!(n instanceof Dict))return a;const s=a.usage,o=n.get("Print");if(o instanceof Dict){const e=o.get("PrintState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":s.print={printState:e.name}}}const c=n.get("View");if(c instanceof Dict){const e=c.get("ViewState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":s.view={viewState:e.name}}}return a}#re(e,t){function parseOnOff(e){const a=[];if(Array.isArray(e))for(const r of e)r instanceof Ref&&t.has(r)&&a.push(r.toString());return a}function parseOrder(e,a=0){if(!Array.isArray(e))return null;const i=[];for(const n of e){if(n instanceof Ref&&t.has(n)){r.put(n);i.push(n.toString());continue}const e=parseNestedOrder(n,a);e&&i.push(e)}if(a>0)return i;const n=[];for(const[e]of t.items())r.has(e)||n.push(e.toString());n.length&&i.push({name:null,order:n});return i}function parseNestedOrder(e,t){if(++t>i){warn("parseNestedOrder - reached MAX_NESTED_LEVELS.");return null}const r=a.fetchIfRef(e);if(!Array.isArray(r))return null;const n=a.fetchIfRef(r[0]);if("string"!=typeof n)return null;const s=parseOrder(r.slice(1),t);return s?.length?{name:stringToPDFString(n),order:s}:null}const a=this.xref,r=new RefSet,i=10;!function parseRBGroups(e){if(Array.isArray(e))for(const r of e){const e=a.fetchIfRef(r);if(!Array.isArray(e)||!e.length)continue;const i=new Set;for(const a of e)if(a instanceof Ref&&t.has(a)&&!i.has(a.toString())){i.add(a.toString());t.get(a).rbGroups.push(i)}}}(e.get("RBGroups"));return{name:"string"==typeof e.get("Name")?stringToPDFString(e.get("Name")):null,creator:"string"==typeof e.get("Creator")?stringToPDFString(e.get("Creator")):null,baseState:e.get("BaseState")instanceof Name?e.get("BaseState").name:null,on:parseOnOff(e.get("ON")),off:parseOnOff(e.get("OFF")),order:parseOrder(e.get("Order")),groups:[...t]}}setActualNumPages(e=null){this._actualNumPages=e}get hasActualNumPages(){return null!==this._actualNumPages}get _pagesCount(){const e=this.toplevelPagesDict.get("Count");if(!Number.isInteger(e))throw new FormatError("Page count in top-level pages dictionary is not an integer.");return shadow(this,"_pagesCount",e)}get numPages(){return this.hasActualNumPages?this._actualNumPages:this._pagesCount}get destinations(){const e=this.#ie(),t=Object.create(null);for(const a of e)if(a instanceof NameTree)for(const[e,r]of a.getAll()){const a=fetchDest(r);a&&(t[stringToPDFString(e)]=a)}else if(a instanceof Dict)for(const[e,r]of a){const a=fetchDest(r);a&&(t[stringToPDFString(e)]||=a)}return shadow(this,"destinations",t)}getDestination(e){const t=this.#ie();for(const a of t)if(a instanceof NameTree||a instanceof Dict){const t=fetchDest(a.get(e));if(t)return t}if(t.length){const t=this.destinations[e];if(t)return t}return null}#ie(){const e=this._catDict.get("Names"),t=[];e?.has("Dests")&&t.push(new NameTree(e.getRaw("Dests"),this.xref));this._catDict.has("Dests")&&t.push(this._catDict.get("Dests"));return t}get pageLabels(){let e=null;try{e=this._readPageLabels()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read page labels.")}return shadow(this,"pageLabels",e)}_readPageLabels(){const e=this._catDict.getRaw("PageLabels");if(!e)return null;const t=new Array(this.numPages);let a=null,r="";const i=new NumberTree(e,this.xref).getAll();let n="",s=1;for(let e=0,o=this.numPages;e=1))throw new FormatError("Invalid start in PageLabel dictionary.");s=e}else s=1}switch(a){case"D":n=s;break;case"R":case"r":n=toRomanNumerals(s,"r"===a);break;case"A":case"a":const e=26,t="a"===a?97:65,r=s-1;n=String.fromCharCode(t+r%e).repeat(Math.floor(r/e)+1);break;default:if(a)throw new FormatError(`Invalid style "${a}" in PageLabel dictionary.`);n=""}t[e]=r+n;s++}return t}get pageLayout(){const e=this._catDict.get("PageLayout");let t="";if(e instanceof Name)switch(e.name){case"SinglePage":case"OneColumn":case"TwoColumnLeft":case"TwoColumnRight":case"TwoPageLeft":case"TwoPageRight":t=e.name}return shadow(this,"pageLayout",t)}get pageMode(){const e=this._catDict.get("PageMode");let t="UseNone";if(e instanceof Name)switch(e.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"FullScreen":case"UseOC":case"UseAttachments":t=e.name}return shadow(this,"pageMode",t)}get viewerPreferences(){const e=this._catDict.get("ViewerPreferences");if(!(e instanceof Dict))return shadow(this,"viewerPreferences",null);let t=null;for(const[a,r]of e){let e;switch(a){case"HideToolbar":case"HideMenubar":case"HideWindowUI":case"FitWindow":case"CenterWindow":case"DisplayDocTitle":case"PickTrayByPDFSize":"boolean"==typeof r&&(e=r);break;case"NonFullScreenPageMode":if(r instanceof Name)switch(r.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"UseOC":e=r.name;break;default:e="UseNone"}break;case"Direction":if(r instanceof Name)switch(r.name){case"L2R":case"R2L":e=r.name;break;default:e="L2R"}break;case"ViewArea":case"ViewClip":case"PrintArea":case"PrintClip":if(r instanceof Name)switch(r.name){case"MediaBox":case"CropBox":case"BleedBox":case"TrimBox":case"ArtBox":e=r.name;break;default:e="CropBox"}break;case"PrintScaling":if(r instanceof Name)switch(r.name){case"None":case"AppDefault":e=r.name;break;default:e="AppDefault"}break;case"Duplex":if(r instanceof Name)switch(r.name){case"Simplex":case"DuplexFlipShortEdge":case"DuplexFlipLongEdge":e=r.name;break;default:e="None"}break;case"PrintPageRange":if(Array.isArray(r)&&r.length%2==0){r.every(((e,t,a)=>Number.isInteger(e)&&e>0&&(0===t||e>=a[t-1])&&e<=this.numPages))&&(e=r)}break;case"NumCopies":Number.isInteger(r)&&r>0&&(e=r);break;default:warn(`Ignoring non-standard key in ViewerPreferences: ${a}.`);continue}if(void 0!==e){t??=Object.create(null);t[a]=e}else warn(`Bad value, for key "${a}", in ViewerPreferences: ${r}.`)}return shadow(this,"viewerPreferences",t)}get openAction(){const e=this._catDict.get("OpenAction"),t=Object.create(null);if(e instanceof Dict){const a=new Dict(this.xref);a.set("A",e);const r={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:a,resultObj:r});Array.isArray(r.dest)?t.dest=r.dest:r.action&&(t.action=r.action)}else Array.isArray(e)&&(t.dest=e);return shadow(this,"openAction",objectSize(t)>0?t:null)}get attachments(){const e=this._catDict.get("Names");let t=null;if(e instanceof Dict&&e.has("EmbeddedFiles")){const a=new NameTree(e.getRaw("EmbeddedFiles"),this.xref);for(const[e,r]of a.getAll()){const a=new FileSpec(r,this.xref);t??=Object.create(null);t[stringToPDFString(e)]=a.serializable}}return shadow(this,"attachments",t)}get xfaImages(){const e=this._catDict.get("Names");let t=null;if(e instanceof Dict&&e.has("XFAImages")){const a=new NameTree(e.getRaw("XFAImages"),this.xref);for(const[e,r]of a.getAll()){t??=new Dict(this.xref);t.set(stringToPDFString(e),r)}}return shadow(this,"xfaImages",t)}_collectJavaScript(){const e=this._catDict.get("Names");let t=null;function appendIfJavaScriptDict(e,a){if(!(a instanceof Dict))return;if(!isName(a.get("S"),"JavaScript"))return;let r=a.get("JS");if(r instanceof BaseStream)r=r.getString();else if("string"!=typeof r)return;r=stringToPDFString(r).replaceAll("\0","");r&&(t||=new Map).set(e,r)}if(e instanceof Dict&&e.has("JavaScript")){const t=new NameTree(e.getRaw("JavaScript"),this.xref);for(const[e,a]of t.getAll())appendIfJavaScriptDict(stringToPDFString(e),a)}const a=this._catDict.get("OpenAction");a&&appendIfJavaScriptDict("OpenAction",a);return t}get jsActions(){const e=this._collectJavaScript();let t=collectActions(this.xref,this._catDict,we);if(e){t||=Object.create(null);for(const[a,r]of e)a in t?t[a].push(r):t[a]=[r]}return shadow(this,"jsActions",t)}async cleanup(e=!1){clearGlobalCaches();this.globalColorSpaceCache.clear();this.globalImageCache.clear(e);this.pageKidsCountCache.clear();this.pageIndexCache.clear();this.pageDictCache.clear();this.nonBlendModesSet.clear();for(const{dict:e}of await Promise.all(this.fontCache))delete e.cacheKey;this.fontCache.clear();this.builtInCMapCache.clear();this.standardFontDataCache.clear();this.systemFontCache.clear()}async getPageDict(e){const t=[this.toplevelPagesDict],a=new RefSet,r=this._catDict.getRaw("Pages");r instanceof Ref&&a.put(r);const i=this.xref,n=this.pageKidsCountCache,s=this.pageIndexCache,o=this.pageDictCache;let c=0;for(;t.length;){const r=t.pop();if(r instanceof Ref){const l=n.get(r);if(l>=0&&c+l<=e){c+=l;continue}if(a.has(r))throw new FormatError("Pages tree contains circular reference.");a.put(r);const h=await(o.get(r)||i.fetchAsync(r));if(h instanceof Dict){let t=h.getRaw("Type");t instanceof Ref&&(t=await i.fetchAsync(t));if(isName(t,"Page")||!h.has("Kids")){n.has(r)||n.put(r,1);s.has(r)||s.put(r,c);if(c===e)return[h,r];c++;continue}}t.push(h);continue}if(!(r instanceof Dict))throw new FormatError("Page dictionary kid reference points to wrong type of object.");const{objId:l}=r;let h=r.getRaw("Count");h instanceof Ref&&(h=await i.fetchAsync(h));if(Number.isInteger(h)&&h>=0){l&&!n.has(l)&&n.put(l,h);if(c+h<=e){c+=h;continue}}let u=r.getRaw("Kids");u instanceof Ref&&(u=await i.fetchAsync(u));if(!Array.isArray(u)){let t=r.getRaw("Type");t instanceof Ref&&(t=await i.fetchAsync(t));if(isName(t,"Page")||!r.has("Kids")){if(c===e)return[r,null];c++;continue}throw new FormatError("Page dictionary kids object is not an array.")}for(let e=u.length-1;e>=0;e--){const a=u[e];t.push(a);r===this.toplevelPagesDict&&a instanceof Ref&&!o.has(a)&&o.put(a,i.fetchAsync(a))}}throw new Error(`Page index ${e} not found.`)}async getAllPageDicts(e=!1){const{ignoreErrors:t}=this.pdfManager.evaluatorOptions,a=[{currentNode:this.toplevelPagesDict,posInKids:0}],r=new RefSet,i=this._catDict.getRaw("Pages");i instanceof Ref&&r.put(i);const n=new Map,s=this.xref,o=this.pageIndexCache;let c=0;function addPageDict(e,t){t&&!o.has(t)&&o.put(t,c);n.set(c++,[e,t])}function addPageError(a){if(a instanceof XRefEntryException&&!e)throw a;if(e&&t&&0===c){warn(`getAllPageDicts - Skipping invalid first page: "${a}".`);a=Dict.empty}n.set(c++,[a,null])}for(;a.length>0;){const e=a.at(-1),{currentNode:t,posInKids:i}=e;let n=t.getRaw("Kids");if(n instanceof Ref)try{n=await s.fetchAsync(n)}catch(e){addPageError(e);break}if(!Array.isArray(n)){addPageError(new FormatError("Page dictionary kids object is not an array."));break}if(i>=n.length){a.pop();continue}const o=n[i];let c;if(o instanceof Ref){if(r.has(o)){addPageError(new FormatError("Pages tree contains circular reference."));break}r.put(o);try{c=await s.fetchAsync(o)}catch(e){addPageError(e);break}}else c=o;if(!(c instanceof Dict)){addPageError(new FormatError("Page dictionary kid reference points to wrong type of object."));break}let l=c.getRaw("Type");if(l instanceof Ref)try{l=await s.fetchAsync(l)}catch(e){addPageError(e);break}isName(l,"Page")||!c.has("Kids")?addPageDict(c,o instanceof Ref?o:null):a.push({currentNode:c,posInKids:0});e.posInKids++}return n}getPageIndex(e){const t=this.pageIndexCache.get(e);if(void 0!==t)return Promise.resolve(t);const a=this.xref;let r=0;const next=t=>function pagesBeforeRef(t){let r,i=0;return a.fetchAsync(t).then((function(a){if(isRefsEqual(t,e)&&!isDict(a,"Page")&&!(a instanceof Dict&&!a.has("Type")&&a.has("Contents")))throw new FormatError("The reference does not point to a /Page dictionary.");if(!a)return null;if(!(a instanceof Dict))throw new FormatError("Node must be a dictionary.");r=a.getRaw("Parent");return a.getAsync("Parent")})).then((function(e){if(!e)return null;if(!(e instanceof Dict))throw new FormatError("Parent must be a dictionary.");return e.getAsync("Kids")})).then((function(e){if(!e)return null;const n=[];let s=!1;for(const r of e){if(!(r instanceof Ref))throw new FormatError("Kid must be a reference.");if(isRefsEqual(r,t)){s=!0;break}n.push(a.fetchAsync(r).then((function(e){if(!(e instanceof Dict))throw new FormatError("Kid node must be a dictionary.");e.has("Count")?i+=e.get("Count"):i++})))}if(!s)throw new FormatError("Kid reference not found in parent's kids.");return Promise.all(n).then((()=>[i,r]))}))}(t).then((t=>{if(!t){this.pageIndexCache.put(e,r);return r}const[a,i]=t;r+=a;return next(i)}));return next(e)}get baseUrl(){const e=this._catDict.get("URI");if(e instanceof Dict){const t=e.get("Base");if("string"==typeof t){const e=createValidAbsoluteUrl(t,null,{tryConvertEncoding:!0});if(e)return shadow(this,"baseUrl",e.href)}}return shadow(this,"baseUrl",this.pdfManager.docBaseUrl)}static parseDestDictionary({destDict:e,resultObj:t,docBaseUrl:a=null,docAttachments:r=null}){if(!(e instanceof Dict)){warn("parseDestDictionary: `destDict` must be a dictionary.");return}let i,n,s=e.get("A");if(!(s instanceof Dict))if(e.has("Dest"))s=e.get("Dest");else{s=e.get("AA");s instanceof Dict&&(s.has("D")?s=s.get("D"):s.has("U")&&(s=s.get("U")))}if(s instanceof Dict){const e=s.get("S");if(!(e instanceof Name)){warn("parseDestDictionary: Invalid type in Action dictionary.");return}const a=e.name;switch(a){case"ResetForm":const e=s.get("Flags"),o=!(1&("number"==typeof e?e:0)),c=[],l=[];for(const e of s.get("Fields")||[])e instanceof Ref?l.push(e.toString()):"string"==typeof e&&c.push(stringToPDFString(e));t.resetForm={fields:c,refs:l,include:o};break;case"URI":i=s.get("URI");i instanceof Name&&(i="/"+i.name);break;case"GoTo":n=s.get("D");break;case"Launch":case"GoToR":const h=s.get("F");if(h instanceof Dict){const e=new FileSpec(h,null,!0),{rawFilename:t}=e.serializable;i=t}else"string"==typeof h&&(i=h);const u=fetchRemoteDest(s);u&&"string"==typeof i&&(i=i.split("#",1)[0]+"#"+u);const d=s.get("NewWindow");"boolean"==typeof d&&(t.newWindow=d);break;case"GoToE":const f=s.get("T");let g;if(r&&f instanceof Dict){const e=f.get("R"),t=f.get("N");isName(e,"C")&&"string"==typeof t&&(g=r[stringToPDFString(t)])}if(g){t.attachment=g;const e=fetchRemoteDest(s);e&&(t.attachmentDest=e)}else warn('parseDestDictionary - unimplemented "GoToE" action.');break;case"Named":const p=s.get("N");p instanceof Name&&(t.action=p.name);break;case"SetOCGState":const m=s.get("State"),b=s.get("PreserveRB");if(!Array.isArray(m)||0===m.length)break;const y=[];for(const e of m)if(e instanceof Name)switch(e.name){case"ON":case"OFF":case"Toggle":y.push(e.name)}else e instanceof Ref&&y.push(e.toString());if(y.length!==m.length)break;t.setOCGState={state:y,preserveRB:"boolean"!=typeof b||b};break;case"JavaScript":const w=s.get("JS");let S;w instanceof BaseStream?S=w.getString():"string"==typeof w&&(S=w);const x=S&&recoverJsURL(stringToPDFString(S));if(x){i=x.url;t.newWindow=x.newWindow;break}default:if("JavaScript"===a||"SubmitForm"===a)break;warn(`parseDestDictionary - unsupported action: "${a}".`)}}else e.has("Dest")&&(n=e.get("Dest"));if("string"==typeof i){const e=createValidAbsoluteUrl(i,a,{addDefaultProtocol:!0,tryConvertEncoding:!0});e&&(t.url=e.href);t.unsafeUrl=i}if(n){n instanceof Name&&(n=n.name);"string"==typeof n?t.dest=stringToPDFString(n):Jn(n)&&(t.dest=n)}}}function addChildren(e,t){if(e instanceof Dict)e=e.getRawValues();else if(e instanceof BaseStream)e=e.dict.getRawValues();else if(!Array.isArray(e))return;for(const r of e)((a=r)instanceof Ref||a instanceof Dict||a instanceof BaseStream||Array.isArray(a))&&t.push(r);var a}class ObjectLoader{constructor(e,t,a){this.dict=e;this.keys=t;this.xref=a;this.refSet=null}async load(){if(this.xref.stream.isDataLoaded)return;const{keys:e,dict:t}=this;this.refSet=new RefSet;const a=[];for(const r of e){const e=t.getRaw(r);void 0!==e&&a.push(e)}return this._walk(a)}async _walk(e){const t=[],a=[];for(;e.length;){let r=e.pop();if(r instanceof Ref){if(this.refSet.has(r))continue;try{this.refSet.put(r);r=this.xref.fetch(r)}catch(e){if(!(e instanceof MissingDataException)){warn(`ObjectLoader._walk - requesting all data: "${e}".`);this.refSet=null;const{manager:t}=this.xref.stream;return t.requestAllChunks()}t.push(r);a.push({begin:e.begin,end:e.end})}}if(r instanceof BaseStream){const e=r.getBaseStreams();if(e){let i=!1;for(const t of e)if(!t.isDataLoaded){i=!0;a.push({begin:t.start,end:t.end})}i&&t.push(r)}}addChildren(r,e)}if(a.length){await this.xref.stream.manager.requestRanges(a);for(const e of t)e instanceof Ref&&this.refSet.remove(e);return this._walk(t)}this.refSet=null}}const Yn=Symbol(),Zn=Symbol(),Qn=Symbol(),es=Symbol(),ts=Symbol(),as=Symbol(),rs=Symbol(),is=Symbol(),ns=Symbol(),ss=Symbol("content"),os=Symbol("data"),cs=Symbol(),ls=Symbol("extra"),hs=Symbol(),us=Symbol(),ds=Symbol(),fs=Symbol(),gs=Symbol(),ps=Symbol(),ms=Symbol(),bs=Symbol(),ys=Symbol(),ws=Symbol(),Ss=Symbol(),xs=Symbol(),As=Symbol(),ks=Symbol(),Cs=Symbol(),vs=Symbol(),Fs=Symbol(),Is=Symbol(),Os=Symbol(),Ts=Symbol(),Ms=Symbol(),Ds=Symbol(),Bs=Symbol(),Rs=Symbol(),Ns=Symbol(),Es=Symbol(),Ls=Symbol(),_s=Symbol(),js=Symbol(),Us=Symbol(),Xs=Symbol(),qs=Symbol(),Hs=Symbol("namespaceId"),Ws=Symbol("nodeName"),zs=Symbol(),$s=Symbol(),Gs=Symbol(),Vs=Symbol(),Ks=Symbol(),Js=Symbol(),Ys=Symbol(),Zs=Symbol(),Qs=Symbol("root"),eo=Symbol(),to=Symbol(),ao=Symbol(),ro=Symbol(),io=Symbol(),no=Symbol(),so=Symbol(),oo=Symbol(),co=Symbol(),lo=Symbol(),ho=Symbol(),uo=Symbol("uid"),fo=Symbol(),go={config:{id:0,check:e=>e.startsWith("http://www.xfa.org/schema/xci/")},connectionSet:{id:1,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-connection-set/")},datasets:{id:2,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-data/")},form:{id:3,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-form/")},localeSet:{id:4,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-locale-set/")},pdf:{id:5,check:e=>"http://ns.adobe.com/xdp/pdf/"===e},signature:{id:6,check:e=>"http://www.w3.org/2000/09/xmldsig#"===e},sourceSet:{id:7,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-source-set/")},stylesheet:{id:8,check:e=>"http://www.w3.org/1999/XSL/Transform"===e},template:{id:9,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-template/")},xdc:{id:10,check:e=>e.startsWith("http://www.xfa.org/schema/xdc/")},xdp:{id:11,check:e=>"http://ns.adobe.com/xdp/"===e},xfdf:{id:12,check:e=>"http://ns.adobe.com/xfdf/"===e},xhtml:{id:13,check:e=>"http://www.w3.org/1999/xhtml"===e},xmpmeta:{id:14,check:e=>"http://ns.adobe.com/xmpmeta/"===e}},po={pt:e=>e,cm:e=>e/2.54*72,mm:e=>e/25.4*72,in:e=>72*e,px:e=>e},mo=/([+-]?\d+\.?\d*)(.*)/;function stripQuotes(e){return e.startsWith("'")||e.startsWith('"')?e.slice(1,-1):e}function getInteger({data:e,defaultValue:t,validate:a}){if(!e)return t;e=e.trim();const r=parseInt(e,10);return!isNaN(r)&&a(r)?r:t}function getFloat({data:e,defaultValue:t,validate:a}){if(!e)return t;e=e.trim();const r=parseFloat(e);return!isNaN(r)&&a(r)?r:t}function getKeyword({data:e,defaultValue:t,validate:a}){return e&&a(e=e.trim())?e:t}function getStringOption(e,t){return getKeyword({data:e,defaultValue:t[0],validate:e=>t.includes(e)})}function getMeasurement(e,t="0"){t||="0";if(!e)return getMeasurement(t);const a=e.trim().match(mo);if(!a)return getMeasurement(t);const[,r,i]=a,n=parseFloat(r);if(isNaN(n))return getMeasurement(t);if(0===n)return 0;const s=po[i];return s?s(n):n}function getRatio(e){if(!e)return{num:1,den:1};const t=e.split(":",2).map((e=>parseFloat(e.trim()))).filter((e=>!isNaN(e)));1===t.length&&t.push(1);if(0===t.length)return{num:1,den:1};const[a,r]=t;return{num:a,den:r}}function getRelevant(e){return e?e.trim().split(/\s+/).map((e=>({excluded:"-"===e[0],viewname:e.substring(1)}))):[]}class HTMLResult{static get FAILURE(){return shadow(this,"FAILURE",new HTMLResult(!1,null,null,null))}static get EMPTY(){return shadow(this,"EMPTY",new HTMLResult(!0,null,null,null))}constructor(e,t,a,r){this.success=e;this.html=t;this.bbox=a;this.breakNode=r}isBreak(){return!!this.breakNode}static breakNode(e){return new HTMLResult(!1,null,null,e)}static success(e,t=null){return new HTMLResult(!0,e,t,null)}}class FontFinder{constructor(e){this.fonts=new Map;this.cache=new Map;this.warned=new Set;this.defaultFont=null;this.add(e)}add(e,t=null){for(const t of e)this.addPdfFont(t);for(const e of this.fonts.values())e.regular||(e.regular=e.italic||e.bold||e.bolditalic);if(!t||0===t.size)return;const a=this.fonts.get("PdfJS-Fallback-PdfJS-XFA");for(const e of t)this.fonts.set(e,a)}addPdfFont(e){const t=e.cssFontInfo,a=t.fontFamily;let r=this.fonts.get(a);if(!r){r=Object.create(null);this.fonts.set(a,r);this.defaultFont||(this.defaultFont=r)}let i="";const n=parseFloat(t.fontWeight);0!==parseFloat(t.italicAngle)?i=n>=700?"bolditalic":"italic":n>=700&&(i="bold");if(!i){(e.name.includes("Bold")||e.psName?.includes("Bold"))&&(i="bold");(e.name.includes("Italic")||e.name.endsWith("It")||e.psName?.includes("Italic")||e.psName?.endsWith("It"))&&(i+="italic")}i||(i="regular");r[i]=e}getDefault(){return this.defaultFont}find(e,t=!0){let a=this.fonts.get(e)||this.cache.get(e);if(a)return a;const r=/,|-|_| |bolditalic|bold|italic|regular|it/gi;let i=e.replaceAll(r,"");a=this.fonts.get(i);if(a){this.cache.set(e,a);return a}i=i.toLowerCase();const n=[];for(const[e,t]of this.fonts.entries())e.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(t);if(0===n.length)for(const[,e]of this.fonts.entries())e.regular.name?.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(e);if(0===n.length){i=i.replaceAll(/psmt|mt/gi,"");for(const[e,t]of this.fonts.entries())e.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(t)}if(0===n.length)for(const e of this.fonts.values())e.regular.name?.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(e);if(n.length>=1){1!==n.length&&t&&warn(`XFA - Too many choices to guess the correct font: ${e}`);this.cache.set(e,n[0]);return n[0]}if(t&&!this.warned.has(e)){this.warned.add(e);warn(`XFA - Cannot find the font: ${e}`)}return null}}function selectFont(e,t){return"italic"===e.posture?"bold"===e.weight?t.bolditalic:t.italic:"bold"===e.weight?t.bold:t.regular}class FontInfo{constructor(e,t,a,r){this.lineHeight=a;this.paraMargin=t||{top:0,bottom:0,left:0,right:0};if(!e){[this.pdfFont,this.xfaFont]=this.defaultFont(r);return}this.xfaFont={typeface:e.typeface,posture:e.posture,weight:e.weight,size:e.size,letterSpacing:e.letterSpacing};const i=r.find(e.typeface);if(i){this.pdfFont=selectFont(e,i);this.pdfFont||([this.pdfFont,this.xfaFont]=this.defaultFont(r))}else[this.pdfFont,this.xfaFont]=this.defaultFont(r)}defaultFont(e){const t=e.find("Helvetica",!1)||e.find("Myriad Pro",!1)||e.find("Arial",!1)||e.getDefault();if(t?.regular){const e=t.regular;return[e,{typeface:e.cssFontInfo.fontFamily,posture:"normal",weight:"normal",size:10,letterSpacing:0}]}return[null,{typeface:"Courier",posture:"normal",weight:"normal",size:10,letterSpacing:0}]}}class FontSelector{constructor(e,t,a,r){this.fontFinder=r;this.stack=[new FontInfo(e,t,a,r)]}pushData(e,t,a){const r=this.stack.at(-1);for(const t of["typeface","posture","weight","size","letterSpacing"])e[t]||(e[t]=r.xfaFont[t]);for(const e of["top","bottom","left","right"])isNaN(t[e])&&(t[e]=r.paraMargin[e]);const i=new FontInfo(e,t,a||r.lineHeight,this.fontFinder);i.pdfFont||(i.pdfFont=r.pdfFont);this.stack.push(i)}popFont(){this.stack.pop()}topFont(){return this.stack.at(-1)}}class TextMeasure{constructor(e,t,a,r){this.glyphs=[];this.fontSelector=new FontSelector(e,t,a,r);this.extraHeight=0}pushData(e,t,a){this.fontSelector.pushData(e,t,a)}popFont(e){return this.fontSelector.popFont()}addPara(){const e=this.fontSelector.topFont();this.extraHeight+=e.paraMargin.top+e.paraMargin.bottom}addString(e){if(!e)return;const t=this.fontSelector.topFont(),a=t.xfaFont.size;if(t.pdfFont){const r=t.xfaFont.letterSpacing,i=t.pdfFont,n=i.lineHeight||1.2,s=t.lineHeight||Math.max(1.2,n)*a,o=n-(void 0===i.lineGap?.2:i.lineGap),c=Math.max(1,o)*a,l=a/1e3,h=i.defaultWidth||i.charsToGlyphs(" ")[0].width;for(const t of e.split(/[\u2029\n]/)){const e=i.encodeString(t).join(""),a=i.charsToGlyphs(e);for(const e of a){const t=e.width||h;this.glyphs.push([t*l+r,s,c,e.unicode,!1])}this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}else{for(const t of e.split(/[\u2029\n]/)){for(const e of t.split(""))this.glyphs.push([a,1.2*a,a,e,!1]);this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}}compute(e){let t=-1,a=0,r=0,i=0,n=0,s=0,o=!1,c=!0;for(let l=0,h=this.glyphs.length;le){r=Math.max(r,n);n=0;i+=s;s=m;t=-1;a=0;o=!0;c=!1}else{s=Math.max(m,s);a=n;n+=h;t=l}else if(n+h>e){i+=s;s=m;if(-1!==t){l=t;r=Math.max(r,a);n=0;t=-1;a=0}else{r=Math.max(r,n);n=h}o=!0;c=!1}else{n+=h;s=Math.max(m,s)}}r=Math.max(r,n);i+=s+this.extraHeight;return{width:1.02*r,height:i,isBroken:o}}}const bo=/^[^.[]+/,yo=/^[^\]]+/,wo=0,So=1,xo=2,Ao=3,ko=4,Co=new Map([["$data",(e,t)=>e.datasets?e.datasets.data:e],["$record",(e,t)=>(e.datasets?e.datasets.data:e)[xs]()[0]],["$template",(e,t)=>e.template],["$connectionSet",(e,t)=>e.connectionSet],["$form",(e,t)=>e.form],["$layout",(e,t)=>e.layout],["$host",(e,t)=>e.host],["$dataWindow",(e,t)=>e.dataWindow],["$event",(e,t)=>e.event],["!",(e,t)=>e.datasets],["$xfa",(e,t)=>e],["xfa",(e,t)=>e],["$",(e,t)=>t]]),vo=new WeakMap;function parseExpression(e,t,a=!0){let r=e.match(bo);if(!r)return null;let[i]=r;const n=[{name:i,cacheName:"."+i,index:0,js:null,formCalc:null,operator:wo}];let s=i.length;for(;s0&&h.push(e)}if(0!==h.length||o||0!==c)e=isFinite(l)?h.filter((e=>le[l])):h.flat();else{const a=t[vs]();if(!(t=a))return null;c=-1;e=[t]}}return 0===e.length?null:e}function createDataNode(e,t,a){const r=parseExpression(a);if(!r)return null;if(r.some((e=>e.operator===So)))return null;const i=Co.get(r[0].name);let n=0;if(i){e=i(e,t);n=1}else e=t||e;for(let t=r.length;ne[so]())).join("")}get[Oo](){const e=Object.getPrototypeOf(this);if(!e._attributes){const t=e._attributes=new Set;for(const e of Object.getOwnPropertyNames(this)){if(null===this[e]||this[e]instanceof XFAObject||this[e]instanceof XFAObjectArray)break;t.add(e)}}return shadow(this,Oo,e._attributes)}[Es](e){let t=this;for(;t;){if(t===e)return!0;t=t[vs]()}return!1}[vs](){return this[Uo]}[Cs](){return this[vs]()}[xs](e=null){return e?this[e]:this[Mo]}[cs](){const e=Object.create(null);this[ss]&&(e.$content=this[ss]);for(const t of Object.getOwnPropertyNames(this)){const a=this[t];null!==a&&(a instanceof XFAObject?e[t]=a[cs]():a instanceof XFAObjectArray?a.isEmpty()||(e[t]=a.dump()):e[t]=a)}return e}[ho](){return null}[co](){return HTMLResult.EMPTY}*[As](){for(const e of this[xs]())yield e}*[No](e,t){for(const a of this[As]())if(!e||t===e.has(a[Ws])){const e=this[gs](),t=a[co](e);t.success||(this[ls].failingNode=a);yield t}}[us](){return null}[Zn](e,t){this[ls].children.push(e)}[gs](){}[es]({filter:e=null,include:t=!0}){if(this[ls].generator){const e=this[gs](),t=this[ls].failingNode[co](e);if(!t.success)return t;t.html&&this[Zn](t.html,t.bbox);delete this[ls].failingNode}else this[ls].generator=this[No](e,t);for(;;){const e=this[ls].generator.next();if(e.done)break;const t=e.value;if(!t.success)return t;t.html&&this[Zn](t.html,t.bbox)}this[ls].generator=null;return HTMLResult.EMPTY}[ro](e){this[qo]=new Set(Object.keys(e))}[Po](e){const t=this[Oo],a=this[qo];return[...e].filter((e=>t.has(e)&&!a.has(e)))}[eo](e,t=new Set){for(const a of this[Mo])a[Xo](e,t)}[Xo](e,t){const a=this[Eo](e,t);a?this[Fo](a,e,t):this[eo](e,t)}[Eo](e,t){const{use:a,usehref:r}=this;if(!a&&!r)return null;let i=null,n=null,s=null,o=a;if(r){o=r;r.startsWith("#som(")&&r.endsWith(")")?n=r.slice(5,-1):r.startsWith(".#som(")&&r.endsWith(")")?n=r.slice(6,-1):r.startsWith("#")?s=r.slice(1):r.startsWith(".#")&&(s=r.slice(2))}else a.startsWith("#")?s=a.slice(1):n=a;this.use=this.usehref="";if(s)i=e.get(s);else{i=searchNode(e.get(Qs),this,n,!0,!1);i&&(i=i[0])}if(!i){warn(`XFA - Invalid prototype reference: ${o}.`);return null}if(i[Ws]!==this[Ws]){warn(`XFA - Incompatible prototype: ${i[Ws]} !== ${this[Ws]}.`);return null}if(t.has(i)){warn("XFA - Cycle detected in prototypes use.");return null}t.add(i);const c=i[Eo](e,t);c&&i[Fo](c,e,t);i[eo](e,t);t.delete(i);return i}[Fo](e,t,a){if(a.has(e)){warn("XFA - Cycle detected in prototypes use.");return}!this[ss]&&e[ss]&&(this[ss]=e[ss]);new Set(a).add(e);for(const t of this[Po](e[qo])){this[t]=e[t];this[qo]&&this[qo].add(t)}for(const r of Object.getOwnPropertyNames(this)){if(this[Oo].has(r))continue;const i=this[r],n=e[r];if(i instanceof XFAObjectArray){for(const e of i[Mo])e[Xo](t,a);for(let r=i[Mo].length,s=n[Mo].length;rXFAObject[Do](e))):"object"==typeof e&&null!==e?Object.assign({},e):e}[is](){const e=Object.create(Object.getPrototypeOf(this));for(const t of Object.getOwnPropertySymbols(this))try{e[t]=this[t]}catch{shadow(e,t,this[t])}e[uo]=`${e[Ws]}${Wo++}`;e[Mo]=[];for(const t of Object.getOwnPropertyNames(this)){if(this[Oo].has(t)){e[t]=XFAObject[Do](this[t]);continue}const a=this[t];e[t]=a instanceof XFAObjectArray?new XFAObjectArray(a[_o]):null}for(const t of this[Mo]){const a=t[Ws],r=t[is]();e[Mo].push(r);r[Uo]=e;null===e[a]?e[a]=r:e[a][Mo].push(r)}return e}[xs](e=null){return e?this[Mo].filter((t=>t[Ws]===e)):this[Mo]}[ps](e){return this[e]}[ms](e,t,a=!0){return Array.from(this[bs](e,t,a))}*[bs](e,t,a=!0){if("parent"!==e){for(const a of this[Mo]){a[Ws]===e&&(yield a);a.name===e&&(yield a);(t||a[Us]())&&(yield*a[bs](e,t,!1))}a&&this[Oo].has(e)&&(yield new XFAAttribute(this,e,this[e]))}else yield this[Uo]}}class XFAObjectArray{constructor(e=1/0){this[_o]=e;this[Mo]=[]}get isXFAObject(){return!1}get isXFAObjectArray(){return!0}push(e){if(this[Mo].length<=this[_o]){this[Mo].push(e);return!0}warn(`XFA - node "${e[Ws]}" accepts no more than ${this[_o]} children`);return!1}isEmpty(){return 0===this[Mo].length}dump(){return 1===this[Mo].length?this[Mo][0][cs]():this[Mo].map((e=>e[cs]()))}[is](){const e=new XFAObjectArray(this[_o]);e[Mo]=this[Mo].map((e=>e[is]()));return e}get children(){return this[Mo]}clear(){this[Mo].length=0}}class XFAAttribute{constructor(e,t,a){this[Uo]=e;this[Ws]=t;this[ss]=a;this[ns]=!1;this[uo]="attribute"+Wo++}[vs](){return this[Uo]}[Ns](){return!0}[ys](){return this[ss].trim()}[io](e){e=e.value||"";this[ss]=e.toString()}[so](){return this[ss]}[Es](e){return this[Uo]===e||this[Uo][Es](e)}}class XmlObject extends XFAObject{constructor(e,t,a={}){super(e,t);this[ss]="";this[Bo]=null;if("#text"!==t){const e=new Map;this[Io]=e;for(const[t,r]of Object.entries(a))e.set(t,new XFAAttribute(this,t,r));if(a.hasOwnProperty(zs)){const e=a[zs].xfa.dataNode;void 0!==e&&("dataGroup"===e?this[Bo]=!1:"dataValue"===e&&(this[Bo]=!0))}}this[ns]=!1}[lo](e){const t=this[Ws];if("#text"===t){e.push(encodeToXmlString(this[ss]));return}const a=utf8StringToString(t),r=this[Hs]===zo?"xfa:":"";e.push(`<${r}${a}`);for(const[t,a]of this[Io].entries()){const r=utf8StringToString(t);e.push(` ${r}="${encodeToXmlString(a[ss])}"`)}null!==this[Bo]&&(this[Bo]?e.push(' xfa:dataNode="dataValue"'):e.push(' xfa:dataNode="dataGroup"'));if(this[ss]||0!==this[Mo].length){e.push(">");if(this[ss])"string"==typeof this[ss]?e.push(encodeToXmlString(this[ss])):this[ss][lo](e);else for(const t of this[Mo])t[lo](e);e.push(``)}else e.push("/>")}[$s](e){if(this[ss]){const e=new XmlObject(this[Hs],"#text");this[Qn](e);e[ss]=this[ss];this[ss]=""}this[Qn](e);return!0}[Vs](e){this[ss]+=e}[hs](){if(this[ss]&&this[Mo].length>0){const e=new XmlObject(this[Hs],"#text");this[Qn](e);e[ss]=this[ss];delete this[ss]}}[co](){return"#text"===this[Ws]?HTMLResult.success({name:"#text",value:this[ss]}):HTMLResult.EMPTY}[xs](e=null){return e?this[Mo].filter((t=>t[Ws]===e)):this[Mo]}[fs](){return this[Io]}[ps](e){const t=this[Io].get(e);return void 0!==t?t:this[xs](e)}*[bs](e,t){const a=this[Io].get(e);a&&(yield a);for(const a of this[Mo]){a[Ws]===e&&(yield a);t&&(yield*a[bs](e,t))}}*[ds](e,t){const a=this[Io].get(e);!a||t&&a[ns]||(yield a);for(const a of this[Mo])yield*a[ds](e,t)}*[Ss](e,t,a){for(const r of this[Mo]){r[Ws]!==e||a&&r[ns]||(yield r);t&&(yield*r[Ss](e,t,a))}}[Ns](){return null===this[Bo]?0===this[Mo].length||this[Mo][0][Hs]===go.xhtml.id:this[Bo]}[ys](){return null===this[Bo]?0===this[Mo].length?this[ss].trim():this[Mo][0][Hs]===go.xhtml.id?this[Mo][0][so]().trim():null:this[ss].trim()}[io](e){e=e.value||"";this[ss]=e.toString()}[cs](e=!1){const t=Object.create(null);e&&(t.$ns=this[Hs]);this[ss]&&(t.$content=this[ss]);t.$name=this[Ws];t.children=[];for(const a of this[Mo])t.children.push(a[cs](e));t.attributes=Object.create(null);for(const[e,a]of this[Io])t.attributes[e]=a[ss];return t}}class ContentObject extends XFAObject{constructor(e,t){super(e,t);this[ss]=""}[Vs](e){this[ss]+=e}[hs](){}}class OptionObject extends ContentObject{constructor(e,t,a){super(e,t);this[jo]=a}[hs](){this[ss]=getKeyword({data:this[ss],defaultValue:this[jo][0],validate:e=>this[jo].includes(e)})}[ts](e){super[ts](e);delete this[jo]}}class StringObject extends ContentObject{[hs](){this[ss]=this[ss].trim()}}class IntegerObject extends ContentObject{constructor(e,t,a,r){super(e,t);this[Ro]=a;this[Ho]=r}[hs](){this[ss]=getInteger({data:this[ss],defaultValue:this[Ro],validate:this[Ho]})}[ts](e){super[ts](e);delete this[Ro];delete this[Ho]}}class Option01 extends IntegerObject{constructor(e,t){super(e,t,0,(e=>1===e))}}class Option10 extends IntegerObject{constructor(e,t){super(e,t,1,(e=>0===e))}}function measureToString(e){return"string"==typeof e?"0px":Number.isInteger(e)?`${e}px`:`${e.toFixed(2)}px`}const $o={anchorType(e,t){const a=e[Cs]();if(a&&(!a.layout||"position"===a.layout)){"transform"in t||(t.transform="");switch(e.anchorType){case"bottomCenter":t.transform+="translate(-50%, -100%)";break;case"bottomLeft":t.transform+="translate(0,-100%)";break;case"bottomRight":t.transform+="translate(-100%,-100%)";break;case"middleCenter":t.transform+="translate(-50%,-50%)";break;case"middleLeft":t.transform+="translate(0,-50%)";break;case"middleRight":t.transform+="translate(-100%,-50%)";break;case"topCenter":t.transform+="translate(-50%,0)";break;case"topRight":t.transform+="translate(-100%,0)"}}},dimensions(e,t){const a=e[Cs]();let r=e.w;const i=e.h;if(a.layout?.includes("row")){const t=a[ls],i=e.colSpan;let n;if(-1===i){n=Math.sumPrecise(t.columnWidths.slice(t.currentColumn));t.currentColumn=0}else{n=Math.sumPrecise(t.columnWidths.slice(t.currentColumn,t.currentColumn+i));t.currentColumn=(t.currentColumn+e.colSpan)%t.columnWidths.length}isNaN(n)||(r=e.w=n)}t.width=""!==r?measureToString(r):"auto";t.height=""!==i?measureToString(i):"auto"},position(e,t){const a=e[Cs]();if(!a?.layout||"position"===a.layout){t.position="absolute";t.left=measureToString(e.x);t.top=measureToString(e.y)}},rotate(e,t){if(e.rotate){"transform"in t||(t.transform="");t.transform+=`rotate(-${e.rotate}deg)`;t.transformOrigin="top left"}},presence(e,t){switch(e.presence){case"invisible":t.visibility="hidden";break;case"hidden":case"inactive":t.display="none"}},hAlign(e,t){if("para"===e[Ws])switch(e.hAlign){case"justifyAll":t.textAlign="justify-all";break;case"radix":t.textAlign="left";break;default:t.textAlign=e.hAlign}else switch(e.hAlign){case"left":t.alignSelf="start";break;case"center":t.alignSelf="center";break;case"right":t.alignSelf="end"}},margin(e,t){e.margin&&(t.margin=e.margin[ho]().margin)}};function setMinMaxDimensions(e,t){if("position"===e[Cs]().layout){e.minW>0&&(t.minWidth=measureToString(e.minW));e.maxW>0&&(t.maxWidth=measureToString(e.maxW));e.minH>0&&(t.minHeight=measureToString(e.minH));e.maxH>0&&(t.maxHeight=measureToString(e.maxH))}}function layoutText(e,t,a,r,i,n){const s=new TextMeasure(t,a,r,i);"string"==typeof e?s.addString(e):e[Ks](s);return s.compute(n)}function layoutNode(e,t){let a=null,r=null,i=!1;if((!e.w||!e.h)&&e.value){let n=0,s=0;if(e.margin){n=e.margin.leftInset+e.margin.rightInset;s=e.margin.topInset+e.margin.bottomInset}let o=null,c=null;if(e.para){c=Object.create(null);o=""===e.para.lineHeight?null:e.para.lineHeight;c.top=""===e.para.spaceAbove?0:e.para.spaceAbove;c.bottom=""===e.para.spaceBelow?0:e.para.spaceBelow;c.left=""===e.para.marginLeft?0:e.para.marginLeft;c.right=""===e.para.marginRight?0:e.para.marginRight}let l=e.font;if(!l){const t=e[Fs]();let a=e[vs]();for(;a&&a!==t;){if(a.font){l=a.font;break}a=a[vs]()}}const h=(e.w||t.width)-n,u=e[Is].fontFinder;if(e.value.exData&&e.value.exData[ss]&&"text/html"===e.value.exData.contentType){const t=layoutText(e.value.exData[ss],l,c,o,u,h);r=t.width;a=t.height;i=t.isBroken}else{const t=e.value[so]();if(t){const e=layoutText(t,l,c,o,u,h);r=e.width;a=e.height;i=e.isBroken}}null===r||e.w||(r+=n);null===a||e.h||(a+=s)}return{w:r,h:a,isBroken:i}}function computeBbox(e,t,a){let r;if(""!==e.w&&""!==e.h)r=[e.x,e.y,e.w,e.h];else{if(!a)return null;let i=e.w;if(""===i){if(0===e.maxW){const t=e[Cs]();i="position"===t.layout&&""!==t.w?0:e.minW}else i=Math.min(e.maxW,a.width);t.attributes.style.width=measureToString(i)}let n=e.h;if(""===n){if(0===e.maxH){const t=e[Cs]();n="position"===t.layout&&""!==t.h?0:e.minH}else n=Math.min(e.maxH,a.height);t.attributes.style.height=measureToString(n)}r=[e.x,e.y,i,n]}return r}function fixDimensions(e){const t=e[Cs]();if(t.layout?.includes("row")){const a=t[ls],r=e.colSpan;let i;i=-1===r?Math.sumPrecise(a.columnWidths.slice(a.currentColumn)):Math.sumPrecise(a.columnWidths.slice(a.currentColumn,a.currentColumn+r));isNaN(i)||(e.w=i)}t.layout&&"position"!==t.layout&&(e.x=e.y=0);"table"===e.layout&&""===e.w&&Array.isArray(e.columnWidths)&&(e.w=Math.sumPrecise(e.columnWidths))}function layoutClass(e){switch(e.layout){case"position":default:return"xfaPosition";case"lr-tb":return"xfaLrTb";case"rl-row":return"xfaRlRow";case"rl-tb":return"xfaRlTb";case"row":return"xfaRow";case"table":return"xfaTable";case"tb":return"xfaTb"}}function toStyle(e,...t){const a=Object.create(null);for(const r of t){const t=e[r];if(null!==t)if($o.hasOwnProperty(r))$o[r](e,a);else if(t instanceof XFAObject){const e=t[ho]();e?Object.assign(a,e):warn(`(DEBUG) - XFA - style for ${r} not implemented yet`)}}return a}function createWrapper(e,t){const{attributes:a}=t,{style:r}=a,i={name:"div",attributes:{class:["xfaWrapper"],style:Object.create(null)},children:[]};a.class.push("xfaWrapped");if(e.border){const{widths:a,insets:n}=e.border[ls];let s,o,c=n[0],l=n[3];const h=n[0]+n[2],u=n[1]+n[3];switch(e.border.hand){case"even":c-=a[0]/2;l-=a[3]/2;s=`calc(100% + ${(a[1]+a[3])/2-u}px)`;o=`calc(100% + ${(a[0]+a[2])/2-h}px)`;break;case"left":c-=a[0];l-=a[3];s=`calc(100% + ${a[1]+a[3]-u}px)`;o=`calc(100% + ${a[0]+a[2]-h}px)`;break;case"right":s=u?`calc(100% - ${u}px)`:"100%";o=h?`calc(100% - ${h}px)`:"100%"}const d=["xfaBorder"];isPrintOnly(e.border)&&d.push("xfaPrintOnly");const f={name:"div",attributes:{class:d,style:{top:`${c}px`,left:`${l}px`,width:s,height:o}},children:[]};for(const e of["border","borderWidth","borderColor","borderRadius","borderStyle"])if(void 0!==r[e]){f.attributes.style[e]=r[e];delete r[e]}i.children.push(f,t)}else i.children.push(t);for(const e of["background","backgroundClip","top","left","width","height","minWidth","minHeight","maxWidth","maxHeight","transform","transformOrigin","visibility"])if(void 0!==r[e]){i.attributes.style[e]=r[e];delete r[e]}i.attributes.style.position="absolute"===r.position?"absolute":"relative";delete r.position;if(r.alignSelf){i.attributes.style.alignSelf=r.alignSelf;delete r.alignSelf}return i}function fixTextIndent(e){const t=getMeasurement(e.textIndent,"0px");if(t>=0)return;const a="padding"+("left"===("right"===e.textAlign?"right":"left")?"Left":"Right"),r=getMeasurement(e[a],"0px");e[a]=r-t+"px"}function setAccess(e,t){switch(e.access){case"nonInteractive":t.push("xfaNonInteractive");break;case"readOnly":t.push("xfaReadOnly");break;case"protected":t.push("xfaDisabled")}}function isPrintOnly(e){return e.relevant.length>0&&!e.relevant[0].excluded&&"print"===e.relevant[0].viewname}function getCurrentPara(e){const t=e[Fs]()[ls].paraStack;return t.length?t.at(-1):null}function setPara(e,t,a){if(a.attributes.class?.includes("xfaRich")){if(t){""===e.h&&(t.height="auto");""===e.w&&(t.width="auto")}const r=getCurrentPara(e);if(r){const e=a.attributes.style;e.display="flex";e.flexDirection="column";switch(r.vAlign){case"top":e.justifyContent="start";break;case"bottom":e.justifyContent="end";break;case"middle":e.justifyContent="center"}const t=r[ho]();for(const[a,r]of Object.entries(t))a in e||(e[a]=r)}}}function setFontFamily(e,t,a,r){if(!a){delete r.fontFamily;return}const i=stripQuotes(e.typeface);r.fontFamily=`"${i}"`;const n=a.find(i);if(n){const{fontFamily:a}=n.regular.cssFontInfo;a!==i&&(r.fontFamily=`"${a}"`);const s=getCurrentPara(t);if(s&&""!==s.lineHeight)return;if(r.lineHeight)return;const o=selectFont(e,n);o&&(r.lineHeight=Math.max(1.2,o.lineHeight))}}function fixURL(e){const t=createValidAbsoluteUrl(e,null,{addDefaultProtocol:!0,tryConvertEncoding:!0});return t?t.href:null}function createLine(e,t){return{name:"div",attributes:{class:["lr-tb"===e.layout?"xfaLr":"xfaRl"]},children:t}}function flushHTML(e){if(!e[ls])return null;const t={name:"div",attributes:e[ls].attributes,children:e[ls].children};if(e[ls].failingNode){const a=e[ls].failingNode[us]();a&&(e.layout.endsWith("-tb")?t.children.push(createLine(e,[a])):t.children.push(a))}return 0===t.children.length?null:t}function addHTML(e,t,a){const r=e[ls],i=r.availableSpace,[n,s,o,c]=a;switch(e.layout){case"position":r.width=Math.max(r.width,n+o);r.height=Math.max(r.height,s+c);r.children.push(t);break;case"lr-tb":case"rl-tb":if(!r.line||1===r.attempt){r.line=createLine(e,[]);r.children.push(r.line);r.numberInLine=0}r.numberInLine+=1;r.line.children.push(t);if(0===r.attempt){r.currentWidth+=o;r.height=Math.max(r.height,r.prevHeight+c)}else{r.currentWidth=o;r.prevHeight=r.height;r.height+=c;r.attempt=0}r.width=Math.max(r.width,r.currentWidth);break;case"rl-row":case"row":{r.children.push(t);r.width+=o;r.height=Math.max(r.height,c);const e=measureToString(r.height);for(const t of r.children)t.attributes.style.height=e;break}case"table":case"tb":r.width=MathClamp(o,r.width,i.width);r.height+=c;r.children.push(t)}}function getAvailableSpace(e){const t=e[ls].availableSpace,a=e.margin?e.margin.topInset+e.margin.bottomInset:0,r=e.margin?e.margin.leftInset+e.margin.rightInset:0;switch(e.layout){case"lr-tb":case"rl-tb":return 0===e[ls].attempt?{width:t.width-r-e[ls].currentWidth,height:t.height-a-e[ls].prevHeight}:{width:t.width-r,height:t.height-a-e[ls].height};case"rl-row":case"row":return{width:Math.sumPrecise(e[ls].columnWidths.slice(e[ls].currentColumn)),height:t.height-r};case"table":case"tb":return{width:t.width-r,height:t.height-a-e[ls].height};default:return t}}function checkDimensions(e,t){if(null===e[Fs]()[ls].firstUnsplittable)return!0;if(0===e.w||0===e.h)return!0;const a=e[Cs](),r=a[ls]?.attempt||0,[,i,n,s]=function getTransformedBBox(e){let t,a,r=""===e.w?NaN:e.w,i=""===e.h?NaN:e.h,[n,s]=[0,0];switch(e.anchorType||""){case"bottomCenter":[n,s]=[r/2,i];break;case"bottomLeft":[n,s]=[0,i];break;case"bottomRight":[n,s]=[r,i];break;case"middleCenter":[n,s]=[r/2,i/2];break;case"middleLeft":[n,s]=[0,i/2];break;case"middleRight":[n,s]=[r,i/2];break;case"topCenter":[n,s]=[r/2,0];break;case"topRight":[n,s]=[r,0]}switch(e.rotate||0){case 0:[t,a]=[-n,-s];break;case 90:[t,a]=[-s,n];[r,i]=[i,-r];break;case 180:[t,a]=[n,s];[r,i]=[-r,-i];break;case 270:[t,a]=[s,-n];[r,i]=[-i,r]}return[e.x+t+Math.min(0,r),e.y+a+Math.min(0,i),Math.abs(r),Math.abs(i)]}(e);switch(a.layout){case"lr-tb":case"rl-tb":return 0===r?e[Fs]()[ls].noLayoutFailure?""!==e.w?Math.round(n-t.width)<=2:t.width>2:!(""!==e.h&&Math.round(s-t.height)>2)&&(""!==e.w?Math.round(n-t.width)<=2||0===a[ls].numberInLine&&t.height>2:t.width>2):!!e[Fs]()[ls].noLayoutFailure||!(""!==e.h&&Math.round(s-t.height)>2)&&((""===e.w||Math.round(n-t.width)<=2||!a[js]())&&t.height>2);case"table":case"tb":return!!e[Fs]()[ls].noLayoutFailure||(""===e.h||e[_s]()?(""===e.w||Math.round(n-t.width)<=2||!a[js]())&&t.height>2:Math.round(s-t.height)<=2);case"position":if(e[Fs]()[ls].noLayoutFailure)return!0;if(""===e.h||Math.round(s+i-t.height)<=2)return!0;return s+i>e[Fs]()[ls].currentContentArea.h;case"rl-row":case"row":return!!e[Fs]()[ls].noLayoutFailure||(""===e.h||Math.round(s-t.height)<=2);default:return!0}}const Go=go.template.id,Vo="http://www.w3.org/2000/svg",Ko=/^H(\d+)$/,Jo=new Set(["image/gif","image/jpeg","image/jpg","image/pjpeg","image/png","image/apng","image/x-png","image/bmp","image/x-ms-bmp","image/tiff","image/tif","application/octet-stream"]),Yo=[[[66,77],"image/bmp"],[[255,216,255],"image/jpeg"],[[73,73,42,0],"image/tiff"],[[77,77,0,42],"image/tiff"],[[71,73,70,56,57,97],"image/gif"],[[137,80,78,71,13,10,26,10],"image/png"]];function getBorderDims(e){if(!e||!e.border)return{w:0,h:0};const t=e.border[ws]();return t?{w:t.widths[0]+t.widths[2]+t.insets[0]+t.insets[2],h:t.widths[1]+t.widths[3]+t.insets[1]+t.insets[3]}:{w:0,h:0}}function hasMargin(e){return e.margin&&(e.margin.topInset||e.margin.rightInset||e.margin.bottomInset||e.margin.leftInset)}function _setValue(e,t){if(!e.value){const t=new Value({});e[Qn](t);e.value=t}e.value[io](t)}function*getContainedChildren(e){for(const t of e[xs]())t instanceof SubformSet?yield*t[As]():yield t}function isRequired(e){return"error"===e.validate?.nullTest}function setTabIndex(e){for(;e;){if(!e.traversal){e[no]=e[vs]()[no];return}if(e[no])return;let t=null;for(const a of e.traversal[xs]())if("next"===a.operation){t=a;break}if(!t||!t.ref){e[no]=e[vs]()[no];return}const a=e[Fs]();e[no]=++a[no];const r=a[to](t.ref,e);if(!r)return;e=r[0]}}function applyAssist(e,t){const a=e.assist;if(a){const e=a[co]();e&&(t.title=e);const r=a.role.match(Ko);if(r){const e="heading",a=r[1];t.role=e;t["aria-level"]=a}}if("table"===e.layout)t.role="table";else if("row"===e.layout)t.role="row";else{const a=e[vs]();"row"===a.layout&&(t.role="TH"===a.assist?.role?"columnheader":"cell")}}function ariaLabel(e){if(!e.assist)return null;const t=e.assist;return t.speak&&""!==t.speak[ss]?t.speak[ss]:t.toolTip?t.toolTip[ss]:null}function valueToHtml(e){return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:Object.create(null)},children:[{name:"span",attributes:{style:Object.create(null)},value:e}]})}function setFirstUnsplittable(e){const t=e[Fs]();if(null===t[ls].firstUnsplittable){t[ls].firstUnsplittable=e;t[ls].noLayoutFailure=!0}}function unsetFirstUnsplittable(e){const t=e[Fs]();t[ls].firstUnsplittable===e&&(t[ls].noLayoutFailure=!1)}function handleBreak(e){if(e[ls])return!1;e[ls]=Object.create(null);if("auto"===e.targetType)return!1;const t=e[Fs]();let a=null;if(e.target){a=t[to](e.target,e[vs]());if(!a)return!1;a=a[0]}const{currentPageArea:r,currentContentArea:i}=t[ls];if("pageArea"===e.targetType){a instanceof PageArea||(a=null);if(e.startNew){e[ls].target=a||r;return!0}if(a&&a!==r){e[ls].target=a;return!0}return!1}a instanceof ContentArea||(a=null);const n=a&&a[vs]();let s,o=n;if(e.startNew)if(a){const e=n.contentArea.children,t=e.indexOf(i),r=e.indexOf(a);-1!==t&&te;r[ls].noLayoutFailure=!0;const s=t[co](a);e[Zn](s.html,s.bbox);r[ls].noLayoutFailure=i;t[Cs]=n}class AppearanceFilter extends StringObject{constructor(e){super(Go,"appearanceFilter");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Arc extends XFAObject{constructor(e){super(Go,"arc",!0);this.circular=getInteger({data:e.circular,defaultValue:0,validate:e=>1===e});this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.startAngle=getFloat({data:e.startAngle,defaultValue:0,validate:e=>!0});this.sweepAngle=getFloat({data:e.sweepAngle,defaultValue:360,validate:e=>!0});this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null;this.fill=null}[co](){const e=this.edge||new Edge({}),t=e[ho](),a=Object.create(null);"visible"===this.fill?.presence?Object.assign(a,this.fill[ho]()):a.fill="transparent";a.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);a.stroke=t.color;let r;const i={xmlns:Vo,style:{width:"100%",height:"100%",overflow:"visible"}};if(360===this.sweepAngle)r={name:"ellipse",attributes:{xmlns:Vo,cx:"50%",cy:"50%",rx:"50%",ry:"50%",style:a}};else{const e=this.startAngle*Math.PI/180,t=this.sweepAngle*Math.PI/180,n=this.sweepAngle>180?1:0,[s,o,c,l]=[50*(1+Math.cos(e)),50*(1-Math.sin(e)),50*(1+Math.cos(e+t)),50*(1-Math.sin(e+t))];r={name:"path",attributes:{xmlns:Vo,d:`M ${s} ${o} A 50 50 0 ${n} 0 ${c} ${l}`,vectorEffect:"non-scaling-stroke",style:a}};Object.assign(i,{viewBox:"0 0 100 100",preserveAspectRatio:"none"})}const n={name:"svg",children:[r],attributes:i};if(hasMargin(this[vs]()[vs]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[n]});n.attributes.style.position="absolute";return HTMLResult.success(n)}}class Area extends XFAObject{constructor(e){super(Go,"area",!0);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null;this.area=new XFAObjectArray;this.draw=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[As](){yield*getContainedChildren(this)}[Us](){return!0}[Rs](){return!0}[Zn](e,t){const[a,r,i,n]=t;this[ls].width=Math.max(this[ls].width,a+i);this[ls].height=Math.max(this[ls].height,r+n);this[ls].children.push(e)}[gs](){return this[ls].availableSpace}[co](e){const t=toStyle(this,"position"),a={style:t,id:this[uo],class:["xfaArea"]};isPrintOnly(this)&&a.class.push("xfaPrintOnly");this.name&&(a.xfaName=this.name);const r=[];this[ls]={children:r,width:0,height:0,availableSpace:e};const i=this[es]({filter:new Set(["area","draw","field","exclGroup","subform","subformSet"]),include:!0});if(!i.success){if(i.isBreak())return i;delete this[ls];return HTMLResult.FAILURE}t.width=measureToString(this[ls].width);t.height=measureToString(this[ls].height);const n={name:"div",attributes:a,children:r},s=[this.x,this.y,this[ls].width,this[ls].height];delete this[ls];return HTMLResult.success(n,s)}}class Assist extends XFAObject{constructor(e){super(Go,"assist",!0);this.id=e.id||"";this.role=e.role||"";this.use=e.use||"";this.usehref=e.usehref||"";this.speak=null;this.toolTip=null}[co](){return this.toolTip?.[ss]||null}}class Barcode extends XFAObject{constructor(e){super(Go,"barcode",!0);this.charEncoding=getKeyword({data:e.charEncoding?e.charEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.checksum=getStringOption(e.checksum,["none","1mod10","1mod10_1mod11","2mod10","auto"]);this.dataColumnCount=getInteger({data:e.dataColumnCount,defaultValue:-1,validate:e=>e>=0});this.dataLength=getInteger({data:e.dataLength,defaultValue:-1,validate:e=>e>=0});this.dataPrep=getStringOption(e.dataPrep,["none","flateCompress"]);this.dataRowCount=getInteger({data:e.dataRowCount,defaultValue:-1,validate:e=>e>=0});this.endChar=e.endChar||"";this.errorCorrectionLevel=getInteger({data:e.errorCorrectionLevel,defaultValue:-1,validate:e=>e>=0&&e<=8});this.id=e.id||"";this.moduleHeight=getMeasurement(e.moduleHeight,"5mm");this.moduleWidth=getMeasurement(e.moduleWidth,"0.25mm");this.printCheckDigit=getInteger({data:e.printCheckDigit,defaultValue:0,validate:e=>1===e});this.rowColumnRatio=getRatio(e.rowColumnRatio);this.startChar=e.startChar||"";this.textLocation=getStringOption(e.textLocation,["below","above","aboveEmbedded","belowEmbedded","none"]);this.truncate=getInteger({data:e.truncate,defaultValue:0,validate:e=>1===e});this.type=getStringOption(e.type?e.type.toLowerCase():"",["aztec","codabar","code2of5industrial","code2of5interleaved","code2of5matrix","code2of5standard","code3of9","code3of9extended","code11","code49","code93","code128","code128a","code128b","code128c","code128sscc","datamatrix","ean8","ean8add2","ean8add5","ean13","ean13add2","ean13add5","ean13pwcd","fim","logmars","maxicode","msi","pdf417","pdf417macro","plessey","postauscust2","postauscust3","postausreplypaid","postausstandard","postukrm4scc","postusdpbc","postusimb","postusstandard","postus5zip","qrcode","rfid","rss14","rss14expanded","rss14limited","rss14stacked","rss14stackedomni","rss14truncated","telepen","ucc128","ucc128random","ucc128sscc","upca","upcaadd2","upcaadd5","upcapwcd","upce","upceadd2","upceadd5","upcean2","upcean5","upsmaxicode"]);this.upsMode=getStringOption(e.upsMode,["usCarrier","internationalCarrier","secureSymbol","standardSymbol"]);this.use=e.use||"";this.usehref=e.usehref||"";this.wideNarrowRatio=getRatio(e.wideNarrowRatio);this.encrypt=null;this.extras=null}}class Bind extends XFAObject{constructor(e){super(Go,"bind",!0);this.match=getStringOption(e.match,["once","dataRef","global","none"]);this.ref=e.ref||"";this.picture=null}}class BindItems extends XFAObject{constructor(e){super(Go,"bindItems");this.connection=e.connection||"";this.labelRef=e.labelRef||"";this.ref=e.ref||"";this.valueRef=e.valueRef||""}}class Bookend extends XFAObject{constructor(e){super(Go,"bookend");this.id=e.id||"";this.leader=e.leader||"";this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||""}}class BooleanElement extends Option01{constructor(e){super(Go,"boolean");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[co](e){return valueToHtml(1===this[ss]?"1":"0")}}class Border extends XFAObject{constructor(e){super(Go,"border",!0);this.break=getStringOption(e.break,["close","open"]);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.extras=null;this.fill=null;this.margin=null}[ws](){if(!this[ls]){const e=this.edge.children.slice();if(e.length<4){const t=e.at(-1)||new Edge({});for(let a=e.length;a<4;a++)e.push(t)}const t=e.map((e=>e.thickness)),a=[0,0,0,0];if(this.margin){a[0]=this.margin.topInset;a[1]=this.margin.rightInset;a[2]=this.margin.bottomInset;a[3]=this.margin.leftInset}this[ls]={widths:t,insets:a,edges:e}}return this[ls]}[ho](){const{edges:e}=this[ws](),t=e.map((e=>{const t=e[ho]();t.color||="#000000";return t})),a=Object.create(null);this.margin&&Object.assign(a,this.margin[ho]());"visible"===this.fill?.presence&&Object.assign(a,this.fill[ho]());if(this.corner.children.some((e=>0!==e.radius))){const e=this.corner.children.map((e=>e[ho]()));if(2===e.length||3===e.length){const t=e.at(-1);for(let a=e.length;a<4;a++)e.push(t)}a.borderRadius=e.map((e=>e.radius)).join(" ")}switch(this.presence){case"invisible":case"hidden":a.borderStyle="";break;case"inactive":a.borderStyle="none";break;default:a.borderStyle=t.map((e=>e.style)).join(" ")}a.borderWidth=t.map((e=>e.width)).join(" ");a.borderColor=t.map((e=>e.color)).join(" ");return a}}class Break extends XFAObject{constructor(e){super(Go,"break",!0);this.after=getStringOption(e.after,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.afterTarget=e.afterTarget||"";this.before=getStringOption(e.before,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.beforeTarget=e.beforeTarget||"";this.bookendLeader=e.bookendLeader||"";this.bookendTrailer=e.bookendTrailer||"";this.id=e.id||"";this.overflowLeader=e.overflowLeader||"";this.overflowTarget=e.overflowTarget||"";this.overflowTrailer=e.overflowTrailer||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class BreakAfter extends XFAObject{constructor(e){super(Go,"breakAfter",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}}class BreakBefore extends XFAObject{constructor(e){super(Go,"breakBefore",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}[co](e){this[ls]={};return HTMLResult.FAILURE}}class Button extends XFAObject{constructor(e){super(Go,"button",!0);this.highlight=getStringOption(e.highlight,["inverted","none","outline","push"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[co](e){const t=this[vs]()[vs](),a={name:"button",attributes:{id:this[uo],class:["xfaButton"],style:{}},children:[]};for(const e of t.event.children){if("click"!==e.activity||!e.script)continue;const t=recoverJsURL(e.script[ss]);if(!t)continue;const r=fixURL(t.url);r&&a.children.push({name:"a",attributes:{id:"link"+this[uo],href:r,newWindow:t.newWindow,class:["xfaLink"],style:{}},children:[]})}return HTMLResult.success(a)}}class Calculate extends XFAObject{constructor(e){super(Go,"calculate",!0);this.id=e.id||"";this.override=getStringOption(e.override,["disabled","error","ignore","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.script=null}}class Caption extends XFAObject{constructor(e){super(Go,"caption",!0);this.id=e.id||"";this.placement=getStringOption(e.placement,["left","bottom","inline","right","top"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.reserve=Math.ceil(getMeasurement(e.reserve));this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.font=null;this.margin=null;this.para=null;this.value=null}[io](e){_setValue(this,e)}[ws](e){if(!this[ls]){let{width:t,height:a}=e;switch(this.placement){case"left":case"right":case"inline":t=this.reserve<=0?t:this.reserve;break;case"top":case"bottom":a=this.reserve<=0?a:this.reserve}this[ls]=layoutNode(this,{width:t,height:a})}return this[ls]}[co](e){if(!this.value)return HTMLResult.EMPTY;this[Ys]();const t=this.value[co](e).html;if(!t){this[Js]();return HTMLResult.EMPTY}const a=this.reserve;if(this.reserve<=0){const{w:t,h:a}=this[ws](e);switch(this.placement){case"left":case"right":case"inline":this.reserve=t;break;case"top":case"bottom":this.reserve=a}}const r=[];"string"==typeof t?r.push({name:"#text",value:t}):r.push(t);const i=toStyle(this,"font","margin","visibility");switch(this.placement){case"left":case"right":this.reserve>0&&(i.width=measureToString(this.reserve));break;case"top":case"bottom":this.reserve>0&&(i.height=measureToString(this.reserve))}setPara(this,null,t);this[Js]();this.reserve=a;return HTMLResult.success({name:"div",attributes:{style:i,class:["xfaCaption"]},children:r})}}class Certificate extends StringObject{constructor(e){super(Go,"certificate");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Certificates extends XFAObject{constructor(e){super(Go,"certificates",!0);this.credentialServerPolicy=getStringOption(e.credentialServerPolicy,["optional","required"]);this.id=e.id||"";this.url=e.url||"";this.urlPolicy=e.urlPolicy||"";this.use=e.use||"";this.usehref=e.usehref||"";this.encryption=null;this.issuers=null;this.keyUsage=null;this.oids=null;this.signing=null;this.subjectDNs=null}}class CheckButton extends XFAObject{constructor(e){super(Go,"checkButton",!0);this.id=e.id||"";this.mark=getStringOption(e.mark,["default","check","circle","cross","diamond","square","star"]);this.shape=getStringOption(e.shape,["square","round"]);this.size=getMeasurement(e.size,"10pt");this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[co](e){const t=toStyle("margin"),a=measureToString(this.size);t.width=t.height=a;let r,i,n;const s=this[vs]()[vs](),o=s.items.children.length&&s.items.children[0][co]().html||[],c={on:(void 0!==o[0]?o[0]:"on").toString(),off:(void 0!==o[1]?o[1]:"off").toString()},l=(s.value?.[so]()||"off")===c.on||void 0,h=s[Cs](),u=s[uo];let d;if(h instanceof ExclGroup){n=h[uo];r="radio";i="xfaRadio";d=h[os]?.[uo]||h[uo]}else{r="checkbox";i="xfaCheckbox";d=s[os]?.[uo]||s[uo]}const f={name:"input",attributes:{class:[i],style:t,fieldId:u,dataId:d,type:r,checked:l,xfaOn:c.on,xfaOff:c.off,"aria-label":ariaLabel(s),"aria-required":!1}};n&&(f.attributes.name=n);if(isRequired(s)){f.attributes["aria-required"]=!0;f.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[f]})}}class ChoiceList extends XFAObject{constructor(e){super(Go,"choiceList",!0);this.commitOn=getStringOption(e.commitOn,["select","exit"]);this.id=e.id||"";this.open=getStringOption(e.open,["userControl","always","multiSelect","onEntry"]);this.textEntry=getInteger({data:e.textEntry,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[co](e){const t=toStyle(this,"border","margin"),a=this[vs]()[vs](),r={fontSize:`calc(${a.font?.size||10}px * var(--total-scale-factor))`},i=[];if(a.items.children.length>0){const e=a.items;let t=0,n=0;if(2===e.children.length){t=e.children[0].save;n=1-t}const s=e.children[t][co]().html,o=e.children[n][co]().html;let c=!1;const l=a.value?.[so]()||"";for(let e=0,t=s.length;eMathClamp(parseInt(e.trim(),10),0,255))).map((e=>isNaN(e)?0:e));if(n.length<3)return{r:a,g:r,b:i};[a,r,i]=n;return{r:a,g:r,b:i}}(e.value):"";this.extras=null}[Os](){return!1}[ho](){return this.value?Util.makeHexColor(this.value.r,this.value.g,this.value.b):null}}class Comb extends XFAObject{constructor(e){super(Go,"comb");this.id=e.id||"";this.numberOfCells=getInteger({data:e.numberOfCells,defaultValue:0,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||""}}class Connect extends XFAObject{constructor(e){super(Go,"connect",!0);this.connection=e.connection||"";this.id=e.id||"";this.ref=e.ref||"";this.usage=getStringOption(e.usage,["exportAndImport","exportOnly","importOnly"]);this.use=e.use||"";this.usehref=e.usehref||"";this.picture=null}}class ContentArea extends XFAObject{constructor(e){super(Go,"contentArea",!0);this.h=getMeasurement(e.h);this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=getMeasurement(e.w);this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null}[co](e){const t={left:measureToString(this.x),top:measureToString(this.y),width:measureToString(this.w),height:measureToString(this.h)},a=["xfaContentarea"];isPrintOnly(this)&&a.push("xfaPrintOnly");return HTMLResult.success({name:"div",children:[],attributes:{style:t,class:a,id:this[uo]}})}}class Corner extends XFAObject{constructor(e){super(Go,"corner",!0);this.id=e.id||"";this.inverted=getInteger({data:e.inverted,defaultValue:0,validate:e=>1===e});this.join=getStringOption(e.join,["square","round"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.radius=getMeasurement(e.radius);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[ho](){const e=toStyle(this,"visibility");e.radius=measureToString("square"===this.join?0:this.radius);return e}}class DateElement extends ContentObject{constructor(e){super(Go,"date");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[hs](){const e=this[ss].trim();this[ss]=e?new Date(e):null}[co](e){return valueToHtml(this[ss]?this[ss].toString():"")}}class DateTime extends ContentObject{constructor(e){super(Go,"dateTime");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[hs](){const e=this[ss].trim();this[ss]=e?new Date(e):null}[co](e){return valueToHtml(this[ss]?this[ss].toString():"")}}class DateTimeEdit extends XFAObject{constructor(e){super(Go,"dateTimeEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.picker=getStringOption(e.picker,["host","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[co](e){const t=toStyle(this,"border","font","margin"),a=this[vs]()[vs](),r={name:"input",attributes:{type:"text",fieldId:a[uo],dataId:a[os]?.[uo]||a[uo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}};if(isRequired(a)){r.attributes["aria-required"]=!0;r.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[r]})}}class Decimal extends ContentObject{constructor(e){super(Go,"decimal");this.fracDigits=getInteger({data:e.fracDigits,defaultValue:2,validate:e=>!0});this.id=e.id||"";this.leadDigits=getInteger({data:e.leadDigits,defaultValue:-1,validate:e=>!0});this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[hs](){const e=parseFloat(this[ss].trim());this[ss]=isNaN(e)?null:e}[co](e){return valueToHtml(null!==this[ss]?this[ss].toString():"")}}class DefaultUi extends XFAObject{constructor(e){super(Go,"defaultUi",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class Desc extends XFAObject{constructor(e){super(Go,"desc",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class DigestMethod extends OptionObject{constructor(e){super(Go,"digestMethod",["","SHA1","SHA256","SHA512","RIPEMD160"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class DigestMethods extends XFAObject{constructor(e){super(Go,"digestMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.digestMethod=new XFAObjectArray}}class Draw extends XFAObject{constructor(e){super(Go,"draw",!0);this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.border=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.value=null;this.setProperty=new XFAObjectArray}[io](e){_setValue(this,e)}[co](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;fixDimensions(this);this[Ys]();const t=this.w,a=this.h,{w:r,h:i,isBroken:n}=layoutNode(this,e);if(r&&""===this.w){if(n&&this[Cs]()[js]()){this[Js]();return HTMLResult.FAILURE}this.w=r}i&&""===this.h&&(this.h=i);setFirstUnsplittable(this);if(!checkDimensions(this,e)){this.w=t;this.h=a;this[Js]();return HTMLResult.FAILURE}unsetFirstUnsplittable(this);const s=toStyle(this,"font","hAlign","dimensions","position","presence","rotate","anchorType","border","margin");setMinMaxDimensions(this,s);if(s.margin){s.padding=s.margin;delete s.margin}const o=["xfaDraw"];this.font&&o.push("xfaFont");isPrintOnly(this)&&o.push("xfaPrintOnly");const c={style:s,id:this[uo],class:o};this.name&&(c.xfaName=this.name);const l={name:"div",attributes:c,children:[]};applyAssist(this,c);const h=computeBbox(this,l,e),u=this.value?this.value[co](e).html:null;if(null===u){this.w=t;this.h=a;this[Js]();return HTMLResult.success(createWrapper(this,l),h)}l.children.push(u);setPara(this,s,u);this.w=t;this.h=a;this[Js]();return HTMLResult.success(createWrapper(this,l),h)}}class Edge extends XFAObject{constructor(e){super(Go,"edge",!0);this.cap=getStringOption(e.cap,["square","butt","round"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[ho](){const e=toStyle(this,"visibility");Object.assign(e,{linecap:this.cap,width:measureToString(this.thickness),color:this.color?this.color[ho]():"#000000",style:""});if("visible"!==this.presence)e.style="none";else switch(this.stroke){case"solid":e.style="solid";break;case"dashDot":case"dashDotDot":case"dashed":e.style="dashed";break;case"dotted":e.style="dotted";break;case"embossed":e.style="ridge";break;case"etched":e.style="groove";break;case"lowered":e.style="inset";break;case"raised":e.style="outset"}return e}}class Encoding extends OptionObject{constructor(e){super(Go,"encoding",["adbe.x509.rsa_sha1","adbe.pkcs7.detached","adbe.pkcs7.sha1"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Encodings extends XFAObject{constructor(e){super(Go,"encodings",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encoding=new XFAObjectArray}}class Encrypt extends XFAObject{constructor(e){super(Go,"encrypt",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=null}}class EncryptData extends XFAObject{constructor(e){super(Go,"encryptData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["encrypt","decrypt"]);this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Encryption extends XFAObject{constructor(e){super(Go,"encryption",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class EncryptionMethod extends OptionObject{constructor(e){super(Go,"encryptionMethod",["","AES256-CBC","TRIPLEDES-CBC","AES128-CBC","AES192-CBC"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EncryptionMethods extends XFAObject{constructor(e){super(Go,"encryptionMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encryptionMethod=new XFAObjectArray}}class Event extends XFAObject{constructor(e){super(Go,"event",!0);this.activity=getStringOption(e.activity,["click","change","docClose","docReady","enter","exit","full","indexChange","initialize","mouseDown","mouseEnter","mouseExit","mouseUp","postExecute","postOpen","postPrint","postSave","postSign","postSubmit","preExecute","preOpen","prePrint","preSave","preSign","preSubmit","ready","validationState"]);this.id=e.id||"";this.listen=getStringOption(e.listen,["refOnly","refAndDescendents"]);this.name=e.name||"";this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.encryptData=null;this.execute=null;this.script=null;this.signData=null;this.submit=null}}class ExData extends ContentObject{constructor(e){super(Go,"exData");this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.maxLength=getInteger({data:e.maxLength,defaultValue:-1,validate:e=>e>=-1});this.name=e.name||"";this.rid=e.rid||"";this.transferEncoding=getStringOption(e.transferEncoding,["none","base64","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[Bs](){return"text/html"===this.contentType}[$s](e){if("text/html"===this.contentType&&e[Hs]===go.xhtml.id){this[ss]=e;return!0}if("text/xml"===this.contentType){this[ss]=e;return!0}return!1}[co](e){return"text/html"===this.contentType&&this[ss]?this[ss][co](e):HTMLResult.EMPTY}}class ExObject extends XFAObject{constructor(e){super(Go,"exObject",!0);this.archive=e.archive||"";this.classId=e.classId||"";this.codeBase=e.codeBase||"";this.codeType=e.codeType||"";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class ExclGroup extends XFAObject{constructor(e){super(Go,"exclGroup",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.accessKey=e.accessKey||"";this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.margin=null;this.para=null;this.traversal=null;this.validate=null;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.field=new XFAObjectArray;this.setProperty=new XFAObjectArray}[Rs](){return!0}[Os](){return!0}[io](e){for(const t of this.field.children){if(!t.value){const e=new Value({});t[Qn](e);t.value=e}t.value[io](e)}}[js](){return this.layout.endsWith("-tb")&&0===this[ls].attempt&&this[ls].numberInLine>0||this[vs]()[js]()}[_s](){const e=this[Cs]();if(!e[_s]())return!1;if(void 0!==this[ls]._isSplittable)return this[ls]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[ls]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[ls].numberInLine)return!1;this[ls]._isSplittable=!0;return!0}[us](){return flushHTML(this)}[Zn](e,t){addHTML(this,e,t)}[gs](){return getAvailableSpace(this)}[co](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;fixDimensions(this);const t=[],a={id:this[uo],class:[]};setAccess(this,a.class);this[ls]||=Object.create(null);Object.assign(this[ls],{children:t,attributes:a,attempt:0,line:null,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const r=this[_s]();r||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const i=new Set(["field"]);if(this.layout.includes("row")){const e=this[Cs]().columnWidths;if(Array.isArray(e)&&e.length>0){this[ls].columnWidths=e;this[ls].currentColumn=0}}const n=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),s=["xfaExclgroup"],o=layoutClass(this);o&&s.push(o);isPrintOnly(this)&&s.push("xfaPrintOnly");a.style=n;a.class=s;this.name&&(a.xfaName=this.name);this[Ys]();const c="lr-tb"===this.layout||"rl-tb"===this.layout,l=c?2:1;for(;this[ls].attempte>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.format=null;this.items=new XFAObjectArray(2);this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.validate=null;this.value=null;this.bindItems=new XFAObjectArray;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.setProperty=new XFAObjectArray}[Rs](){return!0}[io](e){_setValue(this,e)}[co](e){setTabIndex(this);if(!this.ui){this.ui=new Ui({});this.ui[Is]=this[Is];this[Qn](this.ui);let e;switch(this.items.children.length){case 0:e=new TextEdit({});this.ui.textEdit=e;break;case 1:e=new CheckButton({});this.ui.checkButton=e;break;case 2:e=new ChoiceList({});this.ui.choiceList=e}this.ui[Qn](e)}if(!this.ui||"hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;this.caption&&delete this.caption[ls];this[Ys]();const t=this.caption?this.caption[co](e).html:null,a=this.w,r=this.h;let i=0,n=0;if(this.margin){i=this.margin.leftInset+this.margin.rightInset;n=this.margin.topInset+this.margin.bottomInset}let s=null;if(""===this.w||""===this.h){let t=null,a=null,r=0,o=0;if(this.ui.checkButton)r=o=this.ui.checkButton.size;else{const{w:t,h:a}=layoutNode(this,e);if(null!==t){r=t;o=a}else o=function fonts_getMetrics(e,t=!1){let a=null;if(e){const t=stripQuotes(e.typeface),r=e[Is].fontFinder.find(t);a=selectFont(e,r)}if(!a)return{lineHeight:12,lineGap:2,lineNoGap:10};const r=e.size||10,i=a.lineHeight?Math.max(t?0:1.2,a.lineHeight):1.2,n=void 0===a.lineGap?.2:a.lineGap;return{lineHeight:i*r,lineGap:n*r,lineNoGap:Math.max(1,i-n)*r}}(this.font,!0).lineNoGap}s=getBorderDims(this.ui[ws]());r+=s.w;o+=s.h;if(this.caption){const{w:i,h:n,isBroken:s}=this.caption[ws](e);if(s&&this[Cs]()[js]()){this[Js]();return HTMLResult.FAILURE}t=i;a=n;switch(this.caption.placement){case"left":case"right":case"inline":t+=r;break;case"top":case"bottom":a+=o}}else{t=r;a=o}if(t&&""===this.w){t+=i;this.w=Math.min(this.maxW<=0?1/0:this.maxW,this.minW+1e>=1&&e<=5});this.appearanceFilter=null;this.certificates=null;this.digestMethods=null;this.encodings=null;this.encryptionMethods=null;this.handler=null;this.lockDocument=null;this.mdp=null;this.reasons=null;this.timeStamp=null}}class Float extends ContentObject{constructor(e){super(Go,"float");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[hs](){const e=parseFloat(this[ss].trim());this[ss]=isNaN(e)?null:e}[co](e){return valueToHtml(null!==this[ss]?this[ss].toString():"")}}class template_Font extends XFAObject{constructor(e){super(Go,"font",!0);this.baselineShift=getMeasurement(e.baselineShift);this.fontHorizontalScale=getFloat({data:e.fontHorizontalScale,defaultValue:100,validate:e=>e>=0});this.fontVerticalScale=getFloat({data:e.fontVerticalScale,defaultValue:100,validate:e=>e>=0});this.id=e.id||"";this.kerningMode=getStringOption(e.kerningMode,["none","pair"]);this.letterSpacing=getMeasurement(e.letterSpacing,"0");this.lineThrough=getInteger({data:e.lineThrough,defaultValue:0,validate:e=>1===e||2===e});this.lineThroughPeriod=getStringOption(e.lineThroughPeriod,["all","word"]);this.overline=getInteger({data:e.overline,defaultValue:0,validate:e=>1===e||2===e});this.overlinePeriod=getStringOption(e.overlinePeriod,["all","word"]);this.posture=getStringOption(e.posture,["normal","italic"]);this.size=getMeasurement(e.size,"10pt");this.typeface=e.typeface||"Courier";this.underline=getInteger({data:e.underline,defaultValue:0,validate:e=>1===e||2===e});this.underlinePeriod=getStringOption(e.underlinePeriod,["all","word"]);this.use=e.use||"";this.usehref=e.usehref||"";this.weight=getStringOption(e.weight,["normal","bold"]);this.extras=null;this.fill=null}[ts](e){super[ts](e);this[Is].usedTypefaces.add(this.typeface)}[ho](){const e=toStyle(this,"fill"),t=e.color;if(t)if("#000000"===t)delete e.color;else if(!t.startsWith("#")){e.background=t;e.backgroundClip="text";e.color="transparent"}this.baselineShift&&(e.verticalAlign=measureToString(this.baselineShift));e.fontKerning="none"===this.kerningMode?"none":"normal";e.letterSpacing=measureToString(this.letterSpacing);if(0!==this.lineThrough){e.textDecoration="line-through";2===this.lineThrough&&(e.textDecorationStyle="double")}if(0!==this.overline){e.textDecoration="overline";2===this.overline&&(e.textDecorationStyle="double")}e.fontStyle=this.posture;e.fontSize=measureToString(.99*this.size);setFontFamily(this,this,this[Is].fontFinder,e);if(0!==this.underline){e.textDecoration="underline";2===this.underline&&(e.textDecorationStyle="double")}e.fontWeight=this.weight;return e}}class Format extends XFAObject{constructor(e){super(Go,"format",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null}}class Handler extends StringObject{constructor(e){super(Go,"handler");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Hyphenation extends XFAObject{constructor(e){super(Go,"hyphenation");this.excludeAllCaps=getInteger({data:e.excludeAllCaps,defaultValue:0,validate:e=>1===e});this.excludeInitialCap=getInteger({data:e.excludeInitialCap,defaultValue:0,validate:e=>1===e});this.hyphenate=getInteger({data:e.hyphenate,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.pushCharacterCount=getInteger({data:e.pushCharacterCount,defaultValue:3,validate:e=>e>=0});this.remainCharacterCount=getInteger({data:e.remainCharacterCount,defaultValue:3,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||"";this.wordCharacterCount=getInteger({data:e.wordCharacterCount,defaultValue:7,validate:e=>e>=0})}}class Image extends StringObject{constructor(e){super(Go,"image");this.aspect=getStringOption(e.aspect,["fit","actual","height","none","width"]);this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.name=e.name||"";this.transferEncoding=getStringOption(e.transferEncoding,["base64","none","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[co](){if(this.contentType&&!Jo.has(this.contentType.toLowerCase()))return HTMLResult.EMPTY;let e=this[Is].images&&this[Is].images.get(this.href);if(!e&&(this.href||!this[ss]))return HTMLResult.EMPTY;e||"base64"!==this.transferEncoding||(e=function fromBase64Util(e){return Uint8Array.fromBase64?Uint8Array.fromBase64(e):stringToBytes(atob(e))}(this[ss]));if(!e)return HTMLResult.EMPTY;if(!this.contentType){for(const[t,a]of Yo)if(e.length>t.length&&t.every(((t,a)=>t===e[a]))){this.contentType=a;break}if(!this.contentType)return HTMLResult.EMPTY}const t=new Blob([e],{type:this.contentType});let a;switch(this.aspect){case"fit":case"actual":break;case"height":a={height:"100%",objectFit:"fill"};break;case"none":a={width:"100%",height:"100%",objectFit:"fill"};break;case"width":a={width:"100%",objectFit:"fill"}}const r=this[vs]();return HTMLResult.success({name:"img",attributes:{class:["xfaImage"],style:a,src:URL.createObjectURL(t),alt:r?ariaLabel(r[vs]()):null}})}}class ImageEdit extends XFAObject{constructor(e){super(Go,"imageEdit",!0);this.data=getStringOption(e.data,["link","embed"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[co](e){return"embed"===this.data?HTMLResult.success({name:"div",children:[],attributes:{}}):HTMLResult.EMPTY}}class Integer extends ContentObject{constructor(e){super(Go,"integer");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[hs](){const e=parseInt(this[ss].trim(),10);this[ss]=isNaN(e)?null:e}[co](e){return valueToHtml(null!==this[ss]?this[ss].toString():"")}}class Issuers extends XFAObject{constructor(e){super(Go,"issuers",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Items extends XFAObject{constructor(e){super(Go,"items",!0);this.id=e.id||"";this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.ref=e.ref||"";this.save=getInteger({data:e.save,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[co](){const e=[];for(const t of this[xs]())e.push(t[so]());return HTMLResult.success(e)}}class Keep extends XFAObject{constructor(e){super(Go,"keep",!0);this.id=e.id||"";const t=["none","contentArea","pageArea"];this.intact=getStringOption(e.intact,t);this.next=getStringOption(e.next,t);this.previous=getStringOption(e.previous,t);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class KeyUsage extends XFAObject{constructor(e){super(Go,"keyUsage");const t=["","yes","no"];this.crlSign=getStringOption(e.crlSign,t);this.dataEncipherment=getStringOption(e.dataEncipherment,t);this.decipherOnly=getStringOption(e.decipherOnly,t);this.digitalSignature=getStringOption(e.digitalSignature,t);this.encipherOnly=getStringOption(e.encipherOnly,t);this.id=e.id||"";this.keyAgreement=getStringOption(e.keyAgreement,t);this.keyCertSign=getStringOption(e.keyCertSign,t);this.keyEncipherment=getStringOption(e.keyEncipherment,t);this.nonRepudiation=getStringOption(e.nonRepudiation,t);this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Line extends XFAObject{constructor(e){super(Go,"line",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.slope=getStringOption(e.slope,["\\","/"]);this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null}[co](){const e=this[vs]()[vs](),t=this.edge||new Edge({}),a=t[ho](),r=Object.create(null),i="visible"===t.presence?t.thickness:0;r.strokeWidth=measureToString(i);r.stroke=a.color;let n,s,o,c,l="100%",h="100%";if(e.w<=i){[n,s,o,c]=["50%",0,"50%","100%"];l=r.strokeWidth}else if(e.h<=i){[n,s,o,c]=[0,"50%","100%","50%"];h=r.strokeWidth}else"\\"===this.slope?[n,s,o,c]=[0,0,"100%","100%"]:[n,s,o,c]=[0,"100%","100%",0];const u={name:"svg",children:[{name:"line",attributes:{xmlns:Vo,x1:n,y1:s,x2:o,y2:c,style:r}}],attributes:{xmlns:Vo,width:l,height:h,style:{overflow:"visible"}}};if(hasMargin(e))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[u]});u.attributes.style.position="absolute";return HTMLResult.success(u)}}class Linear extends XFAObject{constructor(e){super(Go,"linear",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toRight","toBottom","toLeft","toTop"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[ho](e){e=e?e[ho]():"#FFFFFF";return`linear-gradient(${this.type.replace(/([RBLT])/," $1").toLowerCase()}, ${e}, ${this.color?this.color[ho]():"#000000"})`}}class LockDocument extends ContentObject{constructor(e){super(Go,"lockDocument");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}[hs](){this[ss]=getStringOption(this[ss],["auto","0","1"])}}class Manifest extends XFAObject{constructor(e){super(Go,"manifest",!0);this.action=getStringOption(e.action,["include","all","exclude"]);this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.ref=new XFAObjectArray}}class Margin extends XFAObject{constructor(e){super(Go,"margin",!0);this.bottomInset=getMeasurement(e.bottomInset,"0");this.id=e.id||"";this.leftInset=getMeasurement(e.leftInset,"0");this.rightInset=getMeasurement(e.rightInset,"0");this.topInset=getMeasurement(e.topInset,"0");this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[ho](){return{margin:measureToString(this.topInset)+" "+measureToString(this.rightInset)+" "+measureToString(this.bottomInset)+" "+measureToString(this.leftInset)}}}class Mdp extends XFAObject{constructor(e){super(Go,"mdp");this.id=e.id||"";this.permissions=getInteger({data:e.permissions,defaultValue:2,validate:e=>1===e||3===e});this.signatureType=getStringOption(e.signatureType,["filler","author"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Medium extends XFAObject{constructor(e){super(Go,"medium");this.id=e.id||"";this.imagingBBox=function getBBox(e){const t=-1;if(!e)return{x:t,y:t,width:t,height:t};const a=e.split(",",4).map((e=>getMeasurement(e.trim(),"-1")));if(a.length<4||a[2]<0||a[3]<0)return{x:t,y:t,width:t,height:t};const[r,i,n,s]=a;return{x:r,y:i,width:n,height:s}}(e.imagingBBox);this.long=getMeasurement(e.long);this.orientation=getStringOption(e.orientation,["portrait","landscape"]);this.short=getMeasurement(e.short);this.stock=e.stock||"";this.trayIn=getStringOption(e.trayIn,["auto","delegate","pageFront"]);this.trayOut=getStringOption(e.trayOut,["auto","delegate"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Message extends XFAObject{constructor(e){super(Go,"message",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.text=new XFAObjectArray}}class NumericEdit extends XFAObject{constructor(e){super(Go,"numericEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[co](e){const t=toStyle(this,"border","font","margin"),a=this[vs]()[vs](),r={name:"input",attributes:{type:"text",fieldId:a[uo],dataId:a[os]?.[uo]||a[uo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}};if(isRequired(a)){r.attributes["aria-required"]=!0;r.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[r]})}}class Occur extends XFAObject{constructor(e){super(Go,"occur",!0);this.id=e.id||"";this.initial=""!==e.initial?getInteger({data:e.initial,defaultValue:"",validate:e=>!0}):"";this.max=""!==e.max?getInteger({data:e.max,defaultValue:1,validate:e=>!0}):"";this.min=""!==e.min?getInteger({data:e.min,defaultValue:1,validate:e=>!0}):"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[ts](){const e=this[vs](),t=this.min;""===this.min&&(this.min=e instanceof PageArea||e instanceof PageSet?0:1);""===this.max&&(this.max=""===t?e instanceof PageArea||e instanceof PageSet?-1:1:this.min);-1!==this.max&&this.max!0});this.name=e.name||"";this.numbered=getInteger({data:e.numbered,defaultValue:1,validate:e=>!0});this.oddOrEven=getStringOption(e.oddOrEven,["any","even","odd"]);this.pagePosition=getStringOption(e.pagePosition,["any","first","last","only","rest"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.desc=null;this.extras=null;this.medium=null;this.occur=null;this.area=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.draw=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray}[Xs](){if(!this[ls]){this[ls]={numberOfUse:0};return!0}return!this.occur||-1===this.occur.max||this[ls].numberOfUsee.oddOrEven===t&&e.pagePosition===a));if(r)return r;r=this.pageArea.children.find((e=>"any"===e.oddOrEven&&e.pagePosition===a));if(r)return r;r=this.pageArea.children.find((e=>"any"===e.oddOrEven&&"any"===e.pagePosition));return r||this.pageArea.children[0]}}class Para extends XFAObject{constructor(e){super(Go,"para",!0);this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.lineHeight=e.lineHeight?getMeasurement(e.lineHeight,"0pt"):"";this.marginLeft=e.marginLeft?getMeasurement(e.marginLeft,"0pt"):"";this.marginRight=e.marginRight?getMeasurement(e.marginRight,"0pt"):"";this.orphans=getInteger({data:e.orphans,defaultValue:0,validate:e=>e>=0});this.preserve=e.preserve||"";this.radixOffset=e.radixOffset?getMeasurement(e.radixOffset,"0pt"):"";this.spaceAbove=e.spaceAbove?getMeasurement(e.spaceAbove,"0pt"):"";this.spaceBelow=e.spaceBelow?getMeasurement(e.spaceBelow,"0pt"):"";this.tabDefault=e.tabDefault?getMeasurement(this.tabDefault):"";this.tabStops=(e.tabStops||"").trim().split(/\s+/).map(((e,t)=>t%2==1?getMeasurement(e):e));this.textIndent=e.textIndent?getMeasurement(e.textIndent,"0pt"):"";this.use=e.use||"";this.usehref=e.usehref||"";this.vAlign=getStringOption(e.vAlign,["top","bottom","middle"]);this.widows=getInteger({data:e.widows,defaultValue:0,validate:e=>e>=0});this.hyphenation=null}[ho](){const e=toStyle(this,"hAlign");""!==this.marginLeft&&(e.paddingLeft=measureToString(this.marginLeft));""!==this.marginRight&&(e.paddingRight=measureToString(this.marginRight));""!==this.spaceAbove&&(e.paddingTop=measureToString(this.spaceAbove));""!==this.spaceBelow&&(e.paddingBottom=measureToString(this.spaceBelow));if(""!==this.textIndent){e.textIndent=measureToString(this.textIndent);fixTextIndent(e)}this.lineHeight>0&&(e.lineHeight=measureToString(this.lineHeight));""!==this.tabDefault&&(e.tabSize=measureToString(this.tabDefault));this.tabStops.length;this.hyphenatation&&Object.assign(e,this.hyphenatation[ho]());return e}}class PasswordEdit extends XFAObject{constructor(e){super(Go,"passwordEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.passwordChar=e.passwordChar||"*";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}}class template_Pattern extends XFAObject{constructor(e){super(Go,"pattern",!0);this.id=e.id||"";this.type=getStringOption(e.type,["crossHatch","crossDiagonal","diagonalLeft","diagonalRight","horizontal","vertical"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[ho](e){e=e?e[ho]():"#FFFFFF";const t=this.color?this.color[ho]():"#000000",a="repeating-linear-gradient",r=`${e},${e} 5px,${t} 5px,${t} 10px`;switch(this.type){case"crossHatch":return`${a}(to top,${r}) ${a}(to right,${r})`;case"crossDiagonal":return`${a}(45deg,${r}) ${a}(-45deg,${r})`;case"diagonalLeft":return`${a}(45deg,${r})`;case"diagonalRight":return`${a}(-45deg,${r})`;case"horizontal":return`${a}(to top,${r})`;case"vertical":return`${a}(to right,${r})`}return""}}class Picture extends StringObject{constructor(e){super(Go,"picture");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Proto extends XFAObject{constructor(e){super(Go,"proto",!0);this.appearanceFilter=new XFAObjectArray;this.arc=new XFAObjectArray;this.area=new XFAObjectArray;this.assist=new XFAObjectArray;this.barcode=new XFAObjectArray;this.bindItems=new XFAObjectArray;this.bookend=new XFAObjectArray;this.boolean=new XFAObjectArray;this.border=new XFAObjectArray;this.break=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.button=new XFAObjectArray;this.calculate=new XFAObjectArray;this.caption=new XFAObjectArray;this.certificate=new XFAObjectArray;this.certificates=new XFAObjectArray;this.checkButton=new XFAObjectArray;this.choiceList=new XFAObjectArray;this.color=new XFAObjectArray;this.comb=new XFAObjectArray;this.connect=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.corner=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.dateTimeEdit=new XFAObjectArray;this.decimal=new XFAObjectArray;this.defaultUi=new XFAObjectArray;this.desc=new XFAObjectArray;this.digestMethod=new XFAObjectArray;this.digestMethods=new XFAObjectArray;this.draw=new XFAObjectArray;this.edge=new XFAObjectArray;this.encoding=new XFAObjectArray;this.encodings=new XFAObjectArray;this.encrypt=new XFAObjectArray;this.encryptData=new XFAObjectArray;this.encryption=new XFAObjectArray;this.encryptionMethod=new XFAObjectArray;this.encryptionMethods=new XFAObjectArray;this.event=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.execute=new XFAObjectArray;this.extras=new XFAObjectArray;this.field=new XFAObjectArray;this.fill=new XFAObjectArray;this.filter=new XFAObjectArray;this.float=new XFAObjectArray;this.font=new XFAObjectArray;this.format=new XFAObjectArray;this.handler=new XFAObjectArray;this.hyphenation=new XFAObjectArray;this.image=new XFAObjectArray;this.imageEdit=new XFAObjectArray;this.integer=new XFAObjectArray;this.issuers=new XFAObjectArray;this.items=new XFAObjectArray;this.keep=new XFAObjectArray;this.keyUsage=new XFAObjectArray;this.line=new XFAObjectArray;this.linear=new XFAObjectArray;this.lockDocument=new XFAObjectArray;this.manifest=new XFAObjectArray;this.margin=new XFAObjectArray;this.mdp=new XFAObjectArray;this.medium=new XFAObjectArray;this.message=new XFAObjectArray;this.numericEdit=new XFAObjectArray;this.occur=new XFAObjectArray;this.oid=new XFAObjectArray;this.oids=new XFAObjectArray;this.overflow=new XFAObjectArray;this.pageArea=new XFAObjectArray;this.pageSet=new XFAObjectArray;this.para=new XFAObjectArray;this.passwordEdit=new XFAObjectArray;this.pattern=new XFAObjectArray;this.picture=new XFAObjectArray;this.radial=new XFAObjectArray;this.reason=new XFAObjectArray;this.reasons=new XFAObjectArray;this.rectangle=new XFAObjectArray;this.ref=new XFAObjectArray;this.script=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.signData=new XFAObjectArray;this.signature=new XFAObjectArray;this.signing=new XFAObjectArray;this.solid=new XFAObjectArray;this.speak=new XFAObjectArray;this.stipple=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray;this.subjectDN=new XFAObjectArray;this.subjectDNs=new XFAObjectArray;this.submit=new XFAObjectArray;this.text=new XFAObjectArray;this.textEdit=new XFAObjectArray;this.time=new XFAObjectArray;this.timeStamp=new XFAObjectArray;this.toolTip=new XFAObjectArray;this.traversal=new XFAObjectArray;this.traverse=new XFAObjectArray;this.ui=new XFAObjectArray;this.validate=new XFAObjectArray;this.value=new XFAObjectArray;this.variables=new XFAObjectArray}}class Radial extends XFAObject{constructor(e){super(Go,"radial",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toEdge","toCenter"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[ho](e){e=e?e[ho]():"#FFFFFF";const t=this.color?this.color[ho]():"#000000";return`radial-gradient(circle at center, ${"toEdge"===this.type?`${e},${t}`:`${t},${e}`})`}}class Reason extends StringObject{constructor(e){super(Go,"reason");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Reasons extends XFAObject{constructor(e){super(Go,"reasons",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.reason=new XFAObjectArray}}class Rectangle extends XFAObject{constructor(e){super(Go,"rectangle",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.fill=null}[co](){const e=this.edge.children.length?this.edge.children[0]:new Edge({}),t=e[ho](),a=Object.create(null);"visible"===this.fill?.presence?Object.assign(a,this.fill[ho]()):a.fill="transparent";a.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);a.stroke=t.color;const r=(this.corner.children.length?this.corner.children[0]:new Corner({}))[ho](),i={name:"svg",children:[{name:"rect",attributes:{xmlns:Vo,width:"100%",height:"100%",x:0,y:0,rx:r.radius,ry:r.radius,style:a}}],attributes:{xmlns:Vo,style:{overflow:"visible"},width:"100%",height:"100%"}};if(hasMargin(this[vs]()[vs]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[i]});i.attributes.style.position="absolute";return HTMLResult.success(i)}}class RefElement extends StringObject{constructor(e){super(Go,"ref");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Script extends StringObject{constructor(e){super(Go,"script");this.binding=e.binding||"";this.contentType=e.contentType||"";this.id=e.id||"";this.name=e.name||"";this.runAt=getStringOption(e.runAt,["client","both","server"]);this.use=e.use||"";this.usehref=e.usehref||""}}class SetProperty extends XFAObject{constructor(e){super(Go,"setProperty");this.connection=e.connection||"";this.ref=e.ref||"";this.target=e.target||""}}class SignData extends XFAObject{constructor(e){super(Go,"signData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["sign","clear","verify"]);this.ref=e.ref||"";this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Signature extends XFAObject{constructor(e){super(Go,"signature",!0);this.id=e.id||"";this.type=getStringOption(e.type,["PDF1.3","PDF1.6"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.filter=null;this.manifest=null;this.margin=null}}class Signing extends XFAObject{constructor(e){super(Go,"signing",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Solid extends XFAObject{constructor(e){super(Go,"solid",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[ho](e){return e?e[ho]():"#FFFFFF"}}class Speak extends StringObject{constructor(e){super(Go,"speak");this.disable=getInteger({data:e.disable,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.priority=getStringOption(e.priority,["custom","caption","name","toolTip"]);this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Stipple extends XFAObject{constructor(e){super(Go,"stipple",!0);this.id=e.id||"";this.rate=getInteger({data:e.rate,defaultValue:50,validate:e=>e>=0&&e<=100});this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[ho](e){const t=this.rate/100;return Util.makeHexColor(Math.round(e.value.r*(1-t)+this.value.r*t),Math.round(e.value.g*(1-t)+this.value.g*t),Math.round(e.value.b*(1-t)+this.value.b*t))}}class Subform extends XFAObject{constructor(e){super(Go,"subform",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.allowMacro=getInteger({data:e.allowMacro,defaultValue:0,validate:e=>1===e});this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.columnWidths=(e.columnWidths||"").trim().split(/\s+/).map((e=>"-1"===e?-1:getMeasurement(e)));this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.mergeMode=getStringOption(e.mergeMode,["consumeData","matchTemplate"]);this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.restoreState=getStringOption(e.restoreState,["manual","auto"]);this.scope=getStringOption(e.scope,["name","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.bookend=null;this.border=null;this.break=null;this.calculate=null;this.desc=null;this.extras=null;this.keep=null;this.margin=null;this.occur=null;this.overflow=null;this.pageSet=null;this.para=null;this.traversal=null;this.validate=null;this.variables=null;this.area=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.connect=new XFAObjectArray;this.draw=new XFAObjectArray;this.event=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.proto=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}[Cs](){const e=this[vs]();return e instanceof SubformSet?e[Cs]():e}[Rs](){return!0}[js](){return this.layout.endsWith("-tb")&&0===this[ls].attempt&&this[ls].numberInLine>0||this[vs]()[js]()}*[As](){yield*getContainedChildren(this)}[us](){return flushHTML(this)}[Zn](e,t){addHTML(this,e,t)}[gs](){return getAvailableSpace(this)}[_s](){const e=this[Cs]();if(!e[_s]())return!1;if(void 0!==this[ls]._isSplittable)return this[ls]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[ls]._isSplittable=!1;return!1}if(this.keep&&"none"!==this.keep.intact){this[ls]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[ls].numberInLine)return!1;this[ls]._isSplittable=!0;return!0}[co](e){setTabIndex(this);if(this.break){if("auto"!==this.break.after||""!==this.break.afterTarget){const e=new BreakAfter({targetType:this.break.after,target:this.break.afterTarget,startNew:this.break.startNew.toString()});e[Is]=this[Is];this[Qn](e);this.breakAfter.push(e)}if("auto"!==this.break.before||""!==this.break.beforeTarget){const e=new BreakBefore({targetType:this.break.before,target:this.break.beforeTarget,startNew:this.break.startNew.toString()});e[Is]=this[Is];this[Qn](e);this.breakBefore.push(e)}if(""!==this.break.overflowTarget){const e=new Overflow({target:this.break.overflowTarget,leader:this.break.overflowLeader,trailer:this.break.overflowTrailer});e[Is]=this[Is];this[Qn](e);this.overflow.push(e)}this[Zs](this.break);this.break=null}if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;(this.breakBefore.children.length>1||this.breakAfter.children.length>1)&&warn("XFA - Several breakBefore or breakAfter in subforms: please file a bug.");if(this.breakBefore.children.length>=1){const e=this.breakBefore.children[0];if(handleBreak(e))return HTMLResult.breakNode(e)}if(this[ls]?.afterBreakAfter)return HTMLResult.EMPTY;fixDimensions(this);const t=[],a={id:this[uo],class:[]};setAccess(this,a.class);this[ls]||=Object.create(null);Object.assign(this[ls],{children:t,line:null,attributes:a,attempt:0,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const r=this[Fs](),i=r[ls].noLayoutFailure,n=this[_s]();n||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const s=new Set(["area","draw","exclGroup","field","subform","subformSet"]);if(this.layout.includes("row")){const e=this[Cs]().columnWidths;if(Array.isArray(e)&&e.length>0){this[ls].columnWidths=e;this[ls].currentColumn=0}}const o=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),c=["xfaSubform"],l=layoutClass(this);l&&c.push(l);a.style=o;a.class=c;this.name&&(a.xfaName=this.name);if(this.overflow){const t=this.overflow[ws]();if(t.addLeader){t.addLeader=!1;handleOverflow(this,t.leader,e)}}this[Ys]();const h="lr-tb"===this.layout||"rl-tb"===this.layout,u=h?2:1;for(;this[ls].attempt=1){const e=this.breakAfter.children[0];if(handleBreak(e)){this[ls].afterBreakAfter=y;return HTMLResult.breakNode(e)}}delete this[ls];return y}}class SubformSet extends XFAObject{constructor(e){super(Go,"subformSet",!0);this.id=e.id||"";this.name=e.name||"";this.relation=getStringOption(e.relation,["ordered","choice","unordered"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.bookend=null;this.break=null;this.desc=null;this.extras=null;this.occur=null;this.overflow=null;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[As](){yield*getContainedChildren(this)}[Cs](){let e=this[vs]();for(;!(e instanceof Subform);)e=e[vs]();return e}[Rs](){return!0}}class SubjectDN extends ContentObject{constructor(e){super(Go,"subjectDN");this.delimiter=e.delimiter||",";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[hs](){this[ss]=new Map(this[ss].split(this.delimiter).map((e=>{(e=e.split("=",2))[0]=e[0].trim();return e})))}}class SubjectDNs extends XFAObject{constructor(e){super(Go,"subjectDNs",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.subjectDN=new XFAObjectArray}}class Submit extends XFAObject{constructor(e){super(Go,"submit",!0);this.embedPDF=getInteger({data:e.embedPDF,defaultValue:0,validate:e=>1===e});this.format=getStringOption(e.format,["xdp","formdata","pdf","urlencoded","xfd","xml"]);this.id=e.id||"";this.target=e.target||"";this.textEncoding=getKeyword({data:e.textEncoding?e.textEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.use=e.use||"";this.usehref=e.usehref||"";this.xdpContent=e.xdpContent||"";this.encrypt=null;this.encryptData=new XFAObjectArray;this.signData=new XFAObjectArray}}class Template extends XFAObject{constructor(e){super(Go,"template",!0);this.baseProfile=getStringOption(e.baseProfile,["full","interactiveForms"]);this.extras=null;this.subform=new XFAObjectArray}[hs](){0===this.subform.children.length&&warn("XFA - No subforms in template node.");this.subform.children.length>=2&&warn("XFA - Several subforms in template node: please file a bug.");this[no]=5e3}[_s](){return!0}[to](e,t){return e.startsWith("#")?[this[Ts].get(e.slice(1))]:searchNode(this,t,e,!0,!0)}*[oo](){if(!this.subform.children.length)return HTMLResult.success({name:"div",children:[]});this[ls]={overflowNode:null,firstUnsplittable:null,currentContentArea:null,currentPageArea:null,noLayoutFailure:!1,pageNumber:1,pagePosition:"first",oddOrEven:"odd",blankOrNotBlank:"nonBlank",paraStack:[]};const e=this.subform.children[0];e.pageSet[as]();const t=e.pageSet.pageArea.children,a={name:"div",children:[]};let r=null,i=null,n=null;if(e.breakBefore.children.length>=1){i=e.breakBefore.children[0];n=i.target}else if(e.subform.children.length>=1&&e.subform.children[0].breakBefore.children.length>=1){i=e.subform.children[0].breakBefore.children[0];n=i.target}else if(e.break?.beforeTarget){i=e.break;n=i.beforeTarget}else if(e.subform.children.length>=1&&e.subform.children[0].break?.beforeTarget){i=e.subform.children[0].break;n=i.beforeTarget}if(i){const e=this[to](n,i[vs]());if(e instanceof PageArea){r=e;i[ls]={}}}r||=t[0];r[ls]={numberOfUse:1};const s=r[vs]();s[ls]={numberOfUse:1,pageIndex:s.pageArea.children.indexOf(r),pageSetIndex:0};let o,c=null,l=null,h=!0,u=0,d=0;for(;;){if(h)u=0;else{a.children.pop();if(3==++u){warn("XFA - Something goes wrong: please file a bug.");return a}}o=null;this[ls].currentPageArea=r;const t=r[co]().html;a.children.push(t);if(c){this[ls].noLayoutFailure=!0;t.children.push(c[co](r[ls].space).html);c=null}if(l){this[ls].noLayoutFailure=!0;t.children.push(l[co](r[ls].space).html);l=null}const i=r.contentArea.children,n=t.children.filter((e=>e.attributes.class.includes("xfaContentarea")));h=!1;this[ls].firstUnsplittable=null;this[ls].noLayoutFailure=!1;const flush=t=>{const a=e[us]();if(a){h||=a.children?.length>0;n[t].children.push(a)}};for(let t=d,r=i.length;t0;n[t].children.push(u.html)}else!h&&a.children.length>1&&a.children.pop();return a}if(u.isBreak()){const e=u.breakNode;flush(t);if("auto"===e.targetType)continue;if(e.leader){c=this[to](e.leader,e[vs]());c=c?c[0]:null}if(e.trailer){l=this[to](e.trailer,e[vs]());l=l?l[0]:null}if("pageArea"===e.targetType){o=e[ls].target;t=1/0}else if(e[ls].target){o=e[ls].target;d=e[ls].index+1;t=1/0}else t=e[ls].index}else if(this[ls].overflowNode){const e=this[ls].overflowNode;this[ls].overflowNode=null;const a=e[ws](),r=a.target;a.addLeader=null!==a.leader;a.addTrailer=null!==a.trailer;flush(t);const n=t;t=1/0;if(r instanceof PageArea)o=r;else if(r instanceof ContentArea){const e=i.indexOf(r);if(-1!==e)e>n?t=e-1:d=e;else{o=r[vs]();d=o.contentArea.children.indexOf(r)}}}else flush(t)}this[ls].pageNumber+=1;o&&(o[Xs]()?o[ls].numberOfUse+=1:o=null);r=o||r[ks]();yield null}}}class Text extends ContentObject{constructor(e){super(Go,"text");this.id=e.id||"";this.maxChars=getInteger({data:e.maxChars,defaultValue:0,validate:e=>e>=0});this.name=e.name||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}[Yn](){return!0}[$s](e){if(e[Hs]===go.xhtml.id){this[ss]=e;return!0}warn(`XFA - Invalid content in Text: ${e[Ws]}.`);return!1}[Vs](e){this[ss]instanceof XFAObject||super[Vs](e)}[hs](){"string"==typeof this[ss]&&(this[ss]=this[ss].replaceAll("\r\n","\n"))}[ws](){return"string"==typeof this[ss]?this[ss].split(/[\u2029\u2028\n]/).filter((e=>!!e)).join("\n"):this[ss][so]()}[co](e){if("string"==typeof this[ss]){const e=valueToHtml(this[ss]).html;if(this[ss].includes("\u2029")){e.name="div";e.children=[];this[ss].split("\u2029").map((e=>e.split(/[\u2028\n]/).flatMap((e=>[{name:"span",value:e},{name:"br"}])))).forEach((t=>{e.children.push({name:"p",children:t})}))}else if(/[\u2028\n]/.test(this[ss])){e.name="div";e.children=[];this[ss].split(/[\u2028\n]/).forEach((t=>{e.children.push({name:"span",value:t},{name:"br"})}))}return HTMLResult.success(e)}return this[ss][co](e)}}class TextEdit extends XFAObject{constructor(e){super(Go,"textEdit",!0);this.allowRichText=getInteger({data:e.allowRichText,defaultValue:0,validate:e=>1===e});this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.multiLine=getInteger({data:e.multiLine,defaultValue:"",validate:e=>0===e||1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.vScrollPolicy=getStringOption(e.vScrollPolicy,["auto","off","on"]);this.border=null;this.comb=null;this.extras=null;this.margin=null}[co](e){const t=toStyle(this,"border","font","margin");let a;const r=this[vs]()[vs]();""===this.multiLine&&(this.multiLine=r instanceof Draw?1:0);a=1===this.multiLine?{name:"textarea",attributes:{dataId:r[os]?.[uo]||r[uo],fieldId:r[uo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(r),"aria-required":!1}}:{name:"input",attributes:{type:"text",dataId:r[os]?.[uo]||r[uo],fieldId:r[uo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(r),"aria-required":!1}};if(isRequired(r)){a.attributes["aria-required"]=!0;a.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[a]})}}class Time extends StringObject{constructor(e){super(Go,"time");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[hs](){const e=this[ss].trim();this[ss]=e?new Date(e):null}[co](e){return valueToHtml(this[ss]?this[ss].toString():"")}}class TimeStamp extends XFAObject{constructor(e){super(Go,"timeStamp");this.id=e.id||"";this.server=e.server||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class ToolTip extends StringObject{constructor(e){super(Go,"toolTip");this.id=e.id||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Traversal extends XFAObject{constructor(e){super(Go,"traversal",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.traverse=new XFAObjectArray}}class Traverse extends XFAObject{constructor(e){super(Go,"traverse",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["next","back","down","first","left","right","up"]);this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.script=null}get name(){return this.operation}[Us](){return!1}}class Ui extends XFAObject{constructor(e){super(Go,"ui",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null;this.barcode=null;this.button=null;this.checkButton=null;this.choiceList=null;this.dateTimeEdit=null;this.defaultUi=null;this.imageEdit=null;this.numericEdit=null;this.passwordEdit=null;this.signature=null;this.textEdit=null}[ws](){if(void 0===this[ls]){for(const e of Object.getOwnPropertyNames(this)){if("extras"===e||"picture"===e)continue;const t=this[e];if(t instanceof XFAObject){this[ls]=t;return t}}this[ls]=null}return this[ls]}[co](e){const t=this[ws]();return t?t[co](e):HTMLResult.EMPTY}}class Validate extends XFAObject{constructor(e){super(Go,"validate",!0);this.formatTest=getStringOption(e.formatTest,["warning","disabled","error"]);this.id=e.id||"";this.nullTest=getStringOption(e.nullTest,["disabled","error","warning"]);this.scriptTest=getStringOption(e.scriptTest,["error","disabled","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.picture=null;this.script=null}}class Value extends XFAObject{constructor(e){super(Go,"value",!0);this.id=e.id||"";this.override=getInteger({data:e.override,defaultValue:0,validate:e=>1===e});this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.arc=null;this.boolean=null;this.date=null;this.dateTime=null;this.decimal=null;this.exData=null;this.float=null;this.image=null;this.integer=null;this.line=null;this.rectangle=null;this.text=null;this.time=null}[io](e){const t=this[vs]();if(t instanceof Field&&t.ui?.imageEdit){if(!this.image){this.image=new Image({});this[Qn](this.image)}this.image[ss]=e[ss];return}const a=e[Ws];if(null===this[a]){for(const e of Object.getOwnPropertyNames(this)){const t=this[e];if(t instanceof XFAObject){this[e]=null;this[Zs](t)}}this[e[Ws]]=e;this[Qn](e)}else this[a][ss]=e[ss]}[so](){if(this.exData)return"string"==typeof this.exData[ss]?this.exData[ss].trim():this.exData[ss][so]().trim();for(const e of Object.getOwnPropertyNames(this)){if("image"===e)continue;const t=this[e];if(t instanceof XFAObject)return(t[ss]||"").toString().trim()}return null}[co](e){for(const t of Object.getOwnPropertyNames(this)){const a=this[t];if(a instanceof XFAObject)return a[co](e)}return HTMLResult.EMPTY}}class Variables extends XFAObject{constructor(e){super(Go,"variables",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.manifest=new XFAObjectArray;this.script=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[Us](){return!0}}class TemplateNamespace{static[fo](e,t){if(TemplateNamespace.hasOwnProperty(e)){const a=TemplateNamespace[e](t);a[ro](t);return a}}static appearanceFilter(e){return new AppearanceFilter(e)}static arc(e){return new Arc(e)}static area(e){return new Area(e)}static assist(e){return new Assist(e)}static barcode(e){return new Barcode(e)}static bind(e){return new Bind(e)}static bindItems(e){return new BindItems(e)}static bookend(e){return new Bookend(e)}static boolean(e){return new BooleanElement(e)}static border(e){return new Border(e)}static break(e){return new Break(e)}static breakAfter(e){return new BreakAfter(e)}static breakBefore(e){return new BreakBefore(e)}static button(e){return new Button(e)}static calculate(e){return new Calculate(e)}static caption(e){return new Caption(e)}static certificate(e){return new Certificate(e)}static certificates(e){return new Certificates(e)}static checkButton(e){return new CheckButton(e)}static choiceList(e){return new ChoiceList(e)}static color(e){return new Color(e)}static comb(e){return new Comb(e)}static connect(e){return new Connect(e)}static contentArea(e){return new ContentArea(e)}static corner(e){return new Corner(e)}static date(e){return new DateElement(e)}static dateTime(e){return new DateTime(e)}static dateTimeEdit(e){return new DateTimeEdit(e)}static decimal(e){return new Decimal(e)}static defaultUi(e){return new DefaultUi(e)}static desc(e){return new Desc(e)}static digestMethod(e){return new DigestMethod(e)}static digestMethods(e){return new DigestMethods(e)}static draw(e){return new Draw(e)}static edge(e){return new Edge(e)}static encoding(e){return new Encoding(e)}static encodings(e){return new Encodings(e)}static encrypt(e){return new Encrypt(e)}static encryptData(e){return new EncryptData(e)}static encryption(e){return new Encryption(e)}static encryptionMethod(e){return new EncryptionMethod(e)}static encryptionMethods(e){return new EncryptionMethods(e)}static event(e){return new Event(e)}static exData(e){return new ExData(e)}static exObject(e){return new ExObject(e)}static exclGroup(e){return new ExclGroup(e)}static execute(e){return new Execute(e)}static extras(e){return new Extras(e)}static field(e){return new Field(e)}static fill(e){return new Fill(e)}static filter(e){return new Filter(e)}static float(e){return new Float(e)}static font(e){return new template_Font(e)}static format(e){return new Format(e)}static handler(e){return new Handler(e)}static hyphenation(e){return new Hyphenation(e)}static image(e){return new Image(e)}static imageEdit(e){return new ImageEdit(e)}static integer(e){return new Integer(e)}static issuers(e){return new Issuers(e)}static items(e){return new Items(e)}static keep(e){return new Keep(e)}static keyUsage(e){return new KeyUsage(e)}static line(e){return new Line(e)}static linear(e){return new Linear(e)}static lockDocument(e){return new LockDocument(e)}static manifest(e){return new Manifest(e)}static margin(e){return new Margin(e)}static mdp(e){return new Mdp(e)}static medium(e){return new Medium(e)}static message(e){return new Message(e)}static numericEdit(e){return new NumericEdit(e)}static occur(e){return new Occur(e)}static oid(e){return new Oid(e)}static oids(e){return new Oids(e)}static overflow(e){return new Overflow(e)}static pageArea(e){return new PageArea(e)}static pageSet(e){return new PageSet(e)}static para(e){return new Para(e)}static passwordEdit(e){return new PasswordEdit(e)}static pattern(e){return new template_Pattern(e)}static picture(e){return new Picture(e)}static proto(e){return new Proto(e)}static radial(e){return new Radial(e)}static reason(e){return new Reason(e)}static reasons(e){return new Reasons(e)}static rectangle(e){return new Rectangle(e)}static ref(e){return new RefElement(e)}static script(e){return new Script(e)}static setProperty(e){return new SetProperty(e)}static signData(e){return new SignData(e)}static signature(e){return new Signature(e)}static signing(e){return new Signing(e)}static solid(e){return new Solid(e)}static speak(e){return new Speak(e)}static stipple(e){return new Stipple(e)}static subform(e){return new Subform(e)}static subformSet(e){return new SubformSet(e)}static subjectDN(e){return new SubjectDN(e)}static subjectDNs(e){return new SubjectDNs(e)}static submit(e){return new Submit(e)}static template(e){return new Template(e)}static text(e){return new Text(e)}static textEdit(e){return new TextEdit(e)}static time(e){return new Time(e)}static timeStamp(e){return new TimeStamp(e)}static toolTip(e){return new ToolTip(e)}static traversal(e){return new Traversal(e)}static traverse(e){return new Traverse(e)}static ui(e){return new Ui(e)}static validate(e){return new Validate(e)}static value(e){return new Value(e)}static variables(e){return new Variables(e)}}const Zo=go.datasets.id;function createText(e){const t=new Text({});t[ss]=e;return t}class Binder{constructor(e){this.root=e;this.datasets=e.datasets;this.data=e.datasets?.data||new XmlObject(go.datasets.id,"data");this.emptyMerge=0===this.data[xs]().length;this.root.form=this.form=e.template[is]()}_isConsumeData(){return!this.emptyMerge&&this._mergeMode}_isMatchTemplate(){return!this._isConsumeData()}bind(){this._bindElement(this.form,this.data);return this.form}getData(){return this.data}_bindValue(e,t,a){e[os]=t;if(e[Os]())if(t[Ns]()){const a=t[ys]();e[io](createText(a))}else if(e instanceof Field&&"multiSelect"===e.ui?.choiceList?.open){const a=t[xs]().map((e=>e[ss].trim())).join("\n");e[io](createText(a))}else this._isConsumeData()&&warn("XFA - Nodes haven't the same type.");else!t[Ns]()||this._isMatchTemplate()?this._bindElement(e,t):warn("XFA - Nodes haven't the same type.")}_findDataByNameToConsume(e,t,a,r){if(!e)return null;let i,n;for(let r=0;r<3;r++){i=a[Ss](e,!1,!0);for(;;){n=i.next().value;if(!n)break;if(t===n[Ns]())return n}if(a[Hs]===go.datasets.id&&"data"===a[Ws])break;a=a[vs]()}if(!r)return null;i=this.data[Ss](e,!0,!1);n=i.next().value;if(n)return n;i=this.data[ds](e,!0);n=i.next().value;return n?.[Ns]()?n:null}_setProperties(e,t){if(e.hasOwnProperty("setProperty"))for(const{ref:a,target:r,connection:i}of e.setProperty.children){if(i)continue;if(!a)continue;const n=searchNode(this.root,t,a,!1,!1);if(!n){warn(`XFA - Invalid reference: ${a}.`);continue}const[s]=n;if(!s[Es](this.data)){warn("XFA - Invalid node: must be a data node.");continue}const o=searchNode(this.root,e,r,!1,!1);if(!o){warn(`XFA - Invalid target: ${r}.`);continue}const[c]=o;if(!c[Es](e)){warn("XFA - Invalid target: must be a property or subproperty.");continue}const l=c[vs]();if(c instanceof SetProperty||l instanceof SetProperty){warn("XFA - Invalid target: cannot be a setProperty or one of its properties.");continue}if(c instanceof BindItems||l instanceof BindItems){warn("XFA - Invalid target: cannot be a bindItems or one of its properties.");continue}const h=s[so](),u=c[Ws];if(c instanceof XFAAttribute){const e=Object.create(null);e[u]=h;const t=Reflect.construct(Object.getPrototypeOf(l).constructor,[e]);l[u]=t[u]}else if(c.hasOwnProperty(ss)){c[os]=s;c[ss]=h;c[hs]()}else warn("XFA - Invalid node to use in setProperty")}}_bindItems(e,t){if(!e.hasOwnProperty("items")||!e.hasOwnProperty("bindItems")||e.bindItems.isEmpty())return;for(const t of e.items.children)e[Zs](t);e.items.clear();const a=new Items({}),r=new Items({});e[Qn](a);e.items.push(a);e[Qn](r);e.items.push(r);for(const{ref:i,labelRef:n,valueRef:s,connection:o}of e.bindItems.children){if(o)continue;if(!i)continue;const e=searchNode(this.root,t,i,!1,!1);if(e)for(const t of e){if(!t[Es](this.datasets)){warn(`XFA - Invalid ref (${i}): must be a datasets child.`);continue}const e=searchNode(this.root,t,n,!0,!1);if(!e){warn(`XFA - Invalid label: ${n}.`);continue}const[o]=e;if(!o[Es](this.datasets)){warn("XFA - Invalid label: must be a datasets child.");continue}const c=searchNode(this.root,t,s,!0,!1);if(!c){warn(`XFA - Invalid value: ${s}.`);continue}const[l]=c;if(!l[Es](this.datasets)){warn("XFA - Invalid value: must be a datasets child.");continue}const h=createText(o[so]()),u=createText(l[so]());a[Qn](h);a.text.push(h);r[Qn](u);r.text.push(u)}else warn(`XFA - Invalid reference: ${i}.`)}}_bindOccurrences(e,t,a){let r;if(t.length>1){r=e[is]();r[Zs](r.occur);r.occur=null}this._bindValue(e,t[0],a);this._setProperties(e,t[0]);this._bindItems(e,t[0]);if(1===t.length)return;const i=e[vs](),n=e[Ws],s=i[Ms](e);for(let e=1,o=t.length;et.name===e.name)).length:a[r].children.length;const n=a[Ms](e)+1,s=t.initial-i;if(s){const t=e[is]();t[Zs](t.occur);t.occur=null;a[r].push(t);a[Ds](n,t);for(let e=1;e0)this._bindOccurrences(r,[e[0]],null);else if(this.emptyMerge){const e=t[Hs]===Zo?-1:t[Hs],a=r[os]=new XmlObject(e,r.name||"root");t[Qn](a);this._bindElement(r,a)}continue}if(!r[Rs]())continue;let e=!1,i=null,n=null,s=null;if(r.bind){switch(r.bind.match){case"none":this._setAndBind(r,t);continue;case"global":e=!0;break;case"dataRef":if(!r.bind.ref){warn(`XFA - ref is empty in node ${r[Ws]}.`);this._setAndBind(r,t);continue}n=r.bind.ref}r.bind.picture&&(i=r.bind.picture[ss])}const[o,c]=this._getOccurInfo(r);if(n){s=searchNode(this.root,t,n,!0,!1);if(null===s){s=createDataNode(this.data,t,n);if(!s)continue;this._isConsumeData()&&(s[ns]=!0);this._setAndBind(r,s);continue}this._isConsumeData()&&(s=s.filter((e=>!e[ns])));s.length>c?s=s.slice(0,c):0===s.length&&(s=null);s&&this._isConsumeData()&&s.forEach((e=>{e[ns]=!0}))}else{if(!r.name){this._setAndBind(r,t);continue}if(this._isConsumeData()){const a=[];for(;a.length0?a:null}else{s=t[Ss](r.name,!1,this.emptyMerge).next().value;if(!s){if(0===o){a.push(r);continue}const e=t[Hs]===Zo?-1:t[Hs];s=r[os]=new XmlObject(e,r.name);this.emptyMerge&&(s[ns]=!0);t[Qn](s);this._setAndBind(r,s);continue}this.emptyMerge&&(s[ns]=!0);s=[s]}}s?this._bindOccurrences(r,s,i):o>0?this._setAndBind(r,t):a.push(r)}a.forEach((e=>e[vs]()[Zs](e)))}}class DataHandler{constructor(e,t){this.data=t;this.dataset=e.datasets||null}serialize(e){const t=[[-1,this.data[xs]()]];for(;t.length>0;){const a=t.at(-1),[r,i]=a;if(r+1===i.length){t.pop();continue}const n=i[++a[0]],s=e.get(n[uo]);if(s)n[io](s);else{const t=n[fs]();for(const a of t.values()){const t=e.get(a[uo]);if(t){a[io](t);break}}}const o=n[xs]();o.length>0&&t.push([-1,o])}const a=[''];if(this.dataset)for(const e of this.dataset[xs]())"data"!==e[Ws]&&e[lo](a);this.data[lo](a);a.push("");return a.join("")}}const Qo=go.config.id;class Acrobat extends XFAObject{constructor(e){super(Qo,"acrobat",!0);this.acrobat7=null;this.autoSave=null;this.common=null;this.validate=null;this.validateApprovalSignatures=null;this.submitUrl=new XFAObjectArray}}class Acrobat7 extends XFAObject{constructor(e){super(Qo,"acrobat7",!0);this.dynamicRender=null}}class ADBE_JSConsole extends OptionObject{constructor(e){super(Qo,"ADBE_JSConsole",["delegate","Enable","Disable"])}}class ADBE_JSDebugger extends OptionObject{constructor(e){super(Qo,"ADBE_JSDebugger",["delegate","Enable","Disable"])}}class AddSilentPrint extends Option01{constructor(e){super(Qo,"addSilentPrint")}}class AddViewerPreferences extends Option01{constructor(e){super(Qo,"addViewerPreferences")}}class AdjustData extends Option10{constructor(e){super(Qo,"adjustData")}}class AdobeExtensionLevel extends IntegerObject{constructor(e){super(Qo,"adobeExtensionLevel",0,(e=>e>=1&&e<=8))}}class Agent extends XFAObject{constructor(e){super(Qo,"agent",!0);this.name=e.name?e.name.trim():"";this.common=new XFAObjectArray}}class AlwaysEmbed extends ContentObject{constructor(e){super(Qo,"alwaysEmbed")}}class Amd extends StringObject{constructor(e){super(Qo,"amd")}}class config_Area extends XFAObject{constructor(e){super(Qo,"area");this.level=getInteger({data:e.level,defaultValue:0,validate:e=>e>=1&&e<=3});this.name=getStringOption(e.name,["","barcode","coreinit","deviceDriver","font","general","layout","merge","script","signature","sourceSet","templateCache"])}}class Attributes extends OptionObject{constructor(e){super(Qo,"attributes",["preserve","delegate","ignore"])}}class AutoSave extends OptionObject{constructor(e){super(Qo,"autoSave",["disabled","enabled"])}}class Base extends StringObject{constructor(e){super(Qo,"base")}}class BatchOutput extends XFAObject{constructor(e){super(Qo,"batchOutput");this.format=getStringOption(e.format,["none","concat","zip","zipCompress"])}}class BehaviorOverride extends ContentObject{constructor(e){super(Qo,"behaviorOverride")}[hs](){this[ss]=new Map(this[ss].trim().split(/\s+/).filter((e=>e.includes(":"))).map((e=>e.split(":",2))))}}class Cache extends XFAObject{constructor(e){super(Qo,"cache",!0);this.templateCache=null}}class Change extends Option01{constructor(e){super(Qo,"change")}}class Common extends XFAObject{constructor(e){super(Qo,"common",!0);this.data=null;this.locale=null;this.localeSet=null;this.messaging=null;this.suppressBanner=null;this.template=null;this.validationMessaging=null;this.versionControl=null;this.log=new XFAObjectArray}}class Compress extends XFAObject{constructor(e){super(Qo,"compress");this.scope=getStringOption(e.scope,["imageOnly","document"])}}class CompressLogicalStructure extends Option01{constructor(e){super(Qo,"compressLogicalStructure")}}class CompressObjectStream extends Option10{constructor(e){super(Qo,"compressObjectStream")}}class Compression extends XFAObject{constructor(e){super(Qo,"compression",!0);this.compressLogicalStructure=null;this.compressObjectStream=null;this.level=null;this.type=null}}class Config extends XFAObject{constructor(e){super(Qo,"config",!0);this.acrobat=null;this.present=null;this.trace=null;this.agent=new XFAObjectArray}}class Conformance extends OptionObject{constructor(e){super(Qo,"conformance",["A","B"])}}class ContentCopy extends Option01{constructor(e){super(Qo,"contentCopy")}}class Copies extends IntegerObject{constructor(e){super(Qo,"copies",1,(e=>e>=1))}}class Creator extends StringObject{constructor(e){super(Qo,"creator")}}class CurrentPage extends IntegerObject{constructor(e){super(Qo,"currentPage",0,(e=>e>=0))}}class Data extends XFAObject{constructor(e){super(Qo,"data",!0);this.adjustData=null;this.attributes=null;this.incrementalLoad=null;this.outputXSL=null;this.range=null;this.record=null;this.startNode=null;this.uri=null;this.window=null;this.xsl=null;this.excludeNS=new XFAObjectArray;this.transform=new XFAObjectArray}}class Debug extends XFAObject{constructor(e){super(Qo,"debug",!0);this.uri=null}}class DefaultTypeface extends ContentObject{constructor(e){super(Qo,"defaultTypeface");this.writingScript=getStringOption(e.writingScript,["*","Arabic","Cyrillic","EastEuropeanRoman","Greek","Hebrew","Japanese","Korean","Roman","SimplifiedChinese","Thai","TraditionalChinese","Vietnamese"])}}class Destination extends OptionObject{constructor(e){super(Qo,"destination",["pdf","pcl","ps","webClient","zpl"])}}class DocumentAssembly extends Option01{constructor(e){super(Qo,"documentAssembly")}}class Driver extends XFAObject{constructor(e){super(Qo,"driver",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class DuplexOption extends OptionObject{constructor(e){super(Qo,"duplexOption",["simplex","duplexFlipLongEdge","duplexFlipShortEdge"])}}class DynamicRender extends OptionObject{constructor(e){super(Qo,"dynamicRender",["forbidden","required"])}}class Embed extends Option01{constructor(e){super(Qo,"embed")}}class config_Encrypt extends Option01{constructor(e){super(Qo,"encrypt")}}class config_Encryption extends XFAObject{constructor(e){super(Qo,"encryption",!0);this.encrypt=null;this.encryptionLevel=null;this.permissions=null}}class EncryptionLevel extends OptionObject{constructor(e){super(Qo,"encryptionLevel",["40bit","128bit"])}}class Enforce extends StringObject{constructor(e){super(Qo,"enforce")}}class Equate extends XFAObject{constructor(e){super(Qo,"equate");this.force=getInteger({data:e.force,defaultValue:1,validate:e=>0===e});this.from=e.from||"";this.to=e.to||""}}class EquateRange extends XFAObject{constructor(e){super(Qo,"equateRange");this.from=e.from||"";this.to=e.to||"";this._unicodeRange=e.unicodeRange||""}get unicodeRange(){const e=[],t=/U\+([0-9a-fA-F]+)/,a=this._unicodeRange;for(let r of a.split(",").map((e=>e.trim())).filter((e=>!!e))){r=r.split("-",2).map((e=>{const a=e.match(t);return a?parseInt(a[1],16):0}));1===r.length&&r.push(r[0]);e.push(r)}return shadow(this,"unicodeRange",e)}}class Exclude extends ContentObject{constructor(e){super(Qo,"exclude")}[hs](){this[ss]=this[ss].trim().split(/\s+/).filter((e=>e&&["calculate","close","enter","exit","initialize","ready","validate"].includes(e)))}}class ExcludeNS extends StringObject{constructor(e){super(Qo,"excludeNS")}}class FlipLabel extends OptionObject{constructor(e){super(Qo,"flipLabel",["usePrinterSetting","on","off"])}}class config_FontInfo extends XFAObject{constructor(e){super(Qo,"fontInfo",!0);this.embed=null;this.map=null;this.subsetBelow=null;this.alwaysEmbed=new XFAObjectArray;this.defaultTypeface=new XFAObjectArray;this.neverEmbed=new XFAObjectArray}}class FormFieldFilling extends Option01{constructor(e){super(Qo,"formFieldFilling")}}class GroupParent extends StringObject{constructor(e){super(Qo,"groupParent")}}class IfEmpty extends OptionObject{constructor(e){super(Qo,"ifEmpty",["dataValue","dataGroup","ignore","remove"])}}class IncludeXDPContent extends StringObject{constructor(e){super(Qo,"includeXDPContent")}}class IncrementalLoad extends OptionObject{constructor(e){super(Qo,"incrementalLoad",["none","forwardOnly"])}}class IncrementalMerge extends Option01{constructor(e){super(Qo,"incrementalMerge")}}class Interactive extends Option01{constructor(e){super(Qo,"interactive")}}class Jog extends OptionObject{constructor(e){super(Qo,"jog",["usePrinterSetting","none","pageSet"])}}class LabelPrinter extends XFAObject{constructor(e){super(Qo,"labelPrinter",!0);this.name=getStringOption(e.name,["zpl","dpl","ipl","tcpl"]);this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class Layout extends OptionObject{constructor(e){super(Qo,"layout",["paginate","panel"])}}class Level extends IntegerObject{constructor(e){super(Qo,"level",0,(e=>e>0))}}class Linearized extends Option01{constructor(e){super(Qo,"linearized")}}class Locale extends StringObject{constructor(e){super(Qo,"locale")}}class LocaleSet extends StringObject{constructor(e){super(Qo,"localeSet")}}class Log extends XFAObject{constructor(e){super(Qo,"log",!0);this.mode=null;this.threshold=null;this.to=null;this.uri=null}}class MapElement extends XFAObject{constructor(e){super(Qo,"map",!0);this.equate=new XFAObjectArray;this.equateRange=new XFAObjectArray}}class MediumInfo extends XFAObject{constructor(e){super(Qo,"mediumInfo",!0);this.map=null}}class config_Message extends XFAObject{constructor(e){super(Qo,"message",!0);this.msgId=null;this.severity=null}}class Messaging extends XFAObject{constructor(e){super(Qo,"messaging",!0);this.message=new XFAObjectArray}}class Mode extends OptionObject{constructor(e){super(Qo,"mode",["append","overwrite"])}}class ModifyAnnots extends Option01{constructor(e){super(Qo,"modifyAnnots")}}class MsgId extends IntegerObject{constructor(e){super(Qo,"msgId",1,(e=>e>=1))}}class NameAttr extends StringObject{constructor(e){super(Qo,"nameAttr")}}class NeverEmbed extends ContentObject{constructor(e){super(Qo,"neverEmbed")}}class NumberOfCopies extends IntegerObject{constructor(e){super(Qo,"numberOfCopies",null,(e=>e>=2&&e<=5))}}class OpenAction extends XFAObject{constructor(e){super(Qo,"openAction",!0);this.destination=null}}class Output extends XFAObject{constructor(e){super(Qo,"output",!0);this.to=null;this.type=null;this.uri=null}}class OutputBin extends StringObject{constructor(e){super(Qo,"outputBin")}}class OutputXSL extends XFAObject{constructor(e){super(Qo,"outputXSL",!0);this.uri=null}}class Overprint extends OptionObject{constructor(e){super(Qo,"overprint",["none","both","draw","field"])}}class Packets extends StringObject{constructor(e){super(Qo,"packets")}[hs](){"*"!==this[ss]&&(this[ss]=this[ss].trim().split(/\s+/).filter((e=>["config","datasets","template","xfdf","xslt"].includes(e))))}}class PageOffset extends XFAObject{constructor(e){super(Qo,"pageOffset");this.x=getInteger({data:e.x,defaultValue:"useXDCSetting",validate:e=>!0});this.y=getInteger({data:e.y,defaultValue:"useXDCSetting",validate:e=>!0})}}class PageRange extends StringObject{constructor(e){super(Qo,"pageRange")}[hs](){const e=this[ss].trim().split(/\s+/).map((e=>parseInt(e,10))),t=[];for(let a=0,r=e.length;a!1))}}class Pcl extends XFAObject{constructor(e){super(Qo,"pcl",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.pageOffset=null;this.staple=null;this.xdc=null}}class Pdf extends XFAObject{constructor(e){super(Qo,"pdf",!0);this.name=e.name||"";this.adobeExtensionLevel=null;this.batchOutput=null;this.compression=null;this.creator=null;this.encryption=null;this.fontInfo=null;this.interactive=null;this.linearized=null;this.openAction=null;this.pdfa=null;this.producer=null;this.renderPolicy=null;this.scriptModel=null;this.silentPrint=null;this.submitFormat=null;this.tagged=null;this.version=null;this.viewerPreferences=null;this.xdc=null}}class Pdfa extends XFAObject{constructor(e){super(Qo,"pdfa",!0);this.amd=null;this.conformance=null;this.includeXDPContent=null;this.part=null}}class Permissions extends XFAObject{constructor(e){super(Qo,"permissions",!0);this.accessibleContent=null;this.change=null;this.contentCopy=null;this.documentAssembly=null;this.formFieldFilling=null;this.modifyAnnots=null;this.plaintextMetadata=null;this.print=null;this.printHighQuality=null}}class PickTrayByPDFSize extends Option01{constructor(e){super(Qo,"pickTrayByPDFSize")}}class config_Picture extends StringObject{constructor(e){super(Qo,"picture")}}class PlaintextMetadata extends Option01{constructor(e){super(Qo,"plaintextMetadata")}}class Presence extends OptionObject{constructor(e){super(Qo,"presence",["preserve","dissolve","dissolveStructure","ignore","remove"])}}class Present extends XFAObject{constructor(e){super(Qo,"present",!0);this.behaviorOverride=null;this.cache=null;this.common=null;this.copies=null;this.destination=null;this.incrementalMerge=null;this.layout=null;this.output=null;this.overprint=null;this.pagination=null;this.paginationOverride=null;this.script=null;this.validate=null;this.xdp=null;this.driver=new XFAObjectArray;this.labelPrinter=new XFAObjectArray;this.pcl=new XFAObjectArray;this.pdf=new XFAObjectArray;this.ps=new XFAObjectArray;this.submitUrl=new XFAObjectArray;this.webClient=new XFAObjectArray;this.zpl=new XFAObjectArray}}class Print extends Option01{constructor(e){super(Qo,"print")}}class PrintHighQuality extends Option01{constructor(e){super(Qo,"printHighQuality")}}class PrintScaling extends OptionObject{constructor(e){super(Qo,"printScaling",["appdefault","noScaling"])}}class PrinterName extends StringObject{constructor(e){super(Qo,"printerName")}}class Producer extends StringObject{constructor(e){super(Qo,"producer")}}class Ps extends XFAObject{constructor(e){super(Qo,"ps",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.staple=null;this.xdc=null}}class Range extends ContentObject{constructor(e){super(Qo,"range")}[hs](){this[ss]=this[ss].split(",",2).map((e=>e.split("-").map((e=>parseInt(e.trim(),10))))).filter((e=>e.every((e=>!isNaN(e))))).map((e=>{1===e.length&&e.push(e[0]);return e}))}}class Record extends ContentObject{constructor(e){super(Qo,"record")}[hs](){this[ss]=this[ss].trim();const e=parseInt(this[ss],10);!isNaN(e)&&e>=0&&(this[ss]=e)}}class Relevant extends ContentObject{constructor(e){super(Qo,"relevant")}[hs](){this[ss]=this[ss].trim().split(/\s+/)}}class Rename extends ContentObject{constructor(e){super(Qo,"rename")}[hs](){this[ss]=this[ss].trim();(this[ss].toLowerCase().startsWith("xml")||new RegExp("[\\p{L}_][\\p{L}\\d._\\p{M}-]*","u").test(this[ss]))&&warn("XFA - Rename: invalid XFA name")}}class RenderPolicy extends OptionObject{constructor(e){super(Qo,"renderPolicy",["server","client"])}}class RunScripts extends OptionObject{constructor(e){super(Qo,"runScripts",["both","client","none","server"])}}class config_Script extends XFAObject{constructor(e){super(Qo,"script",!0);this.currentPage=null;this.exclude=null;this.runScripts=null}}class ScriptModel extends OptionObject{constructor(e){super(Qo,"scriptModel",["XFA","none"])}}class Severity extends OptionObject{constructor(e){super(Qo,"severity",["ignore","error","information","trace","warning"])}}class SilentPrint extends XFAObject{constructor(e){super(Qo,"silentPrint",!0);this.addSilentPrint=null;this.printerName=null}}class Staple extends XFAObject{constructor(e){super(Qo,"staple");this.mode=getStringOption(e.mode,["usePrinterSetting","on","off"])}}class StartNode extends StringObject{constructor(e){super(Qo,"startNode")}}class StartPage extends IntegerObject{constructor(e){super(Qo,"startPage",0,(e=>!0))}}class SubmitFormat extends OptionObject{constructor(e){super(Qo,"submitFormat",["html","delegate","fdf","xml","pdf"])}}class SubmitUrl extends StringObject{constructor(e){super(Qo,"submitUrl")}}class SubsetBelow extends IntegerObject{constructor(e){super(Qo,"subsetBelow",100,(e=>e>=0&&e<=100))}}class SuppressBanner extends Option01{constructor(e){super(Qo,"suppressBanner")}}class Tagged extends Option01{constructor(e){super(Qo,"tagged")}}class config_Template extends XFAObject{constructor(e){super(Qo,"template",!0);this.base=null;this.relevant=null;this.startPage=null;this.uri=null;this.xsl=null}}class Threshold extends OptionObject{constructor(e){super(Qo,"threshold",["trace","error","information","warning"])}}class To extends OptionObject{constructor(e){super(Qo,"to",["null","memory","stderr","stdout","system","uri"])}}class TemplateCache extends XFAObject{constructor(e){super(Qo,"templateCache");this.maxEntries=getInteger({data:e.maxEntries,defaultValue:5,validate:e=>e>=0})}}class Trace extends XFAObject{constructor(e){super(Qo,"trace",!0);this.area=new XFAObjectArray}}class Transform extends XFAObject{constructor(e){super(Qo,"transform",!0);this.groupParent=null;this.ifEmpty=null;this.nameAttr=null;this.picture=null;this.presence=null;this.rename=null;this.whitespace=null}}class Type extends OptionObject{constructor(e){super(Qo,"type",["none","ascii85","asciiHex","ccittfax","flate","lzw","runLength","native","xdp","mergedXDP"])}}class Uri extends StringObject{constructor(e){super(Qo,"uri")}}class config_Validate extends OptionObject{constructor(e){super(Qo,"validate",["preSubmit","prePrint","preExecute","preSave"])}}class ValidateApprovalSignatures extends ContentObject{constructor(e){super(Qo,"validateApprovalSignatures")}[hs](){this[ss]=this[ss].trim().split(/\s+/).filter((e=>["docReady","postSign"].includes(e)))}}class ValidationMessaging extends OptionObject{constructor(e){super(Qo,"validationMessaging",["allMessagesIndividually","allMessagesTogether","firstMessageOnly","noMessages"])}}class Version extends OptionObject{constructor(e){super(Qo,"version",["1.7","1.6","1.5","1.4","1.3","1.2"])}}class VersionControl extends XFAObject{constructor(e){super(Qo,"VersionControl");this.outputBelow=getStringOption(e.outputBelow,["warn","error","update"]);this.sourceAbove=getStringOption(e.sourceAbove,["warn","error"]);this.sourceBelow=getStringOption(e.sourceBelow,["update","maintain"])}}class ViewerPreferences extends XFAObject{constructor(e){super(Qo,"viewerPreferences",!0);this.ADBE_JSConsole=null;this.ADBE_JSDebugger=null;this.addViewerPreferences=null;this.duplexOption=null;this.enforce=null;this.numberOfCopies=null;this.pageRange=null;this.pickTrayByPDFSize=null;this.printScaling=null}}class WebClient extends XFAObject{constructor(e){super(Qo,"webClient",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class Whitespace extends OptionObject{constructor(e){super(Qo,"whitespace",["preserve","ltrim","normalize","rtrim","trim"])}}class Window extends ContentObject{constructor(e){super(Qo,"window")}[hs](){const e=this[ss].split(",",2).map((e=>parseInt(e.trim(),10)));if(e.some((e=>isNaN(e))))this[ss]=[0,0];else{1===e.length&&e.push(e[0]);this[ss]=e}}}class Xdc extends XFAObject{constructor(e){super(Qo,"xdc",!0);this.uri=new XFAObjectArray;this.xsl=new XFAObjectArray}}class Xdp extends XFAObject{constructor(e){super(Qo,"xdp",!0);this.packets=null}}class Xsl extends XFAObject{constructor(e){super(Qo,"xsl",!0);this.debug=null;this.uri=null}}class Zpl extends XFAObject{constructor(e){super(Qo,"zpl",!0);this.name=e.name?e.name.trim():"";this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class ConfigNamespace{static[fo](e,t){if(ConfigNamespace.hasOwnProperty(e))return ConfigNamespace[e](t)}static acrobat(e){return new Acrobat(e)}static acrobat7(e){return new Acrobat7(e)}static ADBE_JSConsole(e){return new ADBE_JSConsole(e)}static ADBE_JSDebugger(e){return new ADBE_JSDebugger(e)}static addSilentPrint(e){return new AddSilentPrint(e)}static addViewerPreferences(e){return new AddViewerPreferences(e)}static adjustData(e){return new AdjustData(e)}static adobeExtensionLevel(e){return new AdobeExtensionLevel(e)}static agent(e){return new Agent(e)}static alwaysEmbed(e){return new AlwaysEmbed(e)}static amd(e){return new Amd(e)}static area(e){return new config_Area(e)}static attributes(e){return new Attributes(e)}static autoSave(e){return new AutoSave(e)}static base(e){return new Base(e)}static batchOutput(e){return new BatchOutput(e)}static behaviorOverride(e){return new BehaviorOverride(e)}static cache(e){return new Cache(e)}static change(e){return new Change(e)}static common(e){return new Common(e)}static compress(e){return new Compress(e)}static compressLogicalStructure(e){return new CompressLogicalStructure(e)}static compressObjectStream(e){return new CompressObjectStream(e)}static compression(e){return new Compression(e)}static config(e){return new Config(e)}static conformance(e){return new Conformance(e)}static contentCopy(e){return new ContentCopy(e)}static copies(e){return new Copies(e)}static creator(e){return new Creator(e)}static currentPage(e){return new CurrentPage(e)}static data(e){return new Data(e)}static debug(e){return new Debug(e)}static defaultTypeface(e){return new DefaultTypeface(e)}static destination(e){return new Destination(e)}static documentAssembly(e){return new DocumentAssembly(e)}static driver(e){return new Driver(e)}static duplexOption(e){return new DuplexOption(e)}static dynamicRender(e){return new DynamicRender(e)}static embed(e){return new Embed(e)}static encrypt(e){return new config_Encrypt(e)}static encryption(e){return new config_Encryption(e)}static encryptionLevel(e){return new EncryptionLevel(e)}static enforce(e){return new Enforce(e)}static equate(e){return new Equate(e)}static equateRange(e){return new EquateRange(e)}static exclude(e){return new Exclude(e)}static excludeNS(e){return new ExcludeNS(e)}static flipLabel(e){return new FlipLabel(e)}static fontInfo(e){return new config_FontInfo(e)}static formFieldFilling(e){return new FormFieldFilling(e)}static groupParent(e){return new GroupParent(e)}static ifEmpty(e){return new IfEmpty(e)}static includeXDPContent(e){return new IncludeXDPContent(e)}static incrementalLoad(e){return new IncrementalLoad(e)}static incrementalMerge(e){return new IncrementalMerge(e)}static interactive(e){return new Interactive(e)}static jog(e){return new Jog(e)}static labelPrinter(e){return new LabelPrinter(e)}static layout(e){return new Layout(e)}static level(e){return new Level(e)}static linearized(e){return new Linearized(e)}static locale(e){return new Locale(e)}static localeSet(e){return new LocaleSet(e)}static log(e){return new Log(e)}static map(e){return new MapElement(e)}static mediumInfo(e){return new MediumInfo(e)}static message(e){return new config_Message(e)}static messaging(e){return new Messaging(e)}static mode(e){return new Mode(e)}static modifyAnnots(e){return new ModifyAnnots(e)}static msgId(e){return new MsgId(e)}static nameAttr(e){return new NameAttr(e)}static neverEmbed(e){return new NeverEmbed(e)}static numberOfCopies(e){return new NumberOfCopies(e)}static openAction(e){return new OpenAction(e)}static output(e){return new Output(e)}static outputBin(e){return new OutputBin(e)}static outputXSL(e){return new OutputXSL(e)}static overprint(e){return new Overprint(e)}static packets(e){return new Packets(e)}static pageOffset(e){return new PageOffset(e)}static pageRange(e){return new PageRange(e)}static pagination(e){return new Pagination(e)}static paginationOverride(e){return new PaginationOverride(e)}static part(e){return new Part(e)}static pcl(e){return new Pcl(e)}static pdf(e){return new Pdf(e)}static pdfa(e){return new Pdfa(e)}static permissions(e){return new Permissions(e)}static pickTrayByPDFSize(e){return new PickTrayByPDFSize(e)}static picture(e){return new config_Picture(e)}static plaintextMetadata(e){return new PlaintextMetadata(e)}static presence(e){return new Presence(e)}static present(e){return new Present(e)}static print(e){return new Print(e)}static printHighQuality(e){return new PrintHighQuality(e)}static printScaling(e){return new PrintScaling(e)}static printerName(e){return new PrinterName(e)}static producer(e){return new Producer(e)}static ps(e){return new Ps(e)}static range(e){return new Range(e)}static record(e){return new Record(e)}static relevant(e){return new Relevant(e)}static rename(e){return new Rename(e)}static renderPolicy(e){return new RenderPolicy(e)}static runScripts(e){return new RunScripts(e)}static script(e){return new config_Script(e)}static scriptModel(e){return new ScriptModel(e)}static severity(e){return new Severity(e)}static silentPrint(e){return new SilentPrint(e)}static staple(e){return new Staple(e)}static startNode(e){return new StartNode(e)}static startPage(e){return new StartPage(e)}static submitFormat(e){return new SubmitFormat(e)}static submitUrl(e){return new SubmitUrl(e)}static subsetBelow(e){return new SubsetBelow(e)}static suppressBanner(e){return new SuppressBanner(e)}static tagged(e){return new Tagged(e)}static template(e){return new config_Template(e)}static templateCache(e){return new TemplateCache(e)}static threshold(e){return new Threshold(e)}static to(e){return new To(e)}static trace(e){return new Trace(e)}static transform(e){return new Transform(e)}static type(e){return new Type(e)}static uri(e){return new Uri(e)}static validate(e){return new config_Validate(e)}static validateApprovalSignatures(e){return new ValidateApprovalSignatures(e)}static validationMessaging(e){return new ValidationMessaging(e)}static version(e){return new Version(e)}static versionControl(e){return new VersionControl(e)}static viewerPreferences(e){return new ViewerPreferences(e)}static webClient(e){return new WebClient(e)}static whitespace(e){return new Whitespace(e)}static window(e){return new Window(e)}static xdc(e){return new Xdc(e)}static xdp(e){return new Xdp(e)}static xsl(e){return new Xsl(e)}static zpl(e){return new Zpl(e)}}const ec=go.connectionSet.id;class ConnectionSet extends XFAObject{constructor(e){super(ec,"connectionSet",!0);this.wsdlConnection=new XFAObjectArray;this.xmlConnection=new XFAObjectArray;this.xsdConnection=new XFAObjectArray}}class EffectiveInputPolicy extends XFAObject{constructor(e){super(ec,"effectiveInputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EffectiveOutputPolicy extends XFAObject{constructor(e){super(ec,"effectiveOutputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Operation extends StringObject{constructor(e){super(ec,"operation");this.id=e.id||"";this.input=e.input||"";this.name=e.name||"";this.output=e.output||"";this.use=e.use||"";this.usehref=e.usehref||""}}class RootElement extends StringObject{constructor(e){super(ec,"rootElement");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAction extends StringObject{constructor(e){super(ec,"soapAction");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAddress extends StringObject{constructor(e){super(ec,"soapAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class connection_set_Uri extends StringObject{constructor(e){super(ec,"uri");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlAddress extends StringObject{constructor(e){super(ec,"wsdlAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlConnection extends XFAObject{constructor(e){super(ec,"wsdlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.effectiveInputPolicy=null;this.effectiveOutputPolicy=null;this.operation=null;this.soapAction=null;this.soapAddress=null;this.wsdlAddress=null}}class XmlConnection extends XFAObject{constructor(e){super(ec,"xmlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.uri=null}}class XsdConnection extends XFAObject{constructor(e){super(ec,"xsdConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.rootElement=null;this.uri=null}}class ConnectionSetNamespace{static[fo](e,t){if(ConnectionSetNamespace.hasOwnProperty(e))return ConnectionSetNamespace[e](t)}static connectionSet(e){return new ConnectionSet(e)}static effectiveInputPolicy(e){return new EffectiveInputPolicy(e)}static effectiveOutputPolicy(e){return new EffectiveOutputPolicy(e)}static operation(e){return new Operation(e)}static rootElement(e){return new RootElement(e)}static soapAction(e){return new SoapAction(e)}static soapAddress(e){return new SoapAddress(e)}static uri(e){return new connection_set_Uri(e)}static wsdlAddress(e){return new WsdlAddress(e)}static wsdlConnection(e){return new WsdlConnection(e)}static xmlConnection(e){return new XmlConnection(e)}static xsdConnection(e){return new XsdConnection(e)}}const tc=go.datasets.id;class datasets_Data extends XmlObject{constructor(e){super(tc,"data",e)}[Ls](){return!0}}class Datasets extends XFAObject{constructor(e){super(tc,"datasets",!0);this.data=null;this.Signature=null}[$s](e){const t=e[Ws];("data"===t&&e[Hs]===tc||"Signature"===t&&e[Hs]===go.signature.id)&&(this[t]=e);this[Qn](e)}}class DatasetsNamespace{static[fo](e,t){if(DatasetsNamespace.hasOwnProperty(e))return DatasetsNamespace[e](t)}static datasets(e){return new Datasets(e)}static data(e){return new datasets_Data(e)}}const ac=go.localeSet.id;class CalendarSymbols extends XFAObject{constructor(e){super(ac,"calendarSymbols",!0);this.name="gregorian";this.dayNames=new XFAObjectArray(2);this.eraNames=null;this.meridiemNames=null;this.monthNames=new XFAObjectArray(2)}}class CurrencySymbol extends StringObject{constructor(e){super(ac,"currencySymbol");this.name=getStringOption(e.name,["symbol","isoname","decimal"])}}class CurrencySymbols extends XFAObject{constructor(e){super(ac,"currencySymbols",!0);this.currencySymbol=new XFAObjectArray(3)}}class DatePattern extends StringObject{constructor(e){super(ac,"datePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class DatePatterns extends XFAObject{constructor(e){super(ac,"datePatterns",!0);this.datePattern=new XFAObjectArray(4)}}class DateTimeSymbols extends ContentObject{constructor(e){super(ac,"dateTimeSymbols")}}class Day extends StringObject{constructor(e){super(ac,"day")}}class DayNames extends XFAObject{constructor(e){super(ac,"dayNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.day=new XFAObjectArray(7)}}class Era extends StringObject{constructor(e){super(ac,"era")}}class EraNames extends XFAObject{constructor(e){super(ac,"eraNames",!0);this.era=new XFAObjectArray(2)}}class locale_set_Locale extends XFAObject{constructor(e){super(ac,"locale",!0);this.desc=e.desc||"";this.name="isoname";this.calendarSymbols=null;this.currencySymbols=null;this.datePatterns=null;this.dateTimeSymbols=null;this.numberPatterns=null;this.numberSymbols=null;this.timePatterns=null;this.typeFaces=null}}class locale_set_LocaleSet extends XFAObject{constructor(e){super(ac,"localeSet",!0);this.locale=new XFAObjectArray}}class Meridiem extends StringObject{constructor(e){super(ac,"meridiem")}}class MeridiemNames extends XFAObject{constructor(e){super(ac,"meridiemNames",!0);this.meridiem=new XFAObjectArray(2)}}class Month extends StringObject{constructor(e){super(ac,"month")}}class MonthNames extends XFAObject{constructor(e){super(ac,"monthNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.month=new XFAObjectArray(12)}}class NumberPattern extends StringObject{constructor(e){super(ac,"numberPattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class NumberPatterns extends XFAObject{constructor(e){super(ac,"numberPatterns",!0);this.numberPattern=new XFAObjectArray(4)}}class NumberSymbol extends StringObject{constructor(e){super(ac,"numberSymbol");this.name=getStringOption(e.name,["decimal","grouping","percent","minus","zero"])}}class NumberSymbols extends XFAObject{constructor(e){super(ac,"numberSymbols",!0);this.numberSymbol=new XFAObjectArray(5)}}class TimePattern extends StringObject{constructor(e){super(ac,"timePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class TimePatterns extends XFAObject{constructor(e){super(ac,"timePatterns",!0);this.timePattern=new XFAObjectArray(4)}}class TypeFace extends XFAObject{constructor(e){super(ac,"typeFace",!0);this.name=""|e.name}}class TypeFaces extends XFAObject{constructor(e){super(ac,"typeFaces",!0);this.typeFace=new XFAObjectArray}}class LocaleSetNamespace{static[fo](e,t){if(LocaleSetNamespace.hasOwnProperty(e))return LocaleSetNamespace[e](t)}static calendarSymbols(e){return new CalendarSymbols(e)}static currencySymbol(e){return new CurrencySymbol(e)}static currencySymbols(e){return new CurrencySymbols(e)}static datePattern(e){return new DatePattern(e)}static datePatterns(e){return new DatePatterns(e)}static dateTimeSymbols(e){return new DateTimeSymbols(e)}static day(e){return new Day(e)}static dayNames(e){return new DayNames(e)}static era(e){return new Era(e)}static eraNames(e){return new EraNames(e)}static locale(e){return new locale_set_Locale(e)}static localeSet(e){return new locale_set_LocaleSet(e)}static meridiem(e){return new Meridiem(e)}static meridiemNames(e){return new MeridiemNames(e)}static month(e){return new Month(e)}static monthNames(e){return new MonthNames(e)}static numberPattern(e){return new NumberPattern(e)}static numberPatterns(e){return new NumberPatterns(e)}static numberSymbol(e){return new NumberSymbol(e)}static numberSymbols(e){return new NumberSymbols(e)}static timePattern(e){return new TimePattern(e)}static timePatterns(e){return new TimePatterns(e)}static typeFace(e){return new TypeFace(e)}static typeFaces(e){return new TypeFaces(e)}}const rc=go.signature.id;class signature_Signature extends XFAObject{constructor(e){super(rc,"signature",!0)}}class SignatureNamespace{static[fo](e,t){if(SignatureNamespace.hasOwnProperty(e))return SignatureNamespace[e](t)}static signature(e){return new signature_Signature(e)}}const ic=go.stylesheet.id;class Stylesheet extends XFAObject{constructor(e){super(ic,"stylesheet",!0)}}class StylesheetNamespace{static[fo](e,t){if(StylesheetNamespace.hasOwnProperty(e))return StylesheetNamespace[e](t)}static stylesheet(e){return new Stylesheet(e)}}const nc=go.xdp.id;class xdp_Xdp extends XFAObject{constructor(e){super(nc,"xdp",!0);this.uuid=e.uuid||"";this.timeStamp=e.timeStamp||"";this.config=null;this.connectionSet=null;this.datasets=null;this.localeSet=null;this.stylesheet=new XFAObjectArray;this.template=null}[Gs](e){const t=go[e[Ws]];return t&&e[Hs]===t.id}}class XdpNamespace{static[fo](e,t){if(XdpNamespace.hasOwnProperty(e))return XdpNamespace[e](t)}static xdp(e){return new xdp_Xdp(e)}}const sc=go.xhtml.id,oc=Symbol(),cc=new Set(["color","font","font-family","font-size","font-stretch","font-style","font-weight","margin","margin-bottom","margin-left","margin-right","margin-top","letter-spacing","line-height","orphans","page-break-after","page-break-before","page-break-inside","tab-interval","tab-stop","text-align","text-decoration","text-indent","vertical-align","widows","kerning-mode","xfa-font-horizontal-scale","xfa-font-vertical-scale","xfa-spacerun","xfa-tab-stops"]),lc=new Map([["page-break-after","breakAfter"],["page-break-before","breakBefore"],["page-break-inside","breakInside"],["kerning-mode",e=>"none"===e?"none":"normal"],["xfa-font-horizontal-scale",e=>`scaleX(${Math.max(0,parseInt(e)/100).toFixed(2)})`],["xfa-font-vertical-scale",e=>`scaleY(${Math.max(0,parseInt(e)/100).toFixed(2)})`],["xfa-spacerun",""],["xfa-tab-stops",""],["font-size",(e,t)=>measureToString(.99*(e=t.fontSize=Math.abs(getMeasurement(e))))],["letter-spacing",e=>measureToString(getMeasurement(e))],["line-height",e=>measureToString(getMeasurement(e))],["margin",e=>measureToString(getMeasurement(e))],["margin-bottom",e=>measureToString(getMeasurement(e))],["margin-left",e=>measureToString(getMeasurement(e))],["margin-right",e=>measureToString(getMeasurement(e))],["margin-top",e=>measureToString(getMeasurement(e))],["text-indent",e=>measureToString(getMeasurement(e))],["font-family",e=>e],["vertical-align",e=>measureToString(getMeasurement(e))]]),hc=/\s+/g,uc=/[\r\n]+/g,dc=/\r\n?/g;function mapStyle(e,t,a){const r=Object.create(null);if(!e)return r;const i=Object.create(null);for(const[t,a]of e.split(";").map((e=>e.split(":",2)))){const e=lc.get(t);if(""===e)continue;let n=a;e&&(n="string"==typeof e?e:e(a,i));t.endsWith("scale")?r.transform=r.transform?`${r[t]} ${n}`:n:r[t.replaceAll(/-([a-zA-Z])/g,((e,t)=>t.toUpperCase()))]=n}r.fontFamily&&setFontFamily({typeface:r.fontFamily,weight:r.fontWeight||"normal",posture:r.fontStyle||"normal",size:i.fontSize||0},t,t[Is].fontFinder,r);if(a&&r.verticalAlign&&"0px"!==r.verticalAlign&&r.fontSize){const e=.583,t=.333,a=getMeasurement(r.fontSize);r.fontSize=measureToString(a*e);r.verticalAlign=measureToString(Math.sign(getMeasurement(r.verticalAlign))*a*t)}a&&r.fontSize&&(r.fontSize=`calc(${r.fontSize} * var(--total-scale-factor))`);fixTextIndent(r);return r}const fc=new Set(["body","html"]);class XhtmlObject extends XmlObject{constructor(e,t){super(sc,t);this[oc]=!1;this.style=e.style||""}[ts](e){super[ts](e);this.style=function checkStyle(e){return e.style?e.style.split(";").filter((e=>!!e.trim())).map((e=>e.split(":",2).map((e=>e.trim())))).filter((([t,a])=>{"font-family"===t&&e[Is].usedTypefaces.add(a);return cc.has(t)})).map((e=>e.join(":"))).join(";"):""}(this)}[Yn](){return!fc.has(this[Ws])}[Vs](e,t=!1){if(t)this[oc]=!0;else{e=e.replaceAll(uc,"");this.style.includes("xfa-spacerun:yes")||(e=e.replaceAll(hc," "))}e&&(this[ss]+=e)}[Ks](e,t=!0){const a=Object.create(null),r={top:NaN,bottom:NaN,left:NaN,right:NaN};let i=null;for(const[e,t]of this.style.split(";").map((e=>e.split(":",2))))switch(e){case"font-family":a.typeface=stripQuotes(t);break;case"font-size":a.size=getMeasurement(t);break;case"font-weight":a.weight=t;break;case"font-style":a.posture=t;break;case"letter-spacing":a.letterSpacing=getMeasurement(t);break;case"margin":const e=t.split(/ \t/).map((e=>getMeasurement(e)));switch(e.length){case 1:r.top=r.bottom=r.left=r.right=e[0];break;case 2:r.top=r.bottom=e[0];r.left=r.right=e[1];break;case 3:r.top=e[0];r.bottom=e[2];r.left=r.right=e[1];break;case 4:r.top=e[0];r.left=e[1];r.bottom=e[2];r.right=e[3]}break;case"margin-top":r.top=getMeasurement(t);break;case"margin-bottom":r.bottom=getMeasurement(t);break;case"margin-left":r.left=getMeasurement(t);break;case"margin-right":r.right=getMeasurement(t);break;case"line-height":i=getMeasurement(t)}e.pushData(a,r,i);if(this[ss])e.addString(this[ss]);else for(const t of this[xs]())"#text"!==t[Ws]?t[Ks](e):e.addString(t[ss]);t&&e.popFont()}[co](e){const t=[];this[ls]={children:t};this[es]({});if(0===t.length&&!this[ss])return HTMLResult.EMPTY;let a;a=this[oc]?this[ss]?this[ss].replaceAll(dc,"\n"):void 0:this[ss]||void 0;return HTMLResult.success({name:this[Ws],attributes:{href:this.href,style:mapStyle(this.style,this,this[oc])},children:t,value:a})}}class A extends XhtmlObject{constructor(e){super(e,"a");this.href=fixURL(e.href)||""}}class B extends XhtmlObject{constructor(e){super(e,"b")}[Ks](e){e.pushFont({weight:"bold"});super[Ks](e);e.popFont()}}class Body extends XhtmlObject{constructor(e){super(e,"body")}[co](e){const t=super[co](e),{html:a}=t;if(!a)return HTMLResult.EMPTY;a.name="div";a.attributes.class=["xfaRich"];return t}}class Br extends XhtmlObject{constructor(e){super(e,"br")}[so](){return"\n"}[Ks](e){e.addString("\n")}[co](e){return HTMLResult.success({name:"br"})}}class Html extends XhtmlObject{constructor(e){super(e,"html")}[co](e){const t=[];this[ls]={children:t};this[es]({});if(0===t.length)return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},value:this[ss]||""});if(1===t.length){const e=t[0];if(e.attributes?.class.includes("xfaRich"))return HTMLResult.success(e)}return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},children:t})}}class I extends XhtmlObject{constructor(e){super(e,"i")}[Ks](e){e.pushFont({posture:"italic"});super[Ks](e);e.popFont()}}class Li extends XhtmlObject{constructor(e){super(e,"li")}}class Ol extends XhtmlObject{constructor(e){super(e,"ol")}}class P extends XhtmlObject{constructor(e){super(e,"p")}[Ks](e){super[Ks](e,!1);e.addString("\n");e.addPara();e.popFont()}[so](){return this[vs]()[xs]().at(-1)===this?super[so]():super[so]()+"\n"}}class Span extends XhtmlObject{constructor(e){super(e,"span")}}class Sub extends XhtmlObject{constructor(e){super(e,"sub")}}class Sup extends XhtmlObject{constructor(e){super(e,"sup")}}class Ul extends XhtmlObject{constructor(e){super(e,"ul")}}class XhtmlNamespace{static[fo](e,t){if(XhtmlNamespace.hasOwnProperty(e))return XhtmlNamespace[e](t)}static a(e){return new A(e)}static b(e){return new B(e)}static body(e){return new Body(e)}static br(e){return new Br(e)}static html(e){return new Html(e)}static i(e){return new I(e)}static li(e){return new Li(e)}static ol(e){return new Ol(e)}static p(e){return new P(e)}static span(e){return new Span(e)}static sub(e){return new Sub(e)}static sup(e){return new Sup(e)}static ul(e){return new Ul(e)}}const gc={config:ConfigNamespace,connection:ConnectionSetNamespace,datasets:DatasetsNamespace,localeSet:LocaleSetNamespace,signature:SignatureNamespace,stylesheet:StylesheetNamespace,template:TemplateNamespace,xdp:XdpNamespace,xhtml:XhtmlNamespace};class UnknownNamespace{constructor(e){this.namespaceId=e}[fo](e,t){return new XmlObject(this.namespaceId,e,t)}}class Root extends XFAObject{constructor(e){super(-1,"root",Object.create(null));this.element=null;this[Ts]=e}[$s](e){this.element=e;return!0}[hs](){super[hs]();if(this.element.template instanceof Template){this[Ts].set(Qs,this.element);this.element.template[eo](this[Ts]);this.element.template[Ts]=this[Ts]}}}class Empty extends XFAObject{constructor(){super(-1,"",Object.create(null))}[$s](e){return!1}}class Builder{constructor(e=null){this._namespaceStack=[];this._nsAgnosticLevel=0;this._namespacePrefixes=new Map;this._namespaces=new Map;this._nextNsId=Math.max(...Object.values(go).map((({id:e})=>e)));this._currentNamespace=e||new UnknownNamespace(++this._nextNsId)}buildRoot(e){return new Root(e)}build({nsPrefix:e,name:t,attributes:a,namespace:r,prefixes:i}){const n=null!==r;if(n){this._namespaceStack.push(this._currentNamespace);this._currentNamespace=this._searchNamespace(r)}i&&this._addNamespacePrefix(i);if(a.hasOwnProperty(zs)){const e=gc.datasets,t=a[zs];let r=null;for(const[a,i]of Object.entries(t)){if(this._getNamespaceToUse(a)===e){r={xfa:i};break}}r?a[zs]=r:delete a[zs]}const s=this._getNamespaceToUse(e),o=s?.[fo](t,a)||new Empty;o[Ls]()&&this._nsAgnosticLevel++;(n||i||o[Ls]())&&(o[rs]={hasNamespace:n,prefixes:i,nsAgnostic:o[Ls]()});return o}isNsAgnostic(){return this._nsAgnosticLevel>0}_searchNamespace(e){let t=this._namespaces.get(e);if(t)return t;for(const[a,{check:r}]of Object.entries(go))if(r(e)){t=gc[a];if(t){this._namespaces.set(e,t);return t}break}t=new UnknownNamespace(++this._nextNsId);this._namespaces.set(e,t);return t}_addNamespacePrefix(e){for(const{prefix:t,value:a}of e){const e=this._searchNamespace(a);let r=this._namespacePrefixes.get(t);if(!r){r=[];this._namespacePrefixes.set(t,r)}r.push(e)}}_getNamespaceToUse(e){if(!e)return this._currentNamespace;const t=this._namespacePrefixes.get(e);if(t?.length>0)return t.at(-1);warn(`Unknown namespace prefix: ${e}.`);return null}clean(e){const{hasNamespace:t,prefixes:a,nsAgnostic:r}=e;t&&(this._currentNamespace=this._namespaceStack.pop());a&&a.forEach((({prefix:e})=>{this._namespacePrefixes.get(e).pop()}));r&&this._nsAgnosticLevel--}}class XFAParser extends XMLParserBase{constructor(e=null,t=!1){super();this._builder=new Builder(e);this._stack=[];this._globalData={usedTypefaces:new Set};this._ids=new Map;this._current=this._builder.buildRoot(this._ids);this._errorCode=_n;this._whiteRegex=/^\s+$/;this._nbsps=/\xa0+/g;this._richText=t}parse(e){this.parseXml(e);if(this._errorCode===_n){this._current[hs]();return this._current.element}}onText(e){e=e.replace(this._nbsps,(e=>e.slice(1)+" "));this._richText||this._current[Yn]()?this._current[Vs](e,this._richText):this._whiteRegex.test(e)||this._current[Vs](e.trim())}onCdata(e){this._current[Vs](e)}_mkAttributes(e,t){let a=null,r=null;const i=Object.create({});for(const{name:n,value:s}of e)if("xmlns"===n)a?warn(`XFA - multiple namespace definition in <${t}>`):a=s;else if(n.startsWith("xmlns:")){const e=n.substring(6);r??=[];r.push({prefix:e,value:s})}else{const e=n.indexOf(":");if(-1===e)i[n]=s;else{const t=i[zs]??=Object.create(null),[a,r]=[n.slice(0,e),n.slice(e+1)];(t[a]||=Object.create(null))[r]=s}}return[a,r,i]}_getNameAndPrefix(e,t){const a=e.indexOf(":");return-1===a?[e,null]:[e.substring(a+1),t?"":e.substring(0,a)]}onBeginElement(e,t,a){const[r,i,n]=this._mkAttributes(t,e),[s,o]=this._getNameAndPrefix(e,this._builder.isNsAgnostic()),c=this._builder.build({nsPrefix:o,name:s,attributes:n,namespace:r,prefixes:i});c[Is]=this._globalData;if(a){c[hs]();this._current[$s](c)&&c[ao](this._ids);c[ts](this._builder)}else{this._stack.push(this._current);this._current=c}}onEndElement(e){const t=this._current;if(t[Bs]()&&"string"==typeof t[ss]){const e=new XFAParser;e._globalData=this._globalData;const a=e.parse(t[ss]);t[ss]=null;t[$s](a)}t[hs]();this._current=this._stack.pop();this._current[$s](t)&&t[ao](this._ids);t[ts](this._builder)}onError(e){this._errorCode=e}}class XFAFactory{constructor(e){try{this.root=(new XFAParser).parse(XFAFactory._createDocument(e));const t=new Binder(this.root);this.form=t.bind();this.dataHandler=new DataHandler(this.root,t.getData());this.form[Is].template=this.form}catch(e){warn(`XFA - an error occurred during parsing and binding: ${e}`)}}isValid(){return this.root&&this.form}_createPagesHelper(){const e=this.form[oo]();return new Promise(((t,a)=>{const nextIteration=()=>{try{const a=e.next();a.done?t(a.value):setTimeout(nextIteration,0)}catch(e){a(e)}};setTimeout(nextIteration,0)}))}async _createPages(){try{this.pages=await this._createPagesHelper();this.dims=this.pages.children.map((e=>{const{width:t,height:a}=e.attributes.style;return[0,0,parseInt(t),parseInt(a)]}))}catch(e){warn(`XFA - an error occurred during layout: ${e}`)}}getBoundingBox(e){return this.dims[e]}async getNumPages(){this.pages||await this._createPages();return this.dims.length}setImages(e){this.form[Is].images=e}setFonts(e){this.form[Is].fontFinder=new FontFinder(e);const t=[];for(let e of this.form[Is].usedTypefaces){e=stripQuotes(e);this.form[Is].fontFinder.find(e)||t.push(e)}return t.length>0?t:null}appendFonts(e,t){this.form[Is].fontFinder.add(e,t)}async getPages(){this.pages||await this._createPages();const e=this.pages;this.pages=null;return e}serializeData(e){return this.dataHandler.serialize(e)}static _createDocument(e){return e["/xdp:xdp"]?Object.values(e).join(""):e["xdp:xdp"]}static getRichTextAsHtml(e){if(!e||"string"!=typeof e)return null;try{let t=new XFAParser(XhtmlNamespace,!0).parse(e);if(!["body","xhtml"].includes(t[Ws])){const e=XhtmlNamespace.body({});e[Qn](t);t=e}const a=t[co]();if(!a.success)return null;const{html:r}=a,{attributes:i}=r;if(i){i.class&&(i.class=i.class.filter((e=>!e.startsWith("xfa"))));i.dir="auto"}return{html:r,str:t[so]()}}catch(e){warn(`XFA - an error occurred during parsing of rich text: ${e}`)}return null}}class AnnotationFactory{static createGlobals(e){return Promise.all([e.ensureCatalog("acroForm"),e.ensureDoc("xfaDatasets"),e.ensureCatalog("structTreeRoot"),e.ensureCatalog("baseUrl"),e.ensureCatalog("attachments"),e.ensureCatalog("globalColorSpaceCache")]).then((([t,a,r,i,n,s])=>({pdfManager:e,acroForm:t instanceof Dict?t:Dict.empty,xfaDatasets:a,structTreeRoot:r,baseUrl:i,attachments:n,globalColorSpaceCache:s})),(e=>{warn(`createGlobals: "${e}".`);return null}))}static async create(e,t,a,r,i,n,s){const o=i?await this._getPageIndex(e,t,a.pdfManager):null;return a.pdfManager.ensure(this,"_create",[e,t,a,r,i,n,o,s])}static _create(e,t,a,r,i=!1,n=null,s=null,o=null){const c=e.fetchIfRef(t);if(!(c instanceof Dict))return;const{acroForm:l,pdfManager:h}=a,u=t instanceof Ref?t.toString():`annot_${r.createObjId()}`;let d=c.get("Subtype");d=d instanceof Name?d.name:null;const f={xref:e,ref:t,dict:c,subtype:d,id:u,annotationGlobals:a,collectFields:i,orphanFields:n,needAppearances:!i&&!0===l.get("NeedAppearances"),pageIndex:s,evaluatorOptions:h.evaluatorOptions,pageRef:o};switch(d){case"Link":return new LinkAnnotation(f);case"Text":return new TextAnnotation(f);case"Widget":let e=getInheritableProperty({dict:c,key:"FT"});e=e instanceof Name?e.name:null;switch(e){case"Tx":return new TextWidgetAnnotation(f);case"Btn":return new ButtonWidgetAnnotation(f);case"Ch":return new ChoiceWidgetAnnotation(f);case"Sig":return new SignatureWidgetAnnotation(f)}warn(`Unimplemented widget field type "${e}", falling back to base field type.`);return new WidgetAnnotation(f);case"Popup":return new PopupAnnotation(f);case"FreeText":return new FreeTextAnnotation(f);case"Line":return new LineAnnotation(f);case"Square":return new SquareAnnotation(f);case"Circle":return new CircleAnnotation(f);case"PolyLine":return new PolylineAnnotation(f);case"Polygon":return new PolygonAnnotation(f);case"Caret":return new CaretAnnotation(f);case"Ink":return new InkAnnotation(f);case"Highlight":return new HighlightAnnotation(f);case"Underline":return new UnderlineAnnotation(f);case"Squiggly":return new SquigglyAnnotation(f);case"StrikeOut":return new StrikeOutAnnotation(f);case"Stamp":return new StampAnnotation(f);case"FileAttachment":return new FileAttachmentAnnotation(f);default:i||warn(d?`Unimplemented annotation type "${d}", falling back to base annotation.`:"Annotation is missing the required /Subtype.");return new Annotation(f)}}static async _getPageIndex(e,t,a){try{const r=await e.fetchIfRefAsync(t);if(!(r instanceof Dict))return-1;const i=r.getRaw("P");if(i instanceof Ref)try{return await a.ensureCatalog("getPageIndex",[i])}catch(e){info(`_getPageIndex -- not a valid page reference: "${e}".`)}if(r.has("Kids"))return-1;const n=await a.ensureDoc("numPages");for(let e=0;ee/255))}function getQuadPoints(e,t){const a=e.getArray("QuadPoints");if(!isNumberArray(a,null)||0===a.length||a.length%8>0)return null;const r=new Float32Array(a.length);for(let e=0,i=a.length;et[2]||gt[3]))return null;r.set([d,p,f,p,d,g,f,g],e)}return r}function getTransformMatrix(e,t,a){const r=new Float32Array([1/0,1/0,-1/0,-1/0]);Util.axialAlignedBoundingBox(t,a,r);const[i,n,s,o]=r;if(i===s||n===o)return[1,0,0,1,e[0],e[1]];const c=(e[2]-e[0])/(s-i),l=(e[3]-e[1])/(o-n);return[c,0,0,l,e[0]-i*c,e[1]-n*l]}class Annotation{constructor(e){const{dict:t,xref:a,annotationGlobals:r,ref:i,orphanFields:n}=e,s=n?.get(i);s&&t.set("Parent",s);this.setTitle(t.get("T"));this.setContents(t.get("Contents"));this.setModificationDate(t.get("M"));this.setFlags(t.get("F"));this.setRectangle(t.getArray("Rect"));this.setColor(t.getArray("C"));this.setBorderStyle(t);this.setAppearance(t);this.setOptionalContent(t);const o=t.get("MK");this.setBorderAndBackgroundColors(o);this.setRotation(o,t);this.ref=e.ref instanceof Ref?e.ref:null;this._streams=[];this.appearance&&this._streams.push(this.appearance);const c=!!(this.flags&ee),l=!!(this.flags&te);this.data={annotationFlags:this.flags,borderStyle:this.borderStyle,color:this.color,backgroundColor:this.backgroundColor,borderColor:this.borderColor,rotation:this.rotation,contentsObj:this._contents,hasAppearance:!!this.appearance,id:e.id,modificationDate:this.modificationDate,rect:this.rectangle,subtype:e.subtype,hasOwnCanvas:!1,noRotate:!!(this.flags&Z),noHTML:c&&l,isEditable:!1,structParent:-1};if(r.structTreeRoot){let a=t.get("StructParent");this.data.structParent=a=Number.isInteger(a)&&a>=0?a:-1;r.structTreeRoot.addAnnotationIdToPage(e.pageRef,a)}if(e.collectFields){const r=t.get("Kids");if(Array.isArray(r)){const e=[];for(const t of r)t instanceof Ref&&e.push(t.toString());0!==e.length&&(this.data.kidIds=e)}this.data.actions=collectActions(a,t,ye);this.data.fieldName=this._constructFieldName(t);this.data.pageIndex=e.pageIndex}const h=t.get("IT");h instanceof Name&&(this.data.it=h.name);this._isOffscreenCanvasSupported=e.evaluatorOptions.isOffscreenCanvasSupported;this._fallbackFontDict=null;this._needAppearances=!1}_hasFlag(e,t){return!!(e&t)}_buildFlags(e,t){let{flags:a}=this;if(void 0===e){if(void 0===t)return;return t?a&~Y:a&~J|Y}if(e){a|=Y;return t?a&~Q|J:a&~J|Q}a&=~(J|Q);return t?a&~Y:a|Y}_isViewable(e){return!this._hasFlag(e,K)&&!this._hasFlag(e,Q)}_isPrintable(e){return this._hasFlag(e,Y)&&!this._hasFlag(e,J)&&!this._hasFlag(e,K)}mustBeViewed(e,t){const a=e?.get(this.data.id)?.noView;return void 0!==a?!a:this.viewable&&!this._hasFlag(this.flags,J)}mustBePrinted(e){const t=e?.get(this.data.id)?.noPrint;return void 0!==t?!t:this.printable}mustBeViewedWhenEditing(e,t=null){return e?!this.data.isEditable:!t?.has(this.data.id)}get viewable(){return null!==this.data.quadPoints&&(0===this.flags||this._isViewable(this.flags))}get printable(){return null!==this.data.quadPoints&&(0!==this.flags&&this._isPrintable(this.flags))}_parseStringHelper(e){const t="string"==typeof e?stringToPDFString(e):"";return{str:t,dir:t&&"rtl"===bidi(t).dir?"rtl":"ltr"}}setDefaultAppearance(e){const{dict:t,annotationGlobals:a}=e,r=getInheritableProperty({dict:t,key:"DA"})||a.acroForm.get("DA");this._defaultAppearance="string"==typeof r?r:"";this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance)}setTitle(e){this._title=this._parseStringHelper(e)}setContents(e){this._contents=this._parseStringHelper(e)}setModificationDate(e){this.modificationDate="string"==typeof e?e:null}setFlags(e){this.flags=Number.isInteger(e)&&e>0?e:0;this.flags&K&&"Annotation"!==this.constructor.name&&(this.flags^=K)}hasFlag(e){return this._hasFlag(this.flags,e)}setRectangle(e){this.rectangle=lookupNormalRect(e,[0,0,0,0])}setColor(e){this.color=getRgbColor(e)}setLineEndings(e){this.lineEndings=["None","None"];if(Array.isArray(e)&&2===e.length)for(let t=0;t<2;t++){const a=e[t];if(a instanceof Name)switch(a.name){case"None":continue;case"Square":case"Circle":case"Diamond":case"OpenArrow":case"ClosedArrow":case"Butt":case"ROpenArrow":case"RClosedArrow":case"Slash":this.lineEndings[t]=a.name;continue}warn(`Ignoring invalid lineEnding: ${a}`)}}setRotation(e,t){this.rotation=0;let a=e instanceof Dict?e.get("R")||0:t.get("Rotate")||0;if(Number.isInteger(a)&&0!==a){a%=360;a<0&&(a+=360);a%90==0&&(this.rotation=a)}}setBorderAndBackgroundColors(e){if(e instanceof Dict){this.borderColor=getRgbColor(e.getArray("BC"),null);this.backgroundColor=getRgbColor(e.getArray("BG"),null)}else this.borderColor=this.backgroundColor=null}setBorderStyle(e){this.borderStyle=new AnnotationBorderStyle;if(e instanceof Dict)if(e.has("BS")){const t=e.get("BS");if(t instanceof Dict){const e=t.get("Type");if(!e||isName(e,"Border")){this.borderStyle.setWidth(t.get("W"),this.rectangle);this.borderStyle.setStyle(t.get("S"));this.borderStyle.setDashArray(t.getArray("D"))}}}else if(e.has("Border")){const t=e.getArray("Border");if(Array.isArray(t)&&t.length>=3){this.borderStyle.setHorizontalCornerRadius(t[0]);this.borderStyle.setVerticalCornerRadius(t[1]);this.borderStyle.setWidth(t[2],this.rectangle);4===t.length&&this.borderStyle.setDashArray(t[3],!0)}}else this.borderStyle.setWidth(0)}setAppearance(e){this.appearance=null;const t=e.get("AP");if(!(t instanceof Dict))return;const a=t.get("N");if(a instanceof BaseStream){this.appearance=a;return}if(!(a instanceof Dict))return;const r=e.get("AS");if(!(r instanceof Name&&a.has(r.name)))return;const i=a.get(r.name);i instanceof BaseStream&&(this.appearance=i)}setOptionalContent(e){this.oc=null;const t=e.get("OC");t instanceof Name?warn("setOptionalContent: Support for /Name-entry is not implemented."):t instanceof Dict&&(this.oc=t)}loadResources(e,t){return t.dict.getAsync("Resources").then((t=>{if(!t)return;return new ObjectLoader(t,e,t.xref).load().then((()=>t))}))}async getOperatorList(e,t,a,r){const{hasOwnCanvas:i,id:n,rect:o}=this.data;let c=this.appearance;const l=!!(i&&a&s);if(l&&(0===this.width||0===this.height)){this.data.hasOwnCanvas=!1;return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}if(!c){if(!l)return{opList:new OperatorList,separateForm:!1,separateCanvas:!1};c=new StringStream("");c.dict=new Dict}const h=c.dict,u=await this.loadResources(Ia,c),d=lookupRect(h.getArray("BBox"),[0,0,1,1]),f=lookupMatrix(h.getArray("Matrix"),Fa),g=getTransformMatrix(o,d,f),p=new OperatorList;let m;this.oc&&(m=await e.parseMarkedContentProps(this.oc,null));void 0!==m&&p.addOp(_t,["OC",m]);p.addOp($t,[n,o,g,f,l]);await e.getOperatorList({stream:c,task:t,resources:u,operatorList:p,fallbackFontDict:this._fallbackFontDict});p.addOp(Gt,[]);void 0!==m&&p.addOp(jt,[]);this.reset();return{opList:p,separateForm:!1,separateCanvas:l}}async save(e,t,a,r){return null}get hasTextContent(){return!1}async extractTextContent(e,t,a){if(!this.appearance)return;const r=await this.loadResources(Oa,this.appearance),i=[],n=[];let s=null;const o={desiredSize:Math.Infinity,ready:!0,enqueue(e,t){for(const t of e.items)if(void 0!==t.str){s||=t.transform.slice(-2);n.push(t.str);if(t.hasEOL){i.push(n.join("").trimEnd());n.length=0}}}};await e.getTextContent({stream:this.appearance,task:t,resources:r,includeMarkedContent:!0,keepWhiteSpace:!0,sink:o,viewBox:a});this.reset();n.length&&i.push(n.join("").trimEnd());if(i.length>1||i[0]){const e=this.appearance.dict,t=lookupRect(e.getArray("BBox"),null),a=lookupMatrix(e.getArray("Matrix"),null);this.data.textPosition=this._transformPoint(s,t,a);this.data.textContent=i}}_transformPoint(e,t,a){const{rect:r}=this.data;t||=[0,0,1,1];a||=[1,0,0,1,0,0];const i=getTransformMatrix(r,t,a);i[4]-=r[0];i[5]-=r[1];const n=e.slice();Util.applyTransform(n,i);Util.applyTransform(n,a);return n}getFieldObject(){return this.data.kidIds?{id:this.data.id,actions:this.data.actions,name:this.data.fieldName,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,type:"",kidIds:this.data.kidIds,page:this.data.pageIndex,rotation:this.rotation}:null}reset(){for(const e of this._streams)e.reset()}_constructFieldName(e){if(!e.has("T")&&!e.has("Parent")){warn("Unknown field name, falling back to empty field name.");return""}if(!e.has("Parent"))return stringToPDFString(e.get("T"));const t=[];e.has("T")&&t.unshift(stringToPDFString(e.get("T")));let a=e;const r=new RefSet;e.objId&&r.put(e.objId);for(;a.has("Parent");){a=a.get("Parent");if(!(a instanceof Dict)||a.objId&&r.has(a.objId))break;a.objId&&r.put(a.objId);a.has("T")&&t.unshift(stringToPDFString(a.get("T")))}return t.join(".")}get width(){return this.data.rect[2]-this.data.rect[0]}get height(){return this.data.rect[3]-this.data.rect[1]}}class AnnotationBorderStyle{constructor(){this.width=1;this.rawWidth=1;this.style=fe;this.dashArray=[3];this.horizontalCornerRadius=0;this.verticalCornerRadius=0}setWidth(e,t=[0,0,0,0]){if(e instanceof Name)this.width=0;else if("number"==typeof e){if(e>0){this.rawWidth=e;const a=(t[2]-t[0])/2,r=(t[3]-t[1])/2;if(a>0&&r>0&&(e>a||e>r)){warn(`AnnotationBorderStyle.setWidth - ignoring width: ${e}`);e=1}}this.width=e}}setStyle(e){if(e instanceof Name)switch(e.name){case"S":this.style=fe;break;case"D":this.style=ge;break;case"B":this.style=pe;break;case"I":this.style=me;break;case"U":this.style=be}}setDashArray(e,t=!1){if(Array.isArray(e)){let a=!0,r=!0;for(const t of e){if(!(+t>=0)){a=!1;break}t>0&&(r=!1)}if(0===e.length||a&&!r){this.dashArray=e;t&&this.setStyle(Name.get("D"))}else this.width=0}else e&&(this.width=0)}setHorizontalCornerRadius(e){Number.isInteger(e)&&(this.horizontalCornerRadius=e)}setVerticalCornerRadius(e){Number.isInteger(e)&&(this.verticalCornerRadius=e)}}class MarkupAnnotation extends Annotation{constructor(e){super(e);const{dict:t}=e;if(t.has("IRT")){const e=t.getRaw("IRT");this.data.inReplyTo=e instanceof Ref?e.toString():null;const a=t.get("RT");this.data.replyType=a instanceof Name?a.name:V}let a=null;if(this.data.replyType===G){const e=t.get("IRT");this.setTitle(e.get("T"));this.data.titleObj=this._title;this.setContents(e.get("Contents"));this.data.contentsObj=this._contents;if(e.has("CreationDate")){this.setCreationDate(e.get("CreationDate"));this.data.creationDate=this.creationDate}else this.data.creationDate=null;if(e.has("M")){this.setModificationDate(e.get("M"));this.data.modificationDate=this.modificationDate}else this.data.modificationDate=null;a=e.getRaw("Popup");if(e.has("C")){this.setColor(e.getArray("C"));this.data.color=this.color}else this.data.color=null}else{this.data.titleObj=this._title;this.setCreationDate(t.get("CreationDate"));this.data.creationDate=this.creationDate;a=t.getRaw("Popup");t.has("C")||(this.data.color=null)}this.data.popupRef=a instanceof Ref?a.toString():null;t.has("RC")&&(this.data.richText=XFAFactory.getRichTextAsHtml(t.get("RC")))}setCreationDate(e){this.creationDate="string"==typeof e?e:null}_setDefaultAppearance({xref:e,extra:t,strokeColor:a,fillColor:r,blendMode:i,strokeAlpha:n,fillAlpha:s,pointsCallback:o}){const c=this.data.rect=[1/0,1/0,-1/0,-1/0],l=["q"];t&&l.push(t);a&&l.push(`${a[0]} ${a[1]} ${a[2]} RG`);r&&l.push(`${r[0]} ${r[1]} ${r[2]} rg`);const h=this.data.quadPoints||Float32Array.from([this.rectangle[0],this.rectangle[3],this.rectangle[2],this.rectangle[3],this.rectangle[0],this.rectangle[1],this.rectangle[2],this.rectangle[1]]);for(let e=0,t=h.length;e"string"==typeof e)).map((e=>stringToPDFString(e))):e instanceof Name?stringToPDFString(e.name):"string"==typeof e?stringToPDFString(e):null}hasFieldFlag(e){return!!(this.data.fieldFlags&e)}_isViewable(e){return!0}mustBeViewed(e,t){return t?this.viewable:super.mustBeViewed(e,t)&&!this._hasFlag(this.flags,Q)}getRotationMatrix(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);return 0===t?Fa:getRotationMatrix(t,this.width,this.height)}getBorderAndBackgroundAppearances(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);if(!this.backgroundColor&&!this.borderColor)return"";const a=0===t||180===t?`0 0 ${this.width} ${this.height} re`:`0 0 ${this.height} ${this.width} re`;let r="";this.backgroundColor&&(r=`${getPdfColor(this.backgroundColor,!0)} ${a} f `);if(this.borderColor){r+=`${this.borderStyle.width||1} w ${getPdfColor(this.borderColor,!1)} ${a} S `}return r}async getOperatorList(e,t,a,r){if(a&l&&!(this instanceof SignatureWidgetAnnotation)&&!this.data.noHTML&&!this.data.hasOwnCanvas)return{opList:new OperatorList,separateForm:!0,separateCanvas:!1};if(!this._hasText)return super.getOperatorList(e,t,a,r);const i=await this._getAppearance(e,t,a,r);if(this.appearance&&null===i)return super.getOperatorList(e,t,a,r);const n=new OperatorList;if(!this._defaultAppearance||null===i)return{opList:n,separateForm:!1,separateCanvas:!1};const o=!!(this.data.hasOwnCanvas&&a&s),c=[0,0,this.width,this.height],h=getTransformMatrix(this.data.rect,c,[1,0,0,1,0,0]);let u;this.oc&&(u=await e.parseMarkedContentProps(this.oc,null));void 0!==u&&n.addOp(_t,["OC",u]);n.addOp($t,[this.data.id,this.data.rect,h,this.getRotationMatrix(r),o]);const d=new StringStream(i);await e.getOperatorList({stream:d,task:t,resources:this._fieldResources.mergedResources,operatorList:n});n.addOp(Gt,[]);void 0!==u&&n.addOp(jt,[]);return{opList:n,separateForm:!1,separateCanvas:o}}_getMKDict(e){const t=new Dict(null);e&&t.set("R",e);this.borderColor&&t.set("BC",getPdfColorArray(this.borderColor));this.backgroundColor&&t.set("BG",getPdfColorArray(this.backgroundColor));return t.size>0?t:null}amendSavedDict(e,t){}setValue(e,t,a,r){const{dict:i,ref:n}=function getParentToUpdate(e,t,a){const r=new RefSet,i=e,n={dict:null,ref:null};for(;e instanceof Dict&&!r.has(t);){r.put(t);if(e.has("T"))break;if(!((t=e.getRaw("Parent"))instanceof Ref))return n;e=a.fetch(t)}if(e instanceof Dict&&e!==i){n.dict=e;n.ref=t}return n}(e,this.ref,a);if(i){if(!r.has(n)){const e=i.clone();e.set("V",t);r.put(n,{data:e});return e}}else e.set("V",t);return null}async save(e,t,a,r){const i=a?.get(this.data.id),n=this._buildFlags(i?.noView,i?.noPrint);let s=i?.value,o=i?.rotation;if(s===this.data.fieldValue||void 0===s){if(!this._hasValueFromXFA&&void 0===o&&void 0===n)return;s||=this.data.fieldValue}if(void 0===o&&!this._hasValueFromXFA&&Array.isArray(s)&&Array.isArray(this.data.fieldValue)&&isArrayEqual(s,this.data.fieldValue)&&void 0===n)return;void 0===o&&(o=this.rotation);let l=null;if(!this._needAppearances){l=await this._getAppearance(e,t,c,a);if(null===l&&void 0===n)return}let h=!1;if(l?.needAppearances){h=!0;l=null}const{xref:u}=e,d=u.fetchIfRef(this.ref);if(!(d instanceof Dict))return;const f=new Dict(u);for(const e of d.getKeys())"AP"!==e&&f.set(e,d.getRaw(e));if(void 0!==n){f.set("F",n);if(null===l&&!h){const e=d.getRaw("AP");e&&f.set("AP",e)}}const g={path:this.data.fieldName,value:s},p=this.setValue(f,Array.isArray(s)?s.map(stringToAsciiOrUTF16BE):stringToAsciiOrUTF16BE(s),u,r);this.amendSavedDict(a,p||f);const m=this._getMKDict(o);m&&f.set("MK",m);r.put(this.ref,{data:f,xfa:g,needAppearances:h});if(null!==l){const e=u.getNewTemporaryRef(),t=new Dict(u);f.set("AP",t);t.set("N",e);const i=this._getSaveFieldResources(u),n=new StringStream(l),s=n.dict=new Dict(u);s.set("Subtype",Name.get("Form"));s.set("Resources",i);s.set("BBox",[0,0,this.width,this.height]);const o=this.getRotationMatrix(a);o!==Fa&&s.set("Matrix",o);r.put(e,{data:n,xfa:null,needAppearances:!1})}f.set("M",`D:${getModificationDate()}`)}async _getAppearance(e,t,a,r){if(this.data.password)return null;const n=r?.get(this.data.id);let s,o;if(n){s=n.formattedValue||n.value;o=n.rotation}if(void 0===o&&void 0===s&&!this._needAppearances&&(!this._hasValueFromXFA||this.appearance))return null;const l=this.getBorderAndBackgroundAppearances(r);if(void 0===s){s=this.data.fieldValue;if(!s)return`/Tx BMC q ${l}Q EMC`}Array.isArray(s)&&1===s.length&&(s=s[0]);assert("string"==typeof s,"Expected `value` to be a string.");s=s.trimEnd();if(this.data.combo){const e=this.data.options.find((({exportValue:e})=>s===e));s=e?.displayValue||s}if(""===s)return`/Tx BMC q ${l}Q EMC`;void 0===o&&(o=this.rotation);let h,u=-1;if(this.data.multiLine){h=s.split(/\r\n?|\n/).map((e=>e.normalize("NFC")));u=h.length}else h=[s.replace(/\r\n?|\n/,"").normalize("NFC")];let{width:d,height:f}=this;90!==o&&270!==o||([d,f]=[f,d]);this._defaultAppearance||(this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance="/Helvetica 0 Tf 0 g"));let g,p,m,b=await WidgetAnnotation._getFontData(e,t,this.data.defaultAppearanceData,this._fieldResources.mergedResources);const y=[];let w=!1;for(const e of h){const t=b.encodeString(e);t.length>1&&(w=!0);y.push(t.join(""))}if(w&&a&c)return{needAppearances:!0};if(w&&this._isOffscreenCanvasSupported){const a=this.data.comb?"monospace":"sans-serif",r=new FakeUnicodeFont(e.xref,a),i=r.createFontResources(h.join("")),n=i.getRaw("Font");if(this._fieldResources.mergedResources.has("Font")){const e=this._fieldResources.mergedResources.get("Font");for(const t of n.getKeys())e.set(t,n.getRaw(t))}else this._fieldResources.mergedResources.set("Font",n);const o=r.fontName.name;b=await WidgetAnnotation._getFontData(e,t,{fontName:o,fontSize:0},i);for(let e=0,t=y.length;e2)return`/Tx BMC q ${l}BT `+g+` 1 0 0 1 ${numberToString(2)} ${numberToString(C)} Tm (${escapeString(y[0])}) Tj ET Q EMC`;return`/Tx BMC q ${l}BT `+g+` 1 0 0 1 0 0 Tm ${this._renderText(y[0],b,p,d,k,{shift:0},2,C)} ET Q EMC`}static async _getFontData(e,t,a,r){const i=new OperatorList,n={font:null,clone(){return this}},{fontName:s,fontSize:o}=a;await e.handleSetFont(r,[s&&Name.get(s),o],null,i,t,n,null);return n.font}_getTextWidth(e,t){return Math.sumPrecise(t.charsToGlyphs(e).map((e=>e.width)))/1e3}_computeFontSize(e,t,r,i,n){let{fontSize:s}=this.data.defaultAppearanceData,o=(s||12)*a,c=Math.round(e/o);if(!s){const roundWithTwoDigits=e=>Math.floor(100*e)/100;if(-1===n){const n=this._getTextWidth(r,i);s=roundWithTwoDigits(Math.min(e/a,t/n));c=1}else{const l=r.split(/\r\n?|\n/),h=[];for(const e of l){const t=i.encodeString(e).join(""),a=i.charsToGlyphs(t),r=i.getCharPositions(t);h.push({line:t,glyphs:a,positions:r})}const isTooBig=a=>{let r=0;for(const n of h){r+=this._splitLine(null,i,a,t,n).length*a;if(r>e)return!0}return!1};c=Math.max(c,n);for(;;){o=e/c;s=roundWithTwoDigits(o/a);if(!isTooBig(s))break;c++}}const{fontName:l,fontColor:h}=this.data.defaultAppearanceData;this._defaultAppearance=function createDefaultAppearance({fontSize:e,fontName:t,fontColor:a}){return`/${escapePDFName(t)} ${e} Tf ${getPdfColor(a,!0)}`}({fontSize:s,fontName:l,fontColor:h})}return[this._defaultAppearance,s,e/c]}_renderText(e,t,a,r,i,n,s,o){let c;if(1===i){c=(r-this._getTextWidth(e,t)*a)/2}else if(2===i){c=r-this._getTextWidth(e,t)*a-s}else c=s;const l=numberToString(c-n.shift);n.shift=c;return`${l} ${o=numberToString(o)} Td (${escapeString(e)}) Tj`}_getSaveFieldResources(e){const{localResources:t,appearanceResources:a,acroFormResources:r}=this._fieldResources,i=this.data.defaultAppearanceData?.fontName;if(!i)return t||Dict.empty;for(const e of[t,a])if(e instanceof Dict){const t=e.get("Font");if(t instanceof Dict&&t.has(i))return e}if(r instanceof Dict){const a=r.get("Font");if(a instanceof Dict&&a.has(i)){const r=new Dict(e);r.set(i,a.getRaw(i));const n=new Dict(e);n.set("Font",r);return Dict.merge({xref:e,dictArray:[n,t],mergeSubDicts:!0})}}return t||Dict.empty}getFieldObject(){return null}}class TextWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t}=e;if(t.has("PMD")){this.flags|=J;this.data.hidden=!0;warn("Barcodes are not supported")}this.data.hasOwnCanvas=this.data.readOnly&&!this.data.noHTML;this._hasText=!0;"string"!=typeof this.data.fieldValue&&(this.data.fieldValue="");let a=getInheritableProperty({dict:t,key:"Q"});(!Number.isInteger(a)||a<0||a>2)&&(a=null);this.data.textAlignment=a;let r=getInheritableProperty({dict:t,key:"MaxLen"});(!Number.isInteger(r)||r<0)&&(r=0);this.data.maxLen=r;this.data.multiLine=this.hasFieldFlag(ie);this.data.comb=this.hasFieldFlag(de)&&!this.data.multiLine&&!this.data.password&&!this.hasFieldFlag(le)&&0!==this.data.maxLen;this.data.doNotScroll=this.hasFieldFlag(ue)}get hasTextContent(){return!!this.appearance&&!this._needAppearances}_getCombAppearance(e,t,a,r,i,n,s,o,c,l,h){const u=i/this.data.maxLen,d=this.getBorderAndBackgroundAppearances(h),f=[],g=t.getCharPositions(a);for(const[e,t]of g)f.push(`(${escapeString(a.substring(e,t))}) Tj`);const p=f.join(` ${numberToString(u)} 0 Td `);return`/Tx BMC q ${d}BT `+e+` 1 0 0 1 ${numberToString(s)} ${numberToString(o+c)} Tm ${p} ET Q EMC`}_getMultilineAppearance(e,t,a,r,i,n,s,o,c,l,h,u){const d=[],f=i-2*o,g={shift:0};for(let e=0,n=t.length;er){c.push(e.substring(d,a));d=a;f=p;l=-1;u=-1}else{f+=p;l=a;h=i;u=t}else if(f+p>r)if(-1!==l){c.push(e.substring(d,h));d=h;t=u+1;l=-1;f=0}else{c.push(e.substring(d,a));d=a;f=p}else f+=p}dt?`\\${t}`:"\\s+"));new RegExp(`^\\s*${n}\\s*$`).test(this.data.fieldValue)&&(this.data.textContent=this.data.fieldValue.split("\n"))}getFieldObject(){return{id:this.data.id,value:this.data.fieldValue,defaultValue:this.data.defaultFieldValue||"",multiline:this.data.multiLine,password:this.data.password,charLimit:this.data.maxLen,comb:this.data.comb,editable:!this.data.readOnly,hidden:this.data.hidden,name:this.data.fieldName,rect:this.data.rect,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:"text"}}}class ButtonWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);this.checkedAppearance=null;this.uncheckedAppearance=null;const t=this.hasFieldFlag(se),a=this.hasFieldFlag(oe);this.data.checkBox=!t&&!a;this.data.radioButton=t&&!a;this.data.pushButton=a;this.data.isTooltipOnly=!1;if(this.data.checkBox)this._processCheckBox(e);else if(this.data.radioButton)this._processRadioButton(e);else if(this.data.pushButton){this.data.hasOwnCanvas=!0;this.data.noHTML=!1;this._processPushButton(e)}else warn("Invalid field flags for button widget annotation")}async getOperatorList(e,t,a,r){if(this.data.pushButton)return super.getOperatorList(e,t,a,!1,r);let i=null,n=null;if(r){const e=r.get(this.data.id);i=e?e.value:null;n=e?e.rotation:null}if(null===i&&this.appearance)return super.getOperatorList(e,t,a,r);null==i&&(i=this.data.checkBox?this.data.fieldValue===this.data.exportValue:this.data.fieldValue===this.data.buttonValue);const s=i?this.checkedAppearance:this.uncheckedAppearance;if(s){const i=this.appearance,o=lookupMatrix(s.dict.getArray("Matrix"),Fa);n&&s.dict.set("Matrix",this.getRotationMatrix(r));this.appearance=s;const c=super.getOperatorList(e,t,a,r);this.appearance=i;s.dict.set("Matrix",o);return c}return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}async save(e,t,a,r){this.data.checkBox?this._saveCheckbox(e,t,a,r):this.data.radioButton&&this._saveRadioButton(e,t,a,r)}async _saveCheckbox(e,t,a,r){if(!a)return;const i=a.get(this.data.id),n=this._buildFlags(i?.noView,i?.noPrint);let s=i?.rotation,o=i?.value;if(void 0===s&&void 0===n){if(void 0===o)return;if(this.data.fieldValue===this.data.exportValue===o)return}let c=e.xref.fetchIfRef(this.ref);if(!(c instanceof Dict))return;c=c.clone();void 0===s&&(s=this.rotation);void 0===o&&(o=this.data.fieldValue===this.data.exportValue);const l={path:this.data.fieldName,value:o?this.data.exportValue:""},h=Name.get(o?this.data.exportValue:"Off");this.setValue(c,h,e.xref,r);c.set("AS",h);c.set("M",`D:${getModificationDate()}`);void 0!==n&&c.set("F",n);const u=this._getMKDict(s);u&&c.set("MK",u);r.put(this.ref,{data:c,xfa:l,needAppearances:!1})}async _saveRadioButton(e,t,a,r){if(!a)return;const i=a.get(this.data.id),n=this._buildFlags(i?.noView,i?.noPrint);let s=i?.rotation,o=i?.value;if(void 0===s&&void 0===n){if(void 0===o)return;if(this.data.fieldValue===this.data.buttonValue===o)return}let c=e.xref.fetchIfRef(this.ref);if(!(c instanceof Dict))return;c=c.clone();void 0===o&&(o=this.data.fieldValue===this.data.buttonValue);void 0===s&&(s=this.rotation);const l={path:this.data.fieldName,value:o?this.data.buttonValue:""},h=Name.get(o?this.data.buttonValue:"Off");o&&this.setValue(c,h,e.xref,r);c.set("AS",h);c.set("M",`D:${getModificationDate()}`);void 0!==n&&c.set("F",n);const u=this._getMKDict(s);u&&c.set("MK",u);r.put(this.ref,{data:c,xfa:l,needAppearances:!1})}_getDefaultCheckedAppearance(e,t){const{width:a,height:r}=this,i=[0,0,a,r],n=.8*Math.min(a,r);let s,o;if("check"===t){s={width:.755*n,height:.705*n};o="3"}else if("disc"===t){s={width:.791*n,height:.705*n};o="l"}else unreachable(`_getDefaultCheckedAppearance - unsupported type: ${t}`);const c=`q BT /PdfJsZaDb ${n} Tf 0 g ${numberToString((a-s.width)/2)} ${numberToString((r-s.height)/2)} Td (${o}) Tj ET Q`,l=new Dict(e.xref);l.set("FormType",1);l.set("Subtype",Name.get("Form"));l.set("Type",Name.get("XObject"));l.set("BBox",i);l.set("Matrix",[1,0,0,1,0,0]);l.set("Length",c.length);const h=new Dict(e.xref),u=new Dict(e.xref);u.set("PdfJsZaDb",this.fallbackFontDict);h.set("Font",u);l.set("Resources",h);this.checkedAppearance=new StringStream(c);this.checkedAppearance.dict=l;this._streams.push(this.checkedAppearance)}_processCheckBox(e){const t=e.dict.get("AP");if(!(t instanceof Dict))return;const a=t.get("N");if(!(a instanceof Dict))return;const r=this._decodeFormValue(e.dict.get("AS"));"string"==typeof r&&(this.data.fieldValue=r);const i=null!==this.data.fieldValue&&"Off"!==this.data.fieldValue?this.data.fieldValue:"Yes",n=a.getKeys();if(0===n.length)n.push("Off",i);else if(1===n.length)"Off"===n[0]?n.push(i):n.unshift("Off");else if(n.includes(i)){n.length=0;n.push("Off",i)}else{const e=n.find((e=>"Off"!==e));n.length=0;n.push("Off",e)}n.includes(this.data.fieldValue)||(this.data.fieldValue="Off");this.data.exportValue=n[1];const s=a.get(this.data.exportValue);this.checkedAppearance=s instanceof BaseStream?s:null;const o=a.get("Off");this.uncheckedAppearance=o instanceof BaseStream?o:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"check");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processRadioButton(e){this.data.buttonValue=null;const t=e.dict.get("Parent");if(t instanceof Dict){this.parent=e.dict.getRaw("Parent");const a=t.get("V");a instanceof Name&&(this.data.fieldValue=this._decodeFormValue(a))}const a=e.dict.get("AP");if(!(a instanceof Dict))return;const r=a.get("N");if(!(r instanceof Dict))return;for(const e of r.getKeys())if("Off"!==e){this.data.buttonValue=this._decodeFormValue(e);break}const i=r.get(this.data.buttonValue);this.checkedAppearance=i instanceof BaseStream?i:null;const n=r.get("Off");this.uncheckedAppearance=n instanceof BaseStream?n:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"disc");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processPushButton(e){const{dict:t,annotationGlobals:a}=e;if(t.has("A")||t.has("AA")||this.data.alternativeText){this.data.isTooltipOnly=!t.has("A")&&!t.has("AA");Catalog.parseDestDictionary({destDict:t,resultObj:this.data,docBaseUrl:a.baseUrl,docAttachments:a.attachments})}else warn("Push buttons without action dictionaries are not supported")}getFieldObject(){let e,t="button";if(this.data.checkBox){t="checkbox";e=this.data.exportValue}else if(this.data.radioButton){t="radiobutton";e=this.data.buttonValue}return{id:this.data.id,value:this.data.fieldValue||"Off",defaultValue:this.data.defaultFieldValue,exportValues:e,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,hidden:this.data.hidden,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:t}}get fallbackFontDict(){const e=new Dict;e.set("BaseFont",Name.get("ZapfDingbats"));e.set("Type",Name.get("FallbackType"));e.set("Subtype",Name.get("FallbackType"));e.set("Encoding",Name.get("ZapfDingbatsEncoding"));return shadow(this,"fallbackFontDict",e)}}class ChoiceWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.indices=t.getArray("I");this.hasIndices=Array.isArray(this.indices)&&this.indices.length>0;this.data.options=[];const r=getInheritableProperty({dict:t,key:"Opt"});if(Array.isArray(r))for(let e=0,t=r.length;e=0&&t0&&(this.data.options=this.data.fieldValue.map((e=>({exportValue:e,displayValue:e}))));this.data.combo=this.hasFieldFlag(ce);this.data.multiSelect=this.hasFieldFlag(he);this._hasText=!0}getFieldObject(){const e=this.data.combo?"combobox":"listbox",t=this.data.fieldValue.length>0?this.data.fieldValue[0]:null;return{id:this.data.id,value:t,defaultValue:this.data.defaultFieldValue,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,numItems:this.data.fieldValue.length,multipleSelection:this.data.multiSelect,hidden:this.data.hidden,actions:this.data.actions,items:this.data.options,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:e}}amendSavedDict(e,t){if(!this.hasIndices)return;let a=e?.get(this.data.id)?.value;Array.isArray(a)||(a=[a]);const r=[],{options:i}=this.data;for(let e=0,t=0,n=i.length;ea){a=r;t=e}}[f,g]=this._computeFontSize(e,c-4,t,d,-1)}const p=g*a,m=(p-g)/2,b=Math.floor(l/p);let y=0;if(u.length>0){const e=Math.min(...u),t=Math.max(...u);y=Math.max(0,t-b+1);y>e&&(y=e)}const w=Math.min(y+b+1,h),S=["/Tx BMC q",`1 1 ${c} ${l} re W n`];if(u.length){S.push("0.600006 0.756866 0.854904 rg");for(const e of u)y<=e&&ee.trimEnd()));const{coords:e,bbox:t,matrix:r}=FakeUnicodeFont.getFirstPositionInfo(this.rectangle,this.rotation,a);this.data.textPosition=this._transformPoint(e,t,r)}if(this._isOffscreenCanvasSupported){const i=e.dict.get("CA"),n=new FakeUnicodeFont(r,"sans-serif");this.appearance=n.createAppearance(this._contents.str,this.rectangle,this.rotation,a,t,i);this._streams.push(this.appearance)}else warn("FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly.")}}get hasTextContent(){return this._hasAppearance}static createNewDict(e,t,{apRef:a,ap:r}){const{color:i,fontSize:n,oldAnnotation:s,rect:o,rotation:c,user:l,value:h}=e,u=s||new Dict(t);u.set("Type",Name.get("Annot"));u.set("Subtype",Name.get("FreeText"));if(s){u.set("M",`D:${getModificationDate()}`);u.delete("RC")}else u.set("CreationDate",`D:${getModificationDate()}`);u.set("Rect",o);const d=`/Helv ${n} Tf ${getPdfColor(i,!0)}`;u.set("DA",d);u.set("Contents",stringToAsciiOrUTF16BE(h));u.set("F",4);u.set("Border",[0,0,0]);u.set("Rotate",c);l&&u.set("T",stringToAsciiOrUTF16BE(l));if(a||r){const e=new Dict(t);u.set("AP",e);a?e.set("N",a):e.set("N",r)}return u}static async createNewAppearanceStream(e,t,r){const{baseFontRef:i,evaluator:n,task:s}=r,{color:o,fontSize:c,rect:l,rotation:h,value:u}=e,d=new Dict(t),f=new Dict(t);if(i)f.set("Helv",i);else{const e=new Dict(t);e.set("BaseFont",Name.get("Helvetica"));e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("Type1"));e.set("Encoding",Name.get("WinAnsiEncoding"));f.set("Helv",e)}d.set("Font",f);const g=await WidgetAnnotation._getFontData(n,s,{fontName:"Helv",fontSize:c},d),[p,m,b,y]=l;let w=b-p,S=y-m;h%180!=0&&([w,S]=[S,w]);const x=u.split("\n"),k=c/1e3;let C=-1/0;const v=[];for(let e of x){const t=g.encodeString(e);if(t.length>1)return null;e=t.join("");v.push(e);let a=0;const r=g.charsToGlyphs(e);for(const e of r)a+=e.width*k;C=Math.max(C,a)}let F=1;C>w&&(F=w/C);let O=1;const T=a*c,M=1*c,D=T*x.length;D>S&&(O=S/D);const R=c*Math.min(F,O);let N,E,L;switch(h){case 0:L=[1,0,0,1];E=[l[0],l[1],w,S];N=[l[0],l[3]-M];break;case 90:L=[0,1,-1,0];E=[l[1],-l[2],w,S];N=[l[1],-l[0]-M];break;case 180:L=[-1,0,0,-1];E=[-l[2],-l[3],w,S];N=[-l[2],-l[1]-M];break;case 270:L=[0,-1,1,0];E=[-l[3],l[0],w,S];N=[-l[3],l[2]-M]}const _=["q",`${L.join(" ")} 0 0 cm`,`${E.join(" ")} re W n`,"BT",`${getPdfColor(o,!0)}`,`0 Tc /Helv ${numberToString(R)} Tf`];_.push(`${N.join(" ")} Td (${escapeString(v[0])}) Tj`);const j=numberToString(T);for(let e=1,t=v.length;e{e.push(`${r[0]} ${r[1]} m`,`${r[2]} ${r[3]} l`,"S");return[t[0]-c,t[7]-c,t[2]+c,t[3]+c]}})}}}class SquareAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=D;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA"),i=getRgbColor(t.getArray("IC"),null),n=i?getPdfColorArray(i):null,s=n?r:null;if(0===this.borderStyle.width&&!n)return;this._setDefaultAppearance({xref:a,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:n,strokeAlpha:r,fillAlpha:s,pointsCallback:(e,t)=>{const a=t[4]+this.borderStyle.width/2,r=t[5]+this.borderStyle.width/2,i=t[6]-t[4]-this.borderStyle.width,s=t[3]-t[7]-this.borderStyle.width;e.push(`${a} ${r} ${i} ${s} re`);n?e.push("B"):e.push("S");return[t[0],t[7],t[2],t[3]]}})}}}class CircleAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=R;if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA"),i=getRgbColor(t.getArray("IC"),null),n=i?getPdfColorArray(i):null,s=n?r:null;if(0===this.borderStyle.width&&!n)return;const o=4/3*Math.tan(Math.PI/8);this._setDefaultAppearance({xref:a,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:n,strokeAlpha:r,fillAlpha:s,pointsCallback:(e,t)=>{const a=t[0]+this.borderStyle.width/2,r=t[1]-this.borderStyle.width/2,i=t[6]-this.borderStyle.width/2,s=t[7]+this.borderStyle.width/2,c=a+(i-a)/2,l=r+(s-r)/2,h=(i-a)/2*o,u=(s-r)/2*o;e.push(`${c} ${s} m`,`${c+h} ${s} ${i} ${l+u} ${i} ${l} c`,`${i} ${l-u} ${c+h} ${r} ${c} ${r} c`,`${c-h} ${r} ${a} ${l-u} ${a} ${l} c`,`${a} ${l+u} ${c-h} ${s} ${c} ${s} c`,"h");n?e.push("B"):e.push("S");return[t[0],t[7],t[2],t[3]]}})}}}class PolylineAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=E;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;this.data.vertices=null;if(!(this instanceof PolygonAnnotation)){this.setLineEndings(t.getArray("LE"));this.data.lineEndings=this.lineEndings}const r=t.getArray("Vertices");if(!isNumberArray(r,null))return;const i=this.data.vertices=Float32Array.from(r);if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA"),n=this.borderStyle.width||1,s=2*n,o=[1/0,1/0,-1/0,-1/0];for(let e=0,t=i.length;e{for(let t=0,a=i.length;t{for(const t of this.data.inkLists){for(let a=0,r=t.length;a{e.push(`${t[0]} ${t[1]} m`,`${t[2]} ${t[3]} l`,`${t[6]} ${t[7]} l`,`${t[4]} ${t[5]} l`,"f");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}static createNewDict(e,t,{apRef:a,ap:r}){const{color:i,oldAnnotation:n,opacity:s,rect:o,rotation:c,user:l,quadPoints:h}=e,u=n||new Dict(t);u.set("Type",Name.get("Annot"));u.set("Subtype",Name.get("Highlight"));u.set(n?"M":"CreationDate",`D:${getModificationDate()}`);u.set("CreationDate",`D:${getModificationDate()}`);u.set("Rect",o);u.set("F",4);u.set("Border",[0,0,0]);u.set("Rotate",c);u.set("QuadPoints",h);u.set("C",getPdfColorArray(i));u.set("CA",s);l&&u.set("T",stringToAsciiOrUTF16BE(l));if(a||r){const e=new Dict(t);u.set("AP",e);e.set("N",a||r)}return u}static async createNewAppearanceStream(e,t,a){const{color:r,rect:i,outlines:n,opacity:s}=e,o=[`${getPdfColor(r,!0)}`,"/R0 gs"],c=[];for(const e of n){c.length=0;c.push(`${numberToString(e[0])} ${numberToString(e[1])} m`);for(let t=2,a=e.length;t{e.push(`${t[4]} ${t[5]+1.3} m`,`${t[6]} ${t[7]+1.3} l`,"S");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}}class SquigglyAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=j;if(this.data.quadPoints=getQuadPoints(t,null)){if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA");this._setDefaultAppearance({xref:a,extra:"[] 0 d 1 w",strokeColor:e,strokeAlpha:r,pointsCallback:(e,t)=>{const a=(t[1]-t[5])/6;let r=a,i=t[4];const n=t[5],s=t[6];e.push(`${i} ${n+r} m`);do{i+=2;r=0===r?a:0;e.push(`${i} ${n+r} l`)}while(i{e.push((t[0]+t[4])/2+" "+(t[1]+t[5])/2+" m",(t[2]+t[6])/2+" "+(t[3]+t[7])/2+" l","S");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}}class StampAnnotation extends MarkupAnnotation{#ne=null;constructor(e){super(e);this.data.annotationType=X;this.data.hasOwnCanvas=this.data.noRotate;this.data.isEditable=!this.data.noHTML;this.data.noHTML=!1}mustBeViewedWhenEditing(e,t=null){if(e){if(!this.data.isEditable)return!0;this.#ne??=this.data.hasOwnCanvas;this.data.hasOwnCanvas=!0;return!0}if(null!==this.#ne){this.data.hasOwnCanvas=this.#ne;this.#ne=null}return!t?.has(this.data.id)}static async createImage(e,t){const{width:a,height:r}=e,i=new OffscreenCanvas(a,r),n=i.getContext("2d",{alpha:!0});n.drawImage(e,0,0);const s=n.getImageData(0,0,a,r).data,o=new Uint32Array(s.buffer),c=o.some(FeatureTest.isLittleEndian?e=>e>>>24!=255:e=>!!(255&~e));if(c){n.fillStyle="white";n.fillRect(0,0,a,r);n.drawImage(e,0,0)}const l=i.convertToBlob({type:"image/jpeg",quality:1}).then((e=>e.arrayBuffer())),h=Name.get("XObject"),u=Name.get("Image"),d=new Dict(t);d.set("Type",h);d.set("Subtype",u);d.set("BitsPerComponent",8);d.set("ColorSpace",Name.get("DeviceRGB"));d.set("Filter",Name.get("DCTDecode"));d.set("BBox",[0,0,a,r]);d.set("Width",a);d.set("Height",r);let f=null;if(c){const e=new Uint8Array(o.length);if(FeatureTest.isLittleEndian)for(let t=0,a=o.length;t>>24;else for(let t=0,a=o.length;t=0&&n<=1?n:null}}const pc={get r(){return shadow(this,"r",new Uint8Array([7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21]))},get k(){return shadow(this,"k",new Int32Array([-680876936,-389564586,606105819,-1044525330,-176418897,1200080426,-1473231341,-45705983,1770035416,-1958414417,-42063,-1990404162,1804603682,-40341101,-1502002290,1236535329,-165796510,-1069501632,643717713,-373897302,-701558691,38016083,-660478335,-405537848,568446438,-1019803690,-187363961,1163531501,-1444681467,-51403784,1735328473,-1926607734,-378558,-2022574463,1839030562,-35309556,-1530992060,1272893353,-155497632,-1094730640,681279174,-358537222,-722521979,76029189,-640364487,-421815835,530742520,-995338651,-198630844,1126891415,-1416354905,-57434055,1700485571,-1894986606,-1051523,-2054922799,1873313359,-30611744,-1560198380,1309151649,-145523070,-1120210379,718787259,-343485551]))}};function calculateMD5(e,t,a){let r=1732584193,i=-271733879,n=-1732584194,s=271733878;const o=a+72&-64,c=new Uint8Array(o);let l,h;for(l=0;l>5&255;c[l++]=a>>13&255;c[l++]=a>>21&255;c[l++]=a>>>29&255;l+=3;const d=new Int32Array(16),{k:f,r:g}=pc;for(l=0;l>>32-n)|0;a=r}r=r+a|0;i=i+o|0;n=n+u|0;s=s+p|0}return new Uint8Array([255&r,r>>8&255,r>>16&255,r>>>24&255,255&i,i>>8&255,i>>16&255,i>>>24&255,255&n,n>>8&255,n>>16&255,n>>>24&255,255&s,s>>8&255,s>>16&255,s>>>24&255])}function decodeString(e){try{return stringToUTF8String(e)}catch(t){warn(`UTF-8 decoding failed: "${t}".`);return e}}class DatasetXMLParser extends SimpleXMLParser{constructor(e){super(e);this.node=null}onEndElement(e){const t=super.onEndElement(e);if(t&&"xfa:datasets"===e){this.node=t;throw new Error("Aborting DatasetXMLParser.")}}}class DatasetReader{constructor(e){if(e.datasets)this.node=new SimpleXMLParser({hasAttributes:!0}).parseFromString(e.datasets).documentElement;else{const t=new DatasetXMLParser({hasAttributes:!0});try{t.parseFromString(e["xdp:xdp"])}catch{}this.node=t.node}}getValue(e){if(!this.node||!e)return"";const t=this.node.searchNode(parseXFAPath(e),0);if(!t)return"";const a=t.firstChild;return"value"===a?.nodeName?t.children.map((e=>decodeString(e.textContent))):decodeString(t.textContent)}}class Word64{constructor(e,t){this.high=0|e;this.low=0|t}and(e){this.high&=e.high;this.low&=e.low}xor(e){this.high^=e.high;this.low^=e.low}shiftRight(e){if(e>=32){this.low=this.high>>>e-32|0;this.high=0}else{this.low=this.low>>>e|this.high<<32-e;this.high=this.high>>>e|0}}rotateRight(e){let t,a;if(32&e){a=this.low;t=this.high}else{t=this.low;a=this.high}e&=31;this.low=t>>>e|a<<32-e;this.high=a>>>e|t<<32-e}not(){this.high=~this.high;this.low=~this.low}add(e){const t=(this.low>>>0)+(e.low>>>0);let a=(this.high>>>0)+(e.high>>>0);t>4294967295&&(a+=1);this.low=0|t;this.high=0|a}copyTo(e,t){e[t]=this.high>>>24&255;e[t+1]=this.high>>16&255;e[t+2]=this.high>>8&255;e[t+3]=255&this.high;e[t+4]=this.low>>>24&255;e[t+5]=this.low>>16&255;e[t+6]=this.low>>8&255;e[t+7]=255&this.low}assign(e){this.high=e.high;this.low=e.low}}const mc={get k(){return shadow(this,"k",[new Word64(1116352408,3609767458),new Word64(1899447441,602891725),new Word64(3049323471,3964484399),new Word64(3921009573,2173295548),new Word64(961987163,4081628472),new Word64(1508970993,3053834265),new Word64(2453635748,2937671579),new Word64(2870763221,3664609560),new Word64(3624381080,2734883394),new Word64(310598401,1164996542),new Word64(607225278,1323610764),new Word64(1426881987,3590304994),new Word64(1925078388,4068182383),new Word64(2162078206,991336113),new Word64(2614888103,633803317),new Word64(3248222580,3479774868),new Word64(3835390401,2666613458),new Word64(4022224774,944711139),new Word64(264347078,2341262773),new Word64(604807628,2007800933),new Word64(770255983,1495990901),new Word64(1249150122,1856431235),new Word64(1555081692,3175218132),new Word64(1996064986,2198950837),new Word64(2554220882,3999719339),new Word64(2821834349,766784016),new Word64(2952996808,2566594879),new Word64(3210313671,3203337956),new Word64(3336571891,1034457026),new Word64(3584528711,2466948901),new Word64(113926993,3758326383),new Word64(338241895,168717936),new Word64(666307205,1188179964),new Word64(773529912,1546045734),new Word64(1294757372,1522805485),new Word64(1396182291,2643833823),new Word64(1695183700,2343527390),new Word64(1986661051,1014477480),new Word64(2177026350,1206759142),new Word64(2456956037,344077627),new Word64(2730485921,1290863460),new Word64(2820302411,3158454273),new Word64(3259730800,3505952657),new Word64(3345764771,106217008),new Word64(3516065817,3606008344),new Word64(3600352804,1432725776),new Word64(4094571909,1467031594),new Word64(275423344,851169720),new Word64(430227734,3100823752),new Word64(506948616,1363258195),new Word64(659060556,3750685593),new Word64(883997877,3785050280),new Word64(958139571,3318307427),new Word64(1322822218,3812723403),new Word64(1537002063,2003034995),new Word64(1747873779,3602036899),new Word64(1955562222,1575990012),new Word64(2024104815,1125592928),new Word64(2227730452,2716904306),new Word64(2361852424,442776044),new Word64(2428436474,593698344),new Word64(2756734187,3733110249),new Word64(3204031479,2999351573),new Word64(3329325298,3815920427),new Word64(3391569614,3928383900),new Word64(3515267271,566280711),new Word64(3940187606,3454069534),new Word64(4118630271,4000239992),new Word64(116418474,1914138554),new Word64(174292421,2731055270),new Word64(289380356,3203993006),new Word64(460393269,320620315),new Word64(685471733,587496836),new Word64(852142971,1086792851),new Word64(1017036298,365543100),new Word64(1126000580,2618297676),new Word64(1288033470,3409855158),new Word64(1501505948,4234509866),new Word64(1607167915,987167468),new Word64(1816402316,1246189591)])}};function ch(e,t,a,r,i){e.assign(t);e.and(a);i.assign(t);i.not();i.and(r);e.xor(i)}function maj(e,t,a,r,i){e.assign(t);e.and(a);i.assign(t);i.and(r);e.xor(i);i.assign(a);i.and(r);e.xor(i)}function sigma(e,t,a){e.assign(t);e.rotateRight(28);a.assign(t);a.rotateRight(34);e.xor(a);a.assign(t);a.rotateRight(39);e.xor(a)}function sigmaPrime(e,t,a){e.assign(t);e.rotateRight(14);a.assign(t);a.rotateRight(18);e.xor(a);a.assign(t);a.rotateRight(41);e.xor(a)}function littleSigma(e,t,a){e.assign(t);e.rotateRight(1);a.assign(t);a.rotateRight(8);e.xor(a);a.assign(t);a.shiftRight(7);e.xor(a)}function littleSigmaPrime(e,t,a){e.assign(t);e.rotateRight(19);a.assign(t);a.rotateRight(61);e.xor(a);a.assign(t);a.shiftRight(6);e.xor(a)}function calculateSHA512(e,t,a,r=!1){let i,n,s,o,c,l,h,u;if(r){i=new Word64(3418070365,3238371032);n=new Word64(1654270250,914150663);s=new Word64(2438529370,812702999);o=new Word64(355462360,4144912697);c=new Word64(1731405415,4290775857);l=new Word64(2394180231,1750603025);h=new Word64(3675008525,1694076839);u=new Word64(1203062813,3204075428)}else{i=new Word64(1779033703,4089235720);n=new Word64(3144134277,2227873595);s=new Word64(1013904242,4271175723);o=new Word64(2773480762,1595750129);c=new Word64(1359893119,2917565137);l=new Word64(2600822924,725511199);h=new Word64(528734635,4215389547);u=new Word64(1541459225,327033209)}const d=128*Math.ceil((a+17)/128),f=new Uint8Array(d);let g,p;for(g=0;g>>29&255;f[g++]=a>>21&255;f[g++]=a>>13&255;f[g++]=a>>5&255;f[g++]=a<<3&255;const b=new Array(80);for(g=0;g<80;g++)b[g]=new Word64(0,0);const{k:y}=mc;let w=new Word64(0,0),S=new Word64(0,0),x=new Word64(0,0),k=new Word64(0,0),C=new Word64(0,0),v=new Word64(0,0),F=new Word64(0,0),O=new Word64(0,0);const T=new Word64(0,0),M=new Word64(0,0),D=new Word64(0,0),R=new Word64(0,0);let N,E;for(g=0;g>>t|e<<32-t}function calculate_sha256_ch(e,t,a){return e&t^~e&a}function calculate_sha256_maj(e,t,a){return e&t^e&a^t&a}function calculate_sha256_sigma(e){return rotr(e,2)^rotr(e,13)^rotr(e,22)}function calculate_sha256_sigmaPrime(e){return rotr(e,6)^rotr(e,11)^rotr(e,25)}function calculate_sha256_littleSigma(e){return rotr(e,7)^rotr(e,18)^e>>>3}function calculateSHA256(e,t,a){let r=1779033703,i=3144134277,n=1013904242,s=2773480762,o=1359893119,c=2600822924,l=528734635,h=1541459225;const u=64*Math.ceil((a+9)/64),d=new Uint8Array(u);let f,g;for(f=0;f>>29&255;d[f++]=a>>21&255;d[f++]=a>>13&255;d[f++]=a>>5&255;d[f++]=a<<3&255;const m=new Uint32Array(64),{k:b}=bc;for(f=0;f>>10)+m[g-7]+calculate_sha256_littleSigma(m[g-15])+m[g-16]|0;let e,t,a=r,u=i,p=n,w=s,S=o,x=c,k=l,C=h;for(g=0;g<64;++g){e=C+calculate_sha256_sigmaPrime(S)+calculate_sha256_ch(S,x,k)+b[g]+m[g];t=calculate_sha256_sigma(a)+calculate_sha256_maj(a,u,p);C=k;k=x;x=S;S=w+e|0;w=p;p=u;u=a;a=e+t|0}r=r+a|0;i=i+u|0;n=n+p|0;s=s+w|0;o=o+S|0;c=c+x|0;l=l+k|0;h=h+C|0}var y;return new Uint8Array([r>>24&255,r>>16&255,r>>8&255,255&r,i>>24&255,i>>16&255,i>>8&255,255&i,n>>24&255,n>>16&255,n>>8&255,255&n,s>>24&255,s>>16&255,s>>8&255,255&s,o>>24&255,o>>16&255,o>>8&255,255&o,c>>24&255,c>>16&255,c>>8&255,255&c,l>>24&255,l>>16&255,l>>8&255,255&l,h>>24&255,h>>16&255,h>>8&255,255&h])}class DecryptStream extends DecodeStream{constructor(e,t,a){super(t);this.str=e;this.dict=e.dict;this.decrypt=a;this.nextChunk=null;this.initialized=!1}readBlock(){let e;if(this.initialized)e=this.nextChunk;else{e=this.str.getBytes(512);this.initialized=!0}if(!e?.length){this.eof=!0;return}this.nextChunk=this.str.getBytes(512);const t=this.nextChunk?.length>0;e=(0,this.decrypt)(e,!t);const a=this.bufferLength,r=a+e.length;this.ensureBuffer(r).set(e,a);this.bufferLength=r}}class ARCFourCipher{constructor(e){this.a=0;this.b=0;const t=new Uint8Array(256),a=e.length;for(let e=0;e<256;++e)t[e]=e;for(let r=0,i=0;r<256;++r){const n=t[r];i=i+n+e[r%a]&255;t[r]=t[i];t[i]=n}this.s=t}encryptBlock(e){let t=this.a,a=this.b;const r=this.s,i=e.length,n=new Uint8Array(i);for(let s=0;st<128?t<<1:t<<1^27));constructor(){this.buffer=new Uint8Array(16);this.bufferPosition=0}_expandKey(e){unreachable("Cannot call `_expandKey` on the base class")}_decrypt(e,t){let a,r,i;const n=new Uint8Array(16);n.set(e);for(let e=0,a=this._keySize;e<16;++e,++a)n[e]^=t[a];for(let e=this._cyclesOfRepetition-1;e>=1;--e){a=n[13];n[13]=n[9];n[9]=n[5];n[5]=n[1];n[1]=a;a=n[14];r=n[10];n[14]=n[6];n[10]=n[2];n[6]=a;n[2]=r;a=n[15];r=n[11];i=n[7];n[15]=n[3];n[11]=a;n[7]=r;n[3]=i;for(let e=0;e<16;++e)n[e]=this._inv_s[n[e]];for(let a=0,r=16*e;a<16;++a,++r)n[a]^=t[r];for(let e=0;e<16;e+=4){const t=this._mix[n[e]],r=this._mix[n[e+1]],i=this._mix[n[e+2]],s=this._mix[n[e+3]];a=t^r>>>8^r<<24^i>>>16^i<<16^s>>>24^s<<8;n[e]=a>>>24&255;n[e+1]=a>>16&255;n[e+2]=a>>8&255;n[e+3]=255&a}}a=n[13];n[13]=n[9];n[9]=n[5];n[5]=n[1];n[1]=a;a=n[14];r=n[10];n[14]=n[6];n[10]=n[2];n[6]=a;n[2]=r;a=n[15];r=n[11];i=n[7];n[15]=n[3];n[11]=a;n[7]=r;n[3]=i;for(let e=0;e<16;++e){n[e]=this._inv_s[n[e]];n[e]^=t[e]}return n}_encrypt(e,t){const a=this._s;let r,i,n;const s=new Uint8Array(16);s.set(e);for(let e=0;e<16;++e)s[e]^=t[e];for(let e=1;e=r;--a)if(e[a]!==t){t=0;break}o-=t;n[n.length-1]=e.subarray(0,16-t)}}const c=new Uint8Array(o);for(let e=0,t=0,a=n.length;e=256&&(o=255&(27^o))}for(let t=0;t<4;++t){a[e]=r^=a[e-32];e++;a[e]=i^=a[e-32];e++;a[e]=n^=a[e-32];e++;a[e]=s^=a[e-32];e++}}return a}}class PDFBase{_hash(e,t,a){unreachable("Abstract method `_hash` called")}checkOwnerPassword(e,t,a,r){const i=new Uint8Array(e.length+56);i.set(e,0);i.set(t,e.length);i.set(a,e.length+t.length);return isArrayEqual(this._hash(e,i,a),r)}checkUserPassword(e,t,a){const r=new Uint8Array(e.length+8);r.set(e,0);r.set(t,e.length);return isArrayEqual(this._hash(e,r,[]),a)}getOwnerKey(e,t,a,r){const i=new Uint8Array(e.length+56);i.set(e,0);i.set(t,e.length);i.set(a,e.length+t.length);const n=this._hash(e,i,a);return new AES256Cipher(n).decryptBlock(r,!1,new Uint8Array(16))}getUserKey(e,t,a){const r=new Uint8Array(e.length+8);r.set(e,0);r.set(t,e.length);const i=this._hash(e,r,[]);return new AES256Cipher(i).decryptBlock(a,!1,new Uint8Array(16))}}class PDF17 extends PDFBase{_hash(e,t,a){return calculateSHA256(t,0,t.length)}}class PDF20 extends PDFBase{_hash(e,t,a){let r=calculateSHA256(t,0,t.length).subarray(0,32),i=[0],n=0;for(;n<64||i.at(-1)>n-32;){const t=e.length+r.length+a.length,l=new Uint8Array(t);let h=0;l.set(e,h);h+=e.length;l.set(r,h);h+=r.length;l.set(a,h);const u=new Uint8Array(64*t);for(let e=0,a=0;e<64;e++,a+=t)u.set(l,a);i=new AES128Cipher(r.subarray(0,16)).encrypt(u,r.subarray(16,32));const d=Math.sumPrecise(i.slice(0,16))%3;0===d?r=calculateSHA256(i,0,i.length):1===d?r=(s=i,o=0,c=i.length,calculateSHA512(s,o,c,!0)):2===d&&(r=calculateSHA512(i,0,i.length));n++}var s,o,c;return r.subarray(0,32)}}class CipherTransform{constructor(e,t){this.StringCipherConstructor=e;this.StreamCipherConstructor=t}createStream(e,t){const a=new this.StreamCipherConstructor;return new DecryptStream(e,t,(function cipherTransformDecryptStream(e,t){return a.decryptBlock(e,t)}))}decryptString(e){const t=new this.StringCipherConstructor;let a=stringToBytes(e);a=t.decryptBlock(a,!0);return bytesToString(a)}encryptString(e){const t=new this.StringCipherConstructor;if(t instanceof AESBaseCipher){const a=16-e.length%16;e+=String.fromCharCode(a).repeat(a);const r=new Uint8Array(16);crypto.getRandomValues(r);let i=stringToBytes(e);i=t.encrypt(i,r);const n=new Uint8Array(16+i.length);n.set(r);n.set(i,16);return bytesToString(n)}let a=stringToBytes(e);a=t.encrypt(a);return bytesToString(a)}}class CipherTransformFactory{static get _defaultPasswordBytes(){return shadow(this,"_defaultPasswordBytes",new Uint8Array([40,191,78,94,78,117,138,65,100,0,78,86,255,250,1,8,46,46,0,182,208,104,62,128,47,12,169,254,100,83,105,122]))}#oe(e,t,a,r,i,n,s,o,c,l,h,u){if(t){const e=Math.min(127,t.length);t=t.subarray(0,e)}else t=[];const d=6===e?new PDF20:new PDF17;return d.checkUserPassword(t,o,s)?d.getUserKey(t,c,h):t.length&&d.checkOwnerPassword(t,r,n,a)?d.getOwnerKey(t,i,n,l):null}#ce(e,t,a,r,i,n,s,o){const c=40+a.length+e.length,l=new Uint8Array(c);let h,u,d=0;if(t){u=Math.min(32,t.length);for(;d>8&255;l[d++]=i>>16&255;l[d++]=i>>>24&255;l.set(e,d);d+=e.length;if(n>=4&&!o){l.fill(255,d,d+4);d+=4}let f=calculateMD5(l,0,d);const g=s>>3;if(n>=3)for(h=0;h<50;++h)f=calculateMD5(f,0,g);const p=f.subarray(0,g);let m,b;if(n>=3){d=0;l.set(CipherTransformFactory._defaultPasswordBytes,d);d+=32;l.set(e,d);d+=e.length;m=new ARCFourCipher(p);b=m.encryptBlock(calculateMD5(l,0,d));u=p.length;const t=new Uint8Array(u);for(h=1;h<=19;++h){for(let e=0;er[t]===e))?p:null}#le(e,t,a,r){const i=new Uint8Array(32);let n=0;const s=Math.min(32,e.length);for(;n>3;if(a>=3)for(o=0;o<50;++o)c=calculateMD5(c,0,c.length);let h,u;if(a>=3){u=t;const e=new Uint8Array(l);for(o=19;o>=0;o--){for(let t=0;t>8&255;n[s++]=e>>16&255;n[s++]=255&t;n[s++]=t>>8&255;if(r){n[s++]=115;n[s++]=65;n[s++]=108;n[s++]=84}return calculateMD5(n,0,s).subarray(0,Math.min(i+5,16))}#ue(e,t,a,r,i){if(!(t instanceof Name))throw new FormatError("Invalid crypt filter name.");const n=this,s=e.get(t.name),o=s?.get("CFM");if(!o||"None"===o.name)return function(){return new NullCipher};if("V2"===o.name)return function(){return new ARCFourCipher(n.#he(a,r,i,!1))};if("AESV2"===o.name)return function(){return new AES128Cipher(n.#he(a,r,i,!0))};if("AESV3"===o.name)return function(){return new AES256Cipher(i)};throw new FormatError("Unknown crypto method")}constructor(e,t,a){const r=e.get("Filter");if(!isName(r,"Standard"))throw new FormatError("unknown encryption method");this.filterName=r.name;this.dict=e;const i=e.get("V");if(!Number.isInteger(i)||1!==i&&2!==i&&4!==i&&5!==i)throw new FormatError("unsupported encryption algorithm");this.algorithm=i;let n=e.get("Length");if(!n)if(i<=3)n=40;else{const t=e.get("CF"),a=e.get("StmF");if(t instanceof Dict&&a instanceof Name){t.suppressEncryption=!0;const e=t.get(a.name);n=e?.get("Length")||128;n<40&&(n<<=3)}}if(!Number.isInteger(n)||n<40||n%8!=0)throw new FormatError("invalid key length");const s=stringToBytes(e.get("O")),o=stringToBytes(e.get("U")),c=s.subarray(0,32),l=o.subarray(0,32),h=e.get("P"),u=e.get("R"),d=(4===i||5===i)&&!1!==e.get("EncryptMetadata");this.encryptMetadata=d;const f=stringToBytes(t);let g,p;if(a){if(6===u)try{a=utf8StringToString(a)}catch{warn("CipherTransformFactory: Unable to convert UTF8 encoded password.")}g=stringToBytes(a)}if(5!==i)p=this.#ce(f,g,c,l,h,u,n,d);else{const t=s.subarray(32,40),a=s.subarray(40,48),r=o.subarray(0,48),i=o.subarray(32,40),n=o.subarray(40,48),h=stringToBytes(e.get("OE")),d=stringToBytes(e.get("UE")),f=stringToBytes(e.get("Perms"));p=this.#oe(u,g,c,t,a,r,l,i,n,h,d,f)}if(!p){if(!a)throw new PasswordException("No password given",ha);const e=this.#le(g,c,u,n);p=this.#ce(f,e,c,l,h,u,n,d)}if(!p)throw new PasswordException("Incorrect Password",ua);if(4===i&&p.length<16){this.encryptionKey=new Uint8Array(16);this.encryptionKey.set(p)}else this.encryptionKey=p;if(i>=4){const t=e.get("CF");t instanceof Dict&&(t.suppressEncryption=!0);this.cf=t;this.stmf=e.get("StmF")||Name.get("Identity");this.strf=e.get("StrF")||Name.get("Identity");this.eff=e.get("EFF")||this.stmf}}createCipherTransform(e,t){if(4===this.algorithm||5===this.algorithm)return new CipherTransform(this.#ue(this.cf,this.strf,e,t,this.encryptionKey),this.#ue(this.cf,this.stmf,e,t,this.encryptionKey));const a=this.#he(e,t,this.encryptionKey,!1),cipherConstructor=function(){return new ARCFourCipher(a)};return new CipherTransform(cipherConstructor,cipherConstructor)}}class XRef{#de=null;constructor(e,t){this.stream=e;this.pdfManager=t;this.entries=[];this._xrefStms=new Set;this._cacheMap=new Map;this._pendingRefs=new RefSet;this._newPersistentRefNum=null;this._newTemporaryRefNum=null;this._persistentRefsCache=null}getNewPersistentRef(e){null===this._newPersistentRefNum&&(this._newPersistentRefNum=this.entries.length||1);const t=this._newPersistentRefNum++;this._cacheMap.set(t,e);return Ref.get(t,0)}getNewTemporaryRef(){if(null===this._newTemporaryRefNum){this._newTemporaryRefNum=this.entries.length||1;if(this._newPersistentRefNum){this._persistentRefsCache=new Map;for(let e=this._newTemporaryRefNum;e0;){const[s,o]=n;if(!Number.isInteger(s)||!Number.isInteger(o))throw new FormatError(`Invalid XRef range fields: ${s}, ${o}`);if(!Number.isInteger(a)||!Number.isInteger(r)||!Number.isInteger(i))throw new FormatError(`Invalid XRef entry fields length: ${s}, ${o}`);for(let n=t.entryNum;n=e.length);){a+=String.fromCharCode(r);r=e[t]}return a}function skipUntil(e,t,a){const r=a.length,i=e.length;let n=0;for(;t=r)break;t++;n++}return n}const e=/\b(endobj|\d+\s+\d+\s+obj|xref|trailer\s*<<)\b/g,t=/\b(startxref|\d+\s+\d+\s+obj)\b/g,a=/^(\d+)\s+(\d+)\s+obj\b/,r=new Uint8Array([116,114,97,105,108,101,114]),i=new Uint8Array([115,116,97,114,116,120,114,101,102]),n=new Uint8Array([47,88,82,101,102]);this.entries.length=0;this._cacheMap.clear();const s=this.stream;s.pos=0;const o=s.getBytes(),c=bytesToString(o),l=o.length;let h=s.start;const u=[],d=[];for(;h=l)break;f=o[h]}while(10!==f&&13!==f);continue}const g=readToken(o,h);let p;if(g.startsWith("xref")&&(4===g.length||/\s/.test(g[4]))){h+=skipUntil(o,h,r);u.push(h);h+=skipUntil(o,h,i)}else if(p=a.exec(g)){const t=0|p[1],a=0|p[2],r=h+g.length;let i,u=!1;if(this.entries[t]){if(this.entries[t].gen===a)try{new Parser({lexer:new Lexer(s.makeSubStream(r))}).getObj();u=!0}catch(e){e instanceof ParserEOFException?warn(`indexObjects -- checking object (${g}): "${e}".`):u=!0}}else u=!0;u&&(this.entries[t]={offset:h-s.start,gen:a,uncompressed:!0});e.lastIndex=r;const f=e.exec(c);if(f){i=e.lastIndex+1-h;if("endobj"!==f[1]){warn(`indexObjects: Found "${f[1]}" inside of another "obj", caused by missing "endobj" -- trying to recover.`);i-=f[1].length+1}}else i=l-h;const m=o.subarray(h,h+i),b=skipUntil(m,0,n);if(b0?Math.max(...this._xrefStms):null)}getEntry(e){const t=this.entries[e];return t&&!t.free&&t.offset?t:null}fetchIfRef(e,t=!1){return e instanceof Ref?this.fetch(e,t):e}fetch(e,t=!1){if(!(e instanceof Ref))throw new Error("ref object is not a reference");const a=e.num,r=this._cacheMap.get(a);if(void 0!==r){r instanceof Dict&&!r.objId&&(r.objId=e.toString());return r}let i=this.getEntry(a);if(null===i)return i;if(this._pendingRefs.has(e)){this._pendingRefs.remove(e);warn(`Ignoring circular reference: ${e}.`);return ya}this._pendingRefs.put(e);try{i=i.uncompressed?this.fetchUncompressed(e,i,t):this.fetchCompressed(e,i,t);this._pendingRefs.remove(e)}catch(t){this._pendingRefs.remove(e);throw t}i instanceof Dict?i.objId=e.toString():i instanceof BaseStream&&(i.dict.objId=e.toString());return i}fetchUncompressed(e,t,a=!1){const r=e.gen;let i=e.num;if(t.gen!==r){const n=`Inconsistent generation in XRef: ${e}`;if(this._generationFallback&&t.gen0&&t[3]-t[1]>0)return t;warn(`Empty, or invalid, /${e} entry.`)}return null}get mediaBox(){return shadow(this,"mediaBox",this._getBoundingBox("MediaBox")||yc)}get cropBox(){return shadow(this,"cropBox",this._getBoundingBox("CropBox")||this.mediaBox)}get userUnit(){const e=this.pageDict.get("UserUnit");return shadow(this,"userUnit","number"==typeof e&&e>0?e:1)}get view(){const{cropBox:e,mediaBox:t}=this;if(e!==t&&!isArrayEqual(e,t)){const a=Util.intersect(e,t);if(a&&a[2]-a[0]>0&&a[3]-a[1]>0)return shadow(this,"view",a);warn("Empty /CropBox and /MediaBox intersection.")}return shadow(this,"view",t)}get rotate(){let e=this._getInheritableProperty("Rotate")||0;e%90!=0?e=0:e>=360?e%=360:e<0&&(e=(e%360+360)%360);return shadow(this,"rotate",e)}_onSubStreamError(e,t){if(!this.evaluatorOptions.ignoreErrors)throw e;warn(`getContentStream - ignoring sub-stream (${t}): "${e}".`)}async getContentStream(){const e=await this.pdfManager.ensure(this,"content");return e instanceof BaseStream?e:Array.isArray(e)?new StreamsSequenceStream(e,this._onSubStreamError.bind(this)):new NullStream}get xfaData(){return shadow(this,"xfaData",this.xfaFactory?{bbox:this.xfaFactory.getBoundingBox(this.pageIndex)}:null)}async#fe(e,t,a){const r=[];for(const i of e)if(i.id){const e=Ref.fromString(i.id);if(!e){warn(`A non-linked annotation cannot be modified: ${i.id}`);continue}if(i.deleted){t.put(e,e);if(i.popupRef){const e=Ref.fromString(i.popupRef);e&&t.put(e,e)}continue}a?.put(e);i.ref=e;r.push(this.xref.fetchAsync(e).then((e=>{e instanceof Dict&&(i.oldAnnotation=e.clone())}),(()=>{warn(`Cannot fetch \`oldAnnotation\` for: ${e}.`)})));delete i.id}await Promise.all(r)}async saveNewAnnotations(e,t,a,r,i){if(this.xfaFactory)throw new Error("XFA: Cannot save new annotations.");const n=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalColorSpaceCache:this.globalColorSpaceCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions}),s=new RefSetCache,o=new RefSet;await this.#fe(a,s,o);const c=this.pageDict,l=this.annotations.filter((e=>!(e instanceof Ref&&s.has(e)))),h=await AnnotationFactory.saveNewAnnotations(n,t,a,r,i);for(const{ref:e}of h.annotations)e instanceof Ref&&!o.has(e)&&l.push(e);const u=c.clone();u.set("Annots",l);i.put(this.ref,{data:u});for(const e of s)i.put(e,{data:null})}async save(e,t,a,r){const i=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalColorSpaceCache:this.globalColorSpaceCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions}),n=await this._parsedAnnotations,s=[];for(const e of n)s.push(e.save(i,t,a,r).catch((function(e){warn(`save - ignoring annotation data during "${t.name}" task: "${e}".`);return null})));return Promise.all(s)}async loadResources(e){await(this.resourcesPromise??=this.pdfManager.ensure(this,"resources"));const t=new ObjectLoader(this.resources,e,this.xref);await t.load()}async#ge(e,t){const a=e?.get("Resources");if(!(a instanceof Dict&&a.size))return this.resources;const r=new ObjectLoader(a,t,this.xref);await r.load();return Dict.merge({xref:this.xref,dictArray:[a,this.resources],mergeSubDicts:!0})}async getOperatorList({handler:e,sink:t,task:a,intent:r,cacheKey:i,annotationStorage:c=null,modifiedIds:d=null}){const g=this.getContentStream(),p=this.loadResources(Ia),m=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalColorSpaceCache:this.globalColorSpaceCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions}),b=this.xfaFactory?null:getNewAnnotationsMap(c),y=b?.get(this.pageIndex);let w=Promise.resolve(null),S=null;if(y){const e=this.pdfManager.ensureDoc("annotationGlobals");let t;const r=new Set;for(const{bitmapId:e,bitmap:t}of y)!e||t||r.has(e)||r.add(e);const{isOffscreenCanvasSupported:i}=this.evaluatorOptions;if(r.size>0){const e=y.slice();for(const[t,a]of c)t.startsWith(f)&&a.bitmap&&r.has(a.bitmapId)&&e.push(a);t=AnnotationFactory.generateImages(e,this.xref,i)}else t=AnnotationFactory.generateImages(y,this.xref,i);S=new RefSet;w=Promise.all([e,this.#fe(y,S,null)]).then((([e])=>e?AnnotationFactory.printNewAnnotations(e,m,a,y,t):null))}const x=Promise.all([g,p]).then((async([n])=>{const s=await this.#ge(n.dict,Ia),o=new OperatorList(r,t);e.send("StartRenderPage",{transparency:m.hasBlendModes(s,this.nonBlendModesSet),pageIndex:this.pageIndex,cacheKey:i});await m.getOperatorList({stream:n,task:a,resources:s,operatorList:o});return o}));let[k,C,v]=await Promise.all([x,this._parsedAnnotations,w]);if(v){C=C.filter((e=>!(e.ref&&S.has(e.ref))));for(let e=0,t=v.length;ee.ref&&isRefsEqual(e.ref,a.refToReplace)));if(r>=0){C.splice(r,1,a);v.splice(e--,1);t--}}}C=C.concat(v)}if(0===C.length||r&h){k.flush(!0);return{length:k.totalLength}}const F=!!(r&l),O=!!(r&u),T=!!(r&n),M=!!(r&s),D=!!(r&o),R=[];for(const e of C)(T||M&&e.mustBeViewed(c,F)&&e.mustBeViewedWhenEditing(O,d)||D&&e.mustBePrinted(c))&&R.push(e.getOperatorList(m,a,r,c).catch((function(e){warn(`getOperatorList - ignoring annotation data during "${a.name}" task: "${e}".`);return{opList:null,separateForm:!1,separateCanvas:!1}})));const N=await Promise.all(R);let E=!1,L=!1;for(const{opList:e,separateForm:t,separateCanvas:a}of N){k.addOpList(e);E||=t;L||=a}k.flush(!0,{form:E,canvas:L});return{length:k.totalLength}}async extractTextContent({handler:e,task:t,includeMarkedContent:a,disableNormalization:r,sink:i}){const n=this.getContentStream(),s=this.loadResources(Oa),o=this.pdfManager.ensureCatalog("lang"),[c,,l]=await Promise.all([n,s,o]),h=await this.#ge(c.dict,Oa);return new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalColorSpaceCache:this.globalColorSpaceCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions}).getTextContent({stream:c,task:t,resources:h,includeMarkedContent:a,disableNormalization:r,sink:i,viewBox:this.view,lang:l})}async getStructTree(){const e=await this.pdfManager.ensureCatalog("structTreeRoot");if(!e)return null;await this._parsedAnnotations;try{const t=await this.pdfManager.ensure(this,"_parseStructTree",[e]);return await this.pdfManager.ensure(t,"serializable")}catch(e){warn(`getStructTree: "${e}".`);return null}}_parseStructTree(e){const t=new StructTreePage(e,this.pageDict);t.parse(this.ref);return t}async getAnnotationsData(e,t,a){const r=await this._parsedAnnotations;if(0===r.length)return r;const i=[],c=[];let l;const h=!!(a&n),u=!!(a&s),d=!!(a&o);for(const a of r){const r=h||u&&a.viewable;(r||d&&a.printable)&&i.push(a.data);if(a.hasTextContent&&r){l||=new PartialEvaluator({xref:this.xref,handler:e,pageIndex:this.pageIndex,idFactory:this._localIdFactory,fontCache:this.fontCache,builtInCMapCache:this.builtInCMapCache,standardFontDataCache:this.standardFontDataCache,globalColorSpaceCache:this.globalColorSpaceCache,globalImageCache:this.globalImageCache,systemFontCache:this.systemFontCache,options:this.evaluatorOptions});c.push(a.extractTextContent(l,t,[-1/0,-1/0,1/0,1/0]).catch((function(e){warn(`getAnnotationsData - ignoring textContent during "${t.name}" task: "${e}".`)})))}}await Promise.all(c);return i}get annotations(){const e=this._getInheritableProperty("Annots");return shadow(this,"annotations",Array.isArray(e)?e:[])}get _parsedAnnotations(){return shadow(this,"_parsedAnnotations",this.pdfManager.ensure(this,"annotations").then((async e=>{if(0===e.length)return e;const[t,a]=await Promise.all([this.pdfManager.ensureDoc("annotationGlobals"),this.pdfManager.ensureDoc("fieldObjects")]);if(!t)return[];const r=a?.orphanFields,i=[];for(const a of e)i.push(AnnotationFactory.create(this.xref,a,t,this._localIdFactory,!1,r,this.ref).catch((function(e){warn(`_parsedAnnotations: "${e}".`);return null})));const n=[];let s,o;for(const e of await Promise.all(i))e&&(e instanceof WidgetAnnotation?(o||=[]).push(e):e instanceof PopupAnnotation?(s||=[]).push(e):n.push(e));o&&n.push(...o);s&&n.push(...s);return n})))}get jsActions(){return shadow(this,"jsActions",collectActions(this.xref,this.pageDict,Se))}}const wc=new Uint8Array([37,80,68,70,45]),Sc=new Uint8Array([115,116,97,114,116,120,114,101,102]),xc=new Uint8Array([101,110,100,111,98,106]);function find(e,t,a=1024,r=!1){const i=t.length,n=e.peekBytes(a),s=n.length-i;if(s<=0)return!1;if(r){const a=i-1;let r=n.length-1;for(;r>=a;){let s=0;for(;s=i){e.pos+=r-a;return!0}r--}}else{let a=0;for(;a<=s;){let r=0;for(;r=i){e.pos+=a;return!0}a++}}return!1}class PDFDocument{constructor(e,t){if(t.length<=0)throw new InvalidPDFException("The PDF file is empty, i.e. its size is zero bytes.");this.pdfManager=e;this.stream=t;this.xref=new XRef(t,e);this._pagePromises=new Map;this._version=null;const a={font:0};this._globalIdFactory=class{static getDocId(){return`g_${e.docId}`}static createFontId(){return"f"+ ++a.font}static createObjId(){unreachable("Abstract method `createObjId` called.")}static getPageObjId(){unreachable("Abstract method `getPageObjId` called.")}}}parse(e){this.xref.parse(e);this.catalog=new Catalog(this.pdfManager,this.xref)}get linearization(){let e=null;try{e=Linearization.create(this.stream)}catch(e){if(e instanceof MissingDataException)throw e;info(e)}return shadow(this,"linearization",e)}get startXRef(){const e=this.stream;let t=0;if(this.linearization){e.reset();if(find(e,xc)){e.skip(6);let a=e.peekByte();for(;isWhiteSpace(a);){e.pos++;a=e.peekByte()}t=e.pos-e.start}}else{const a=1024,r=Sc.length;let i=!1,n=e.end;for(;!i&&n>0;){n-=a-r;n<0&&(n=0);e.pos=n;i=find(e,Sc,a,!0)}if(i){e.skip(9);let a;do{a=e.getByte()}while(isWhiteSpace(a));let r="";for(;a>=32&&a<=57;){r+=String.fromCharCode(a);a=e.getByte()}t=parseInt(r,10);isNaN(t)&&(t=0)}}return shadow(this,"startXRef",t)}checkHeader(){const e=this.stream;e.reset();if(!find(e,wc))return;e.moveStart();e.skip(wc.length);let t,a="";for(;(t=e.getByte())>32&&a.length<7;)a+=String.fromCharCode(t);Ca.test(a)?this._version=a:warn(`Invalid PDF header version: ${a}`)}parseStartXRef(){this.xref.setStartXRef(this.startXRef)}get numPages(){let e=0;e=this.catalog.hasActualNumPages?this.catalog.numPages:this.xfaFactory?this.xfaFactory.getNumPages():this.linearization?this.linearization.numPages:this.catalog.numPages;return shadow(this,"numPages",e)}_hasOnlyDocumentSignatures(e,t=0){return!!Array.isArray(e)&&e.every((e=>{if(!((e=this.xref.fetchIfRef(e))instanceof Dict))return!1;if(e.has("Kids")){if(++t>10){warn("_hasOnlyDocumentSignatures: maximum recursion depth reached");return!1}return this._hasOnlyDocumentSignatures(e.get("Kids"),t)}const a=isName(e.get("FT"),"Sig"),r=e.get("Rect"),i=Array.isArray(r)&&r.every((e=>0===e));return a&&i}))}get _xfaStreams(){const{acroForm:e}=this.catalog;if(!e)return null;const t=e.get("XFA"),a=new Map(["xdp:xdp","template","datasets","config","connectionSet","localeSet","stylesheet","/xdp:xdp"].map((e=>[e,null])));if(t instanceof BaseStream&&!t.isEmpty){a.set("xdp:xdp",t);return a}if(!Array.isArray(t)||0===t.length)return null;for(let e=0,r=t.length;e0;e.hasFields=r;const i=t.get("XFA");e.hasXfa=Array.isArray(i)&&i.length>0||i instanceof BaseStream&&!i.isEmpty;const n=!!(1&t.get("SigFlags")),s=n&&this._hasOnlyDocumentSignatures(a);e.hasAcroForm=r&&!s;e.hasSignatures=n}catch(e){if(e instanceof MissingDataException)throw e;warn(`Cannot fetch form information: "${e}".`)}return shadow(this,"formInfo",e)}get documentInfo(){const e={PDFFormatVersion:this.version,Language:this.catalog.lang,EncryptFilterName:this.xref.encrypt?this.xref.encrypt.filterName:null,IsLinearized:!!this.linearization,IsAcroFormPresent:this.formInfo.hasAcroForm,IsXFAPresent:this.formInfo.hasXfa,IsCollectionPresent:!!this.catalog.collection,IsSignaturesPresent:this.formInfo.hasSignatures};let t;try{t=this.xref.trailer.get("Info")}catch(e){if(e instanceof MissingDataException)throw e;info("The document information dictionary is invalid.")}if(!(t instanceof Dict))return shadow(this,"documentInfo",e);for(const[a,r]of t){switch(a){case"Title":case"Author":case"Subject":case"Keywords":case"Creator":case"Producer":case"CreationDate":case"ModDate":if("string"==typeof r){e[a]=stringToPDFString(r);continue}break;case"Trapped":if(r instanceof Name){e[a]=r;continue}break;default:let t;switch(typeof r){case"string":t=stringToPDFString(r);break;case"number":case"boolean":t=r;break;default:r instanceof Name&&(t=r)}if(void 0===t){warn(`Bad value, for custom key "${a}", in Info: ${r}.`);continue}e.Custom??=Object.create(null);e.Custom[a]=t;continue}warn(`Bad value, for key "${a}", in Info: ${r}.`)}return shadow(this,"documentInfo",e)}get fingerprints(){const e="\0".repeat(16);function validate(t){return"string"==typeof t&&16===t.length&&t!==e}const t=this.xref.trailer.get("ID");let a,r;if(Array.isArray(t)&&validate(t[0])){a=stringToBytes(t[0]);t[1]!==t[0]&&validate(t[1])&&(r=stringToBytes(t[1]))}else a=calculateMD5(this.stream.getByteRange(0,1024),0,1024);return shadow(this,"fingerprints",[toHexUtil(a),r?toHexUtil(r):null])}async _getLinearizationPage(e){const{catalog:t,linearization:a,xref:r}=this,i=Ref.get(a.objectNumberFirst,0);try{const e=await r.fetchAsync(i);if(e instanceof Dict){let a=e.getRaw("Type");a instanceof Ref&&(a=await r.fetchAsync(a));if(isName(a,"Page")||!e.has("Type")&&!e.has("Kids")&&e.has("Contents")){t.pageKidsCountCache.has(i)||t.pageKidsCountCache.put(i,1);t.pageIndexCache.has(i)||t.pageIndexCache.put(i,0);return[e,i]}}throw new FormatError("The Linearization dictionary doesn't point to a valid Page dictionary.")}catch(a){warn(`_getLinearizationPage: "${a.message}".`);return t.getPageDict(e)}}getPage(e){const t=this._pagePromises.get(e);if(t)return t;const{catalog:a,linearization:r,xfaFactory:i}=this;let n;n=i?Promise.resolve([Dict.empty,null]):r?.pageFirst===e?this._getLinearizationPage(e):a.getPageDict(e);n=n.then((([t,r])=>new Page({pdfManager:this.pdfManager,xref:this.xref,pageIndex:e,pageDict:t,ref:r,globalIdFactory:this._globalIdFactory,fontCache:a.fontCache,builtInCMapCache:a.builtInCMapCache,standardFontDataCache:a.standardFontDataCache,globalColorSpaceCache:a.globalColorSpaceCache,globalImageCache:a.globalImageCache,systemFontCache:a.systemFontCache,nonBlendModesSet:a.nonBlendModesSet,xfaFactory:i})));this._pagePromises.set(e,n);return n}async checkFirstPage(e=!1){if(!e)try{await this.getPage(0)}catch(e){if(e instanceof XRefEntryException){this._pagePromises.delete(0);await this.cleanup();throw new XRefParseException}}}async checkLastPage(e=!1){const{catalog:t,pdfManager:a}=this;t.setActualNumPages();let r;try{await Promise.all([a.ensureDoc("xfaFactory"),a.ensureDoc("linearization"),a.ensureCatalog("numPages")]);if(this.xfaFactory)return;r=this.linearization?this.linearization.numPages:t.numPages;if(!Number.isInteger(r))throw new FormatError("Page count is not an integer.");if(r<=1)return;await this.getPage(r-1)}catch(i){this._pagePromises.delete(r-1);await this.cleanup();if(i instanceof XRefEntryException&&!e)throw new XRefParseException;warn(`checkLastPage - invalid /Pages tree /Count: ${r}.`);let n;try{n=await t.getAllPageDicts(e)}catch(a){if(a instanceof XRefEntryException&&!e)throw new XRefParseException;t.setActualNumPages(1);return}for(const[e,[r,i]]of n){let n;if(r instanceof Error){n=Promise.reject(r);n.catch((()=>{}))}else n=Promise.resolve(new Page({pdfManager:a,xref:this.xref,pageIndex:e,pageDict:r,ref:i,globalIdFactory:this._globalIdFactory,fontCache:t.fontCache,builtInCMapCache:t.builtInCMapCache,standardFontDataCache:t.standardFontDataCache,globalColorSpaceCache:this.globalColorSpaceCache,globalImageCache:t.globalImageCache,systemFontCache:t.systemFontCache,nonBlendModesSet:t.nonBlendModesSet,xfaFactory:null}));this._pagePromises.set(e,n)}t.setActualNumPages(n.size)}}async fontFallback(e,t){const{catalog:a,pdfManager:r}=this;for(const i of await Promise.all(a.fontCache))if(i.loadedName===e){i.fallback(t,r.evaluatorOptions);return}}async cleanup(e=!1){return this.catalog?this.catalog.cleanup(e):clearGlobalCaches()}async#pe(e,t,a,r,i,n,s){const{xref:o}=this;if(!(a instanceof Ref)||n.has(a))return;n.put(a);const c=await o.fetchAsync(a);if(!(c instanceof Dict))return;let l=await c.getAsync("Subtype");l=l instanceof Name?l.name:null;if("Link"===l)return;if(c.has("T")){const t=stringToPDFString(await c.getAsync("T"));e=""===e?t:`${e}.${t}`}else{let a=c;for(;;){a=a.getRaw("Parent")||t;if(a instanceof Ref){if(n.has(a))break;a=await o.fetchAsync(a)}if(!(a instanceof Dict))break;if(a.has("T")){const t=stringToPDFString(await a.getAsync("T"));e=""===e?t:`${e}.${t}`;break}}}t&&!c.has("Parent")&&isName(c.get("Subtype"),"Widget")&&s.put(a,t);r.has(e)||r.set(e,[]);r.get(e).push(AnnotationFactory.create(o,a,i,null,!0,s,null).then((e=>e?.getFieldObject())).catch((function(e){warn(`#collectFieldObjects: "${e}".`);return null})));if(!c.has("Kids"))return;const h=await c.getAsync("Kids");if(Array.isArray(h))for(const t of h)await this.#pe(e,a,t,r,i,n,s)}get fieldObjects(){return shadow(this,"fieldObjects",this.pdfManager.ensureDoc("formInfo").then((async e=>{if(!e.hasFields)return null;const[t,a]=await Promise.all([this.pdfManager.ensureDoc("annotationGlobals"),this.pdfManager.ensureCatalog("acroForm")]);if(!t)return null;const r=new RefSet,i=Object.create(null),n=new Map,s=new RefSetCache;for(const e of await a.getAsync("Fields"))await this.#pe("",null,e,n,t,r,s);const o=[];for(const[e,t]of n)o.push(Promise.all(t).then((t=>{(t=t.filter((e=>!!e))).length>0&&(i[e]=t)})));await Promise.all(o);return{allFields:objectSize(i)>0?i:null,orphanFields:s}})))}get hasJSActions(){return shadow(this,"hasJSActions",this.pdfManager.ensureDoc("_parseHasJSActions"))}async _parseHasJSActions(){const[e,t]=await Promise.all([this.pdfManager.ensureCatalog("jsActions"),this.pdfManager.ensureDoc("fieldObjects")]);return!!e||!!t?.allFields&&Object.values(t.allFields).some((e=>e.some((e=>null!==e.actions))))}get calculationOrderIds(){const e=this.catalog.acroForm?.get("CO");if(!Array.isArray(e)||0===e.length)return shadow(this,"calculationOrderIds",null);const t=[];for(const a of e)a instanceof Ref&&t.push(a.toString());return shadow(this,"calculationOrderIds",t.length?t:null)}get annotationGlobals(){return shadow(this,"annotationGlobals",AnnotationFactory.createGlobals(this.pdfManager))}}class BasePdfManager{constructor({docBaseUrl:e,docId:t,enableXfa:a,evaluatorOptions:r,handler:i,password:n}){this._docBaseUrl=function parseDocBaseUrl(e){if(e){const t=createValidAbsoluteUrl(e);if(t)return t.href;warn(`Invalid absolute docBaseUrl: "${e}".`)}return null}(e);this._docId=t;this._password=n;this.enableXfa=a;r.isOffscreenCanvasSupported&&=FeatureTest.isOffscreenCanvasSupported;r.isImageDecoderSupported&&=FeatureTest.isImageDecoderSupported;this.evaluatorOptions=Object.freeze(r);ImageResizer.setOptions(r);JpegStream.setOptions(r);OperatorList.setOptions(r);const s={...r,handler:i};JpxImage.setOptions(s);IccColorSpace.setOptions(s);CmykICCBasedCS.setOptions(s)}get docId(){return this._docId}get password(){return this._password}get docBaseUrl(){return this._docBaseUrl}get catalog(){return this.pdfDocument.catalog}ensureDoc(e,t){return this.ensure(this.pdfDocument,e,t)}ensureXRef(e,t){return this.ensure(this.pdfDocument.xref,e,t)}ensureCatalog(e,t){return this.ensure(this.pdfDocument.catalog,e,t)}getPage(e){return this.pdfDocument.getPage(e)}fontFallback(e,t){return this.pdfDocument.fontFallback(e,t)}loadXfaFonts(e,t){return this.pdfDocument.loadXfaFonts(e,t)}loadXfaImages(){return this.pdfDocument.loadXfaImages()}serializeXfaData(e){return this.pdfDocument.serializeXfaData(e)}cleanup(e=!1){return this.pdfDocument.cleanup(e)}async ensure(e,t,a){unreachable("Abstract method `ensure` called")}requestRange(e,t){unreachable("Abstract method `requestRange` called")}requestLoadedStream(e=!1){unreachable("Abstract method `requestLoadedStream` called")}sendProgressiveData(e){unreachable("Abstract method `sendProgressiveData` called")}updatePassword(e){this._password=e}terminate(e){unreachable("Abstract method `terminate` called")}}class LocalPdfManager extends BasePdfManager{constructor(e){super(e);const t=new Stream(e.source);this.pdfDocument=new PDFDocument(this,t);this._loadedStreamPromise=Promise.resolve(t)}async ensure(e,t,a){const r=e[t];return"function"==typeof r?r.apply(e,a):r}requestRange(e,t){return Promise.resolve()}requestLoadedStream(e=!1){return this._loadedStreamPromise}terminate(e){}}class NetworkPdfManager extends BasePdfManager{constructor(e){super(e);this.streamManager=new ChunkedStreamManager(e.source,{msgHandler:e.handler,length:e.length,disableAutoFetch:e.disableAutoFetch,rangeChunkSize:e.rangeChunkSize});this.pdfDocument=new PDFDocument(this,this.streamManager.getStream())}async ensure(e,t,a){try{const r=e[t];return"function"==typeof r?r.apply(e,a):r}catch(r){if(!(r instanceof MissingDataException))throw r;await this.requestRange(r.begin,r.end);return this.ensure(e,t,a)}}requestRange(e,t){return this.streamManager.requestRange(e,t)}requestLoadedStream(e=!1){return this.streamManager.requestAllChunks(e)}sendProgressiveData(e){this.streamManager.onReceiveData({chunk:e})}terminate(e){this.streamManager.abort(e)}}const Ac=1,kc=2,Cc=1,vc=2,Fc=3,Ic=4,Oc=5,Tc=6,Mc=7,Dc=8;function onFn(){}function wrapReason(e){if(e instanceof AbortException||e instanceof InvalidPDFException||e instanceof PasswordException||e instanceof ResponseException||e instanceof UnknownErrorException)return e;e instanceof Error||"object"==typeof e&&null!==e||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(e.name){case"AbortException":return new AbortException(e.message);case"InvalidPDFException":return new InvalidPDFException(e.message);case"PasswordException":return new PasswordException(e.message,e.code);case"ResponseException":return new ResponseException(e.message,e.status,e.missing);case"UnknownErrorException":return new UnknownErrorException(e.message,e.details)}return new UnknownErrorException(e.message,e.toString())}class MessageHandler{#me=new AbortController;constructor(e,t,a){this.sourceName=e;this.targetName=t;this.comObj=a;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);a.addEventListener("message",this.#be.bind(this),{signal:this.#me.signal})}#be({data:e}){if(e.targetName!==this.sourceName)return;if(e.stream){this.#ye(e);return}if(e.callback){const t=e.callbackId,a=this.callbackCapabilities[t];if(!a)throw new Error(`Cannot resolve callback ${t}`);delete this.callbackCapabilities[t];if(e.callback===Ac)a.resolve(e.data);else{if(e.callback!==kc)throw new Error("Unexpected callback case");a.reject(wrapReason(e.reason))}return}const t=this.actionHandler[e.action];if(!t)throw new Error(`Unknown action from worker: ${e.action}`);if(e.callbackId){const a=this.sourceName,r=e.sourceName,i=this.comObj;Promise.try(t,e.data).then((function(t){i.postMessage({sourceName:a,targetName:r,callback:Ac,callbackId:e.callbackId,data:t})}),(function(t){i.postMessage({sourceName:a,targetName:r,callback:kc,callbackId:e.callbackId,reason:wrapReason(t)})}))}else e.streamId?this.#we(e):t(e.data)}on(e,t){const a=this.actionHandler;if(a[e])throw new Error(`There is already an actionName called "${e}"`);a[e]=t}send(e,t,a){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,data:t},a)}sendWithPromise(e,t,a){const r=this.callbackId++,i=Promise.withResolvers();this.callbackCapabilities[r]=i;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,callbackId:r,data:t},a)}catch(e){i.reject(e)}return i.promise}sendWithStream(e,t,a,r){const i=this.streamId++,n=this.sourceName,s=this.targetName,o=this.comObj;return new ReadableStream({start:a=>{const c=Promise.withResolvers();this.streamControllers[i]={controller:a,startCall:c,pullCall:null,cancelCall:null,isClosed:!1};o.postMessage({sourceName:n,targetName:s,action:e,streamId:i,data:t,desiredSize:a.desiredSize},r);return c.promise},pull:e=>{const t=Promise.withResolvers();this.streamControllers[i].pullCall=t;o.postMessage({sourceName:n,targetName:s,stream:Tc,streamId:i,desiredSize:e.desiredSize});return t.promise},cancel:e=>{assert(e instanceof Error,"cancel must have a valid reason");const t=Promise.withResolvers();this.streamControllers[i].cancelCall=t;this.streamControllers[i].isClosed=!0;o.postMessage({sourceName:n,targetName:s,stream:Cc,streamId:i,reason:wrapReason(e)});return t.promise}},a)}#we(e){const t=e.streamId,a=this.sourceName,r=e.sourceName,i=this.comObj,n=this,s=this.actionHandler[e.action],o={enqueue(e,n=1,s){if(this.isCancelled)return;const o=this.desiredSize;this.desiredSize-=n;if(o>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}i.postMessage({sourceName:a,targetName:r,stream:Ic,streamId:t,chunk:e},s)},close(){if(!this.isCancelled){this.isCancelled=!0;i.postMessage({sourceName:a,targetName:r,stream:Fc,streamId:t});delete n.streamSinks[t]}},error(e){assert(e instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;i.postMessage({sourceName:a,targetName:r,stream:Oc,streamId:t,reason:wrapReason(e)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:e.desiredSize,ready:null};o.sinkCapability.resolve();o.ready=o.sinkCapability.promise;this.streamSinks[t]=o;Promise.try(s,e.data,o).then((function(){i.postMessage({sourceName:a,targetName:r,stream:Dc,streamId:t,success:!0})}),(function(e){i.postMessage({sourceName:a,targetName:r,stream:Dc,streamId:t,reason:wrapReason(e)})}))}#ye(e){const t=e.streamId,a=this.sourceName,r=e.sourceName,i=this.comObj,n=this.streamControllers[t],s=this.streamSinks[t];switch(e.stream){case Dc:e.success?n.startCall.resolve():n.startCall.reject(wrapReason(e.reason));break;case Mc:e.success?n.pullCall.resolve():n.pullCall.reject(wrapReason(e.reason));break;case Tc:if(!s){i.postMessage({sourceName:a,targetName:r,stream:Mc,streamId:t,success:!0});break}s.desiredSize<=0&&e.desiredSize>0&&s.sinkCapability.resolve();s.desiredSize=e.desiredSize;Promise.try(s.onPull||onFn).then((function(){i.postMessage({sourceName:a,targetName:r,stream:Mc,streamId:t,success:!0})}),(function(e){i.postMessage({sourceName:a,targetName:r,stream:Mc,streamId:t,reason:wrapReason(e)})}));break;case Ic:assert(n,"enqueue should have stream controller");if(n.isClosed)break;n.controller.enqueue(e.chunk);break;case Fc:assert(n,"close should have stream controller");if(n.isClosed)break;n.isClosed=!0;n.controller.close();this.#Se(n,t);break;case Oc:assert(n,"error should have stream controller");n.controller.error(wrapReason(e.reason));this.#Se(n,t);break;case vc:e.success?n.cancelCall.resolve():n.cancelCall.reject(wrapReason(e.reason));this.#Se(n,t);break;case Cc:if(!s)break;const o=wrapReason(e.reason);Promise.try(s.onCancel||onFn,o).then((function(){i.postMessage({sourceName:a,targetName:r,stream:vc,streamId:t,success:!0})}),(function(e){i.postMessage({sourceName:a,targetName:r,stream:vc,streamId:t,reason:wrapReason(e)})}));s.sinkCapability.reject(o);s.isCancelled=!0;delete this.streamSinks[t];break;default:throw new Error("Unexpected stream case")}}async#Se(e,t){await Promise.allSettled([e.startCall?.promise,e.pullCall?.promise,e.cancelCall?.promise]);delete this.streamControllers[t]}destroy(){this.#me?.abort();this.#me=null}}async function writeObject(e,t,a,{encrypt:r=null}){const i=r?.createCipherTransform(e.num,e.gen);a.push(`${e.num} ${e.gen} obj\n`);t instanceof Dict?await writeDict(t,a,i):t instanceof BaseStream?await writeStream(t,a,i):(Array.isArray(t)||ArrayBuffer.isView(t))&&await writeArray(t,a,i);a.push("\nendobj\n")}async function writeDict(e,t,a){t.push("<<");for(const r of e.getKeys()){t.push(` /${escapePDFName(r)} `);await writeValue(e.getRaw(r),t,a)}t.push(">>")}async function writeStream(e,t,a){let r=e.getBytes();const{dict:i}=e,[n,s]=await Promise.all([i.getAsync("Filter"),i.getAsync("DecodeParms")]),o=isName(Array.isArray(n)?await i.xref.fetchIfRefAsync(n[0]):n,"FlateDecode");if(r.length>=256||o)try{const e=new CompressionStream("deflate"),t=e.writable.getWriter();await t.ready;t.write(r).then((async()=>{await t.ready;await t.close()})).catch((()=>{}));const a=await new Response(e.readable).arrayBuffer();r=new Uint8Array(a);let c,l;if(n){if(!o){c=Array.isArray(n)?[Name.get("FlateDecode"),...n]:[Name.get("FlateDecode"),n];s&&(l=Array.isArray(s)?[null,...s]:[null,s])}}else c=Name.get("FlateDecode");c&&i.set("Filter",c);l&&i.set("DecodeParms",l)}catch(e){info(`writeStream - cannot compress data: "${e}".`)}let c=bytesToString(r);a&&(c=a.encryptString(c));i.set("Length",c.length);await writeDict(i,t,a);t.push(" stream\n",c,"\nendstream")}async function writeArray(e,t,a){t.push("[");let r=!0;for(const i of e){r?r=!1:t.push(" ");await writeValue(i,t,a)}t.push("]")}async function writeValue(e,t,a){if(e instanceof Name)t.push(`/${escapePDFName(e.name)}`);else if(e instanceof Ref)t.push(`${e.num} ${e.gen} R`);else if(Array.isArray(e)||ArrayBuffer.isView(e))await writeArray(e,t,a);else if("string"==typeof e){a&&(e=a.encryptString(e));t.push(`(${escapeString(e)})`)}else"number"==typeof e?t.push(numberToString(e)):"boolean"==typeof e?t.push(e.toString()):e instanceof Dict?await writeDict(e,t,a):e instanceof BaseStream?await writeStream(e,t,a):null===e?t.push("null"):warn(`Unhandled value in writer: ${typeof e}, please file a bug.`)}function writeInt(e,t,a,r){for(let i=t+a-1;i>a-1;i--){r[i]=255&e;e>>=8}return a+t}function writeString(e,t,a){const r=e.length;for(let i=0;i1&&(n=a.documentElement.searchNode([i.at(-1)],0));n?n.childNodes=Array.isArray(r)?r.map((e=>new SimpleDOMNode("value",e))):[new SimpleDOMNode("#text",r)]:warn(`Node not found for path: ${t}`)}const r=[];a.documentElement.dump(r);return r.join("")}(r.fetchIfRef(t).getString(),a)}const i=new StringStream(e);i.dict=new Dict(r);i.dict.set("Type",Name.get("EmbeddedFile"));a.put(t,{data:i})}function getIndexes(e){const t=[];for(const{ref:a}of e)a.num===t.at(-2)+t.at(-1)?t[t.length-1]+=1:t.push(a.num,1);return t}function computeIDs(e,t,a){if(Array.isArray(t.fileIds)&&t.fileIds.length>0){const r=function computeMD5(e,t){const a=Math.floor(Date.now()/1e3),r=t.filename||"",i=[a.toString(),r,e.toString(),...t.infoMap.values()],n=Math.sumPrecise(i.map((e=>e.length))),s=new Uint8Array(n);let o=0;for(const e of i)o=writeString(e,o,s);return bytesToString(calculateMD5(s,0,s.length))}(e,t);a.set("ID",[t.fileIds[0],r])}}async function incrementalUpdate({originalData:e,xrefInfo:t,changes:a,xref:r=null,hasXfa:i=!1,xfaDatasetsRef:n=null,hasXfaDatasetsEntry:s=!1,needAppearances:o,acroFormRef:c=null,acroForm:l=null,xfaData:h=null,useXrefStream:u=!1}){await async function updateAcroform({xref:e,acroForm:t,acroFormRef:a,hasXfa:r,hasXfaDatasetsEntry:i,xfaDatasetsRef:n,needAppearances:s,changes:o}){!r||i||n||warn("XFA - Cannot save it");if(!s&&(!r||!n||i))return;const c=t.clone();if(r&&!i){const e=t.get("XFA").slice();e.splice(2,0,"datasets");e.splice(3,0,n);c.set("XFA",e)}s&&c.set("NeedAppearances",!0);o.put(a,{data:c})}({xref:r,acroForm:l,acroFormRef:c,hasXfa:i,hasXfaDatasetsEntry:s,xfaDatasetsRef:n,needAppearances:o,changes:a});i&&updateXFA({xfaData:h,xfaDatasetsRef:n,changes:a,xref:r});const d=function getTrailerDict(e,t,a){const r=new Dict(null);r.set("Prev",e.startXRef);const i=e.newRef;if(a){t.put(i,{data:""});r.set("Size",i.num+1);r.set("Type",Name.get("XRef"))}else r.set("Size",i.num);null!==e.rootRef&&r.set("Root",e.rootRef);null!==e.infoRef&&r.set("Info",e.infoRef);null!==e.encryptRef&&r.set("Encrypt",e.encryptRef);return r}(t,a,u),f=[],g=await async function writeChanges(e,t,a=[]){const r=[];for(const[i,{data:n}]of e.items())if(null!==n&&"string"!=typeof n){await writeObject(i,n,a,t);r.push({ref:i,data:a.join("")});a.length=0}else r.push({ref:i,data:n});return r.sort(((e,t)=>e.ref.num-t.ref.num))}(a,r,f);let p=e.length;const m=e.at(-1);if(10!==m&&13!==m){f.push("\n");p+=1}for(const{data:e}of g)null!==e&&f.push(e);await(u?async function getXRefStreamTable(e,t,a,r,i){const n=[];let s=0,o=0;for(const{ref:e,data:r}of a){let a;s=Math.max(s,t);if(null!==r){a=Math.min(e.gen,65535);n.push([1,t,a]);t+=r.length}else{a=Math.min(e.gen+1,65535);n.push([0,0,a])}o=Math.max(o,a)}r.set("Index",getIndexes(a));const c=[1,getSizeInBytes(s),getSizeInBytes(o)];r.set("W",c);computeIDs(t,e,r);const l=Math.sumPrecise(c),h=new Uint8Array(l*n.length),u=new Stream(h);u.dict=r;let d=0;for(const[e,t,a]of n){d=writeInt(e,c[0],d,h);d=writeInt(t,c[1],d,h);d=writeInt(a,c[2],d,h)}await writeObject(e.newRef,u,i,{});i.push("startxref\n",t.toString(),"\n%%EOF\n")}(t,p,g,d,f):async function getXRefTable(e,t,a,r,i){i.push("xref\n");const n=getIndexes(a);let s=0;for(const{ref:e,data:r}of a){if(e.num===n[s]){i.push(`${n[s]} ${n[s+1]}\n`);s+=2}if(null!==r){i.push(`${t.toString().padStart(10,"0")} ${Math.min(e.gen,65535).toString().padStart(5,"0")} n\r\n`);t+=r.length}else i.push(`0000000000 ${Math.min(e.gen+1,65535).toString().padStart(5,"0")} f\r\n`)}computeIDs(t,e,r);i.push("trailer\n");await writeDict(r,i);i.push("\nstartxref\n",t.toString(),"\n%%EOF\n")}(t,p,g,d,f));const b=e.length+Math.sumPrecise(f.map((e=>e.length))),y=new Uint8Array(b);y.set(e);let w=e.length;for(const e of f)w=writeString(e,w,y);return y}class PDFWorkerStream{constructor(e){this._msgHandler=e;this._contentLength=null;this._fullRequestReader=null;this._rangeRequestReaders=[]}getFullReader(){assert(!this._fullRequestReader,"PDFWorkerStream.getFullReader can only be called once.");this._fullRequestReader=new PDFWorkerStreamReader(this._msgHandler);return this._fullRequestReader}getRangeReader(e,t){const a=new PDFWorkerStreamRangeReader(e,t,this._msgHandler);this._rangeRequestReaders.push(a);return a}cancelAllRequests(e){this._fullRequestReader?.cancel(e);for(const t of this._rangeRequestReaders.slice(0))t.cancel(e)}}class PDFWorkerStreamReader{constructor(e){this._msgHandler=e;this.onProgress=null;this._contentLength=null;this._isRangeSupported=!1;this._isStreamingSupported=!1;const t=this._msgHandler.sendWithStream("GetReader");this._reader=t.getReader();this._headersReady=this._msgHandler.sendWithPromise("ReaderHeadersReady").then((e=>{this._isStreamingSupported=e.isStreamingSupported;this._isRangeSupported=e.isRangeSupported;this._contentLength=e.contentLength}))}get headersReady(){return this._headersReady}get contentLength(){return this._contentLength}get isStreamingSupported(){return this._isStreamingSupported}get isRangeSupported(){return this._isRangeSupported}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class PDFWorkerStreamRangeReader{constructor(e,t,a){this._msgHandler=a;this.onProgress=null;const r=this._msgHandler.sendWithStream("GetRangeReader",{begin:e,end:t});this._reader=r.getReader()}get isStreamingSupported(){return!1}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class WorkerTask{constructor(e){this.name=e;this.terminated=!1;this._capability=Promise.withResolvers()}get finished(){return this._capability.promise}finish(){this._capability.resolve()}terminate(){this.terminated=!0}ensureNotTerminated(){if(this.terminated)throw new Error("Worker task was terminated")}}class WorkerMessageHandler{static{"undefined"==typeof window&&!e&&"undefined"!=typeof self&&"function"==typeof self.postMessage&&"onmessage"in self&&this.initializeFromPort(self)}static setup(e,t){let a=!1;e.on("test",(t=>{if(!a){a=!0;e.send("test",t instanceof Uint8Array)}}));e.on("configure",(e=>{!function setVerbosityLevel(e){Number.isInteger(e)&&(da=e)}(e.verbosity)}));e.on("GetDocRequest",(e=>this.createDocumentHandler(e,t)))}static createDocumentHandler(e,t){let a,r=!1,i=null;const n=new Set,s=getVerbosityLevel(),{docId:o,apiVersion:c}=e,l="5.2.133";if(c!==l)throw new Error(`The API version "${c}" does not match the Worker version "${l}".`);const buildMsg=(e,t)=>`The \`${e}.prototype\` contains unexpected enumerable property "${t}", thus breaking e.g. \`for...in\` iteration of ${e}s.`;for(const e in{})throw new Error(buildMsg("Object",e));for(const e in[])throw new Error(buildMsg("Array",e));const h=o+"_worker";let u=new MessageHandler(h,o,t);function ensureNotTerminated(){if(r)throw new Error("Worker was terminated")}function startWorkerTask(e){n.add(e)}function finishWorkerTask(e){e.finish();n.delete(e)}async function loadDocument(e){await a.ensureDoc("checkHeader");await a.ensureDoc("parseStartXRef");await a.ensureDoc("parse",[e]);await a.ensureDoc("checkFirstPage",[e]);await a.ensureDoc("checkLastPage",[e]);const t=await a.ensureDoc("isPureXfa");if(t){const e=new WorkerTask("loadXfaFonts");startWorkerTask(e);await Promise.all([a.loadXfaFonts(u,e).catch((e=>{})).then((()=>finishWorkerTask(e))),a.loadXfaImages()])}const[r,i]=await Promise.all([a.ensureDoc("numPages"),a.ensureDoc("fingerprints")]);return{numPages:r,fingerprints:i,htmlForXfa:t?await a.ensureDoc("htmlForXfa"):null}}function setupDoc(e){function onSuccess(e){ensureNotTerminated();u.send("GetDoc",{pdfInfo:e})}function onFailure(e){ensureNotTerminated();if(e instanceof PasswordException){const t=new WorkerTask(`PasswordException: response ${e.code}`);startWorkerTask(t);u.sendWithPromise("PasswordRequest",e).then((function({password:e}){finishWorkerTask(t);a.updatePassword(e);pdfManagerReady()})).catch((function(){finishWorkerTask(t);u.send("DocException",e)}))}else u.send("DocException",wrapReason(e))}function pdfManagerReady(){ensureNotTerminated();loadDocument(!1).then(onSuccess,(function(e){ensureNotTerminated();e instanceof XRefParseException?a.requestLoadedStream().then((function(){ensureNotTerminated();loadDocument(!0).then(onSuccess,onFailure)})):onFailure(e)}))}ensureNotTerminated();(async function getPdfManager({data:e,password:t,disableAutoFetch:a,rangeChunkSize:r,length:n,docBaseUrl:s,enableXfa:c,evaluatorOptions:l}){const h={source:null,disableAutoFetch:a,docBaseUrl:s,docId:o,enableXfa:c,evaluatorOptions:l,handler:u,length:n,password:t,rangeChunkSize:r};if(e){h.source=e;return new LocalPdfManager(h)}const d=new PDFWorkerStream(u),f=d.getFullReader(),g=Promise.withResolvers();let p,m=[],b=0;f.headersReady.then((function(){if(f.isRangeSupported){h.source=d;h.length=f.contentLength;h.disableAutoFetch||=f.isStreamingSupported;p=new NetworkPdfManager(h);for(const e of m)p.sendProgressiveData(e);m=[];g.resolve(p);i=null}})).catch((function(e){g.reject(e);i=null}));new Promise((function(e,t){const readChunk=function({value:e,done:a}){try{ensureNotTerminated();if(a){if(!p){const e=arrayBuffersToBytes(m);m=[];n&&e.length!==n&&warn("reported HTTP length is different from actual");h.source=e;p=new LocalPdfManager(h);g.resolve(p)}i=null;return}b+=e.byteLength;f.isStreamingSupported||u.send("DocProgress",{loaded:b,total:Math.max(b,f.contentLength||0)});p?p.sendProgressiveData(e):m.push(e);f.read().then(readChunk,t)}catch(e){t(e)}};f.read().then(readChunk,t)})).catch((function(e){g.reject(e);i=null}));i=e=>{d.cancelAllRequests(e)};return g.promise})(e).then((function(e){if(r){e.terminate(new AbortException("Worker was terminated."));throw new Error("Worker was terminated")}a=e;a.requestLoadedStream(!0).then((e=>{u.send("DataLoaded",{length:e.bytes.byteLength})}))})).then(pdfManagerReady,onFailure)}u.on("GetPage",(function(e){return a.getPage(e.pageIndex).then((function(e){return Promise.all([a.ensure(e,"rotate"),a.ensure(e,"ref"),a.ensure(e,"userUnit"),a.ensure(e,"view")]).then((function([e,t,a,r]){return{rotate:e,ref:t,refStr:t?.toString()??null,userUnit:a,view:r}}))}))}));u.on("GetPageIndex",(function(e){const t=Ref.get(e.num,e.gen);return a.ensureCatalog("getPageIndex",[t])}));u.on("GetDestinations",(function(e){return a.ensureCatalog("destinations")}));u.on("GetDestination",(function(e){return a.ensureCatalog("getDestination",[e.id])}));u.on("GetPageLabels",(function(e){return a.ensureCatalog("pageLabels")}));u.on("GetPageLayout",(function(e){return a.ensureCatalog("pageLayout")}));u.on("GetPageMode",(function(e){return a.ensureCatalog("pageMode")}));u.on("GetViewerPreferences",(function(e){return a.ensureCatalog("viewerPreferences")}));u.on("GetOpenAction",(function(e){return a.ensureCatalog("openAction")}));u.on("GetAttachments",(function(e){return a.ensureCatalog("attachments")}));u.on("GetDocJSActions",(function(e){return a.ensureCatalog("jsActions")}));u.on("GetPageJSActions",(function({pageIndex:e}){return a.getPage(e).then((e=>a.ensure(e,"jsActions")))}));u.on("GetOutline",(function(e){return a.ensureCatalog("documentOutline")}));u.on("GetOptionalContentConfig",(function(e){return a.ensureCatalog("optionalContentConfig")}));u.on("GetPermissions",(function(e){return a.ensureCatalog("permissions")}));u.on("GetMetadata",(function(e){return Promise.all([a.ensureDoc("documentInfo"),a.ensureCatalog("metadata")])}));u.on("GetMarkInfo",(function(e){return a.ensureCatalog("markInfo")}));u.on("GetData",(function(e){return a.requestLoadedStream().then((e=>e.bytes))}));u.on("GetAnnotations",(function({pageIndex:e,intent:t}){return a.getPage(e).then((function(a){const r=new WorkerTask(`GetAnnotations: page ${e}`);startWorkerTask(r);return a.getAnnotationsData(u,r,t).then((e=>{finishWorkerTask(r);return e}),(e=>{finishWorkerTask(r);throw e}))}))}));u.on("GetFieldObjects",(function(e){return a.ensureDoc("fieldObjects").then((e=>e?.allFields||null))}));u.on("HasJSActions",(function(e){return a.ensureDoc("hasJSActions")}));u.on("GetCalculationOrderIds",(function(e){return a.ensureDoc("calculationOrderIds")}));u.on("SaveDocument",(async function({isPureXfa:e,numPages:t,annotationStorage:r,filename:i}){const n=[a.requestLoadedStream(),a.ensureCatalog("acroForm"),a.ensureCatalog("acroFormRef"),a.ensureDoc("startXRef"),a.ensureDoc("xref"),a.ensureDoc("linearization"),a.ensureCatalog("structTreeRoot")],s=new RefSetCache,o=[],c=e?null:getNewAnnotationsMap(r),[l,h,d,f,g,p,m]=await Promise.all(n),b=g.trailer.getRaw("Root")||null;let y;if(c){m?await m.canUpdateStructTree({pdfManager:a,newAnnotationsByPage:c})&&(y=m):await StructTreeRoot.canCreateStructureTree({catalogRef:b,pdfManager:a,newAnnotationsByPage:c})&&(y=null);const e=AnnotationFactory.generateImages(r.values(),g,a.evaluatorOptions.isOffscreenCanvasSupported),t=void 0===y?o:[];for(const[r,i]of c)t.push(a.getPage(r).then((t=>{const a=new WorkerTask(`Save (editor): page ${r}`);startWorkerTask(a);return t.saveNewAnnotations(u,a,i,e,s).finally((function(){finishWorkerTask(a)}))})));null===y?o.push(Promise.all(t).then((async()=>{await StructTreeRoot.createStructureTree({newAnnotationsByPage:c,xref:g,catalogRef:b,pdfManager:a,changes:s})}))):y&&o.push(Promise.all(t).then((async()=>{await y.updateStructureTree({newAnnotationsByPage:c,pdfManager:a,changes:s})})))}if(e)o.push(a.serializeXfaData(r));else for(let e=0;ee.needAppearances)),k=h instanceof Dict&&h.get("XFA")||null;let C=null,v=!1;if(Array.isArray(k)){for(let e=0,t=k.length;e{g.resetNewTemporaryRef()}))}));u.on("GetOperatorList",(function(e,t){const r=e.pageIndex;a.getPage(r).then((function(a){const i=new WorkerTask(`GetOperatorList: page ${r}`);startWorkerTask(i);const n=s>=Ae?Date.now():0;a.getOperatorList({handler:u,sink:t,task:i,intent:e.intent,cacheKey:e.cacheKey,annotationStorage:e.annotationStorage,modifiedIds:e.modifiedIds}).then((function(e){finishWorkerTask(i);n&&info(`page=${r+1} - getOperatorList: time=${Date.now()-n}ms, len=${e.length}`);t.close()}),(function(e){finishWorkerTask(i);i.terminated||t.error(e)}))}))}));u.on("GetTextContent",(function(e,t){const{pageIndex:r,includeMarkedContent:i,disableNormalization:n}=e;a.getPage(r).then((function(e){const a=new WorkerTask("GetTextContent: page "+r);startWorkerTask(a);const o=s>=Ae?Date.now():0;e.extractTextContent({handler:u,task:a,sink:t,includeMarkedContent:i,disableNormalization:n}).then((function(){finishWorkerTask(a);o&&info(`page=${r+1} - getTextContent: time=`+(Date.now()-o)+"ms");t.close()}),(function(e){finishWorkerTask(a);a.terminated||t.error(e)}))}))}));u.on("GetStructTree",(function(e){return a.getPage(e.pageIndex).then((e=>a.ensure(e,"getStructTree")))}));u.on("FontFallback",(function(e){return a.fontFallback(e.id,u)}));u.on("Cleanup",(function(e){return a.cleanup(!0)}));u.on("Terminate",(function(e){r=!0;const t=[];if(a){a.terminate(new AbortException("Worker was terminated."));const e=a.cleanup();t.push(e);a=null}else clearGlobalCaches();i?.(new AbortException("Worker was terminated."));for(const e of n){t.push(e.finished);e.terminate()}return Promise.all(t).then((function(){u.destroy();u=null}))}));u.on("Ready",(function(t){setupDoc(e);e=null}));return h}static initializeFromPort(e){const t=new MessageHandler("worker","main",e);this.setup(t,e);t.send("ready",null)}}globalThis.pdfjsWorker={WorkerMessageHandler};export{WorkerMessageHandler}; \ No newline at end of file diff --git a/src/configs/agenda-config.json b/src/configs/agenda-config.json index 960b0ac..89bea0a 100644 --- a/src/configs/agenda-config.json +++ b/src/configs/agenda-config.json @@ -1,3 +1,3 @@ { "discardedItems": [] -} +} \ No newline at end of file diff --git a/src/configs/config-manager.js b/src/configs/config-manager.js index b76bad5..c2aa18e 100644 --- a/src/configs/config-manager.js +++ b/src/configs/config-manager.js @@ -12,21 +12,16 @@ if (!fs.existsSync(configFilePath)) { "current": "blocks" }, "modules": { - "blogsMod": "on", - "housingMod": "off", - "karvanMod": "on", "popularMod": "on", "topicsMod": "on", "summariesMod": "on", "latestMod": "on", "threadsMod": "on", "multiverseMod": "on", - "pollsMod": "on", "fediverseMod": "off", "invitesMod": "on", "walletMod": "on", "legacyMod": "on", - "devMod": "on", "cipherMod": "on", "bookmarksMod": "on", "videosMod": "on", @@ -56,7 +51,6 @@ if (!fs.existsSync(configFilePath)) { "jobsMod": "on", "shopsMod": "on", "projectsMod": "on", - "industryMod": "on", "bankingMod": "on", "parliamentMod": "on", "courtsMod": "on", @@ -84,6 +78,7 @@ if (!fs.existsSync(configFilePath)) { "limit": 2000 }, "homePage": "activity", + "bottomBarPins": ["search", "inbox", "publish"], "language": "en", "wish": "whole", "pmVisibility": "whole", @@ -100,10 +95,8 @@ const getConfig = () => { if (typeof cfg.ux === 'string') cfg.ux = { current: cfg.ux }; if (!cfg.ux || typeof cfg.ux !== 'object') cfg.ux = { current: 'blocks' }; if (cfg.ux.current === 'menus') cfg.ux.current = 'blocks'; - if (cfg.ux.current !== 'blocks' && cfg.ux.current !== 'ainav' && cfg.ux.current !== 'chats') cfg.ux.current = 'blocks'; + if (cfg.ux.current !== 'blocks' && cfg.ux.current !== 'ainav') cfg.ux.current = 'blocks'; if (cfg.ux.current === 'ainav' && cfg.modules && cfg.modules.aiNavMod !== 'on') cfg.ux.current = 'blocks'; - if (cfg.ux.current === 'chats' && cfg.modules && cfg.modules.chatsMod !== 'on') cfg.ux.current = 'blocks'; - let pins = Array.isArray(cfg.bottomBarPins) ? cfg.bottomBarPins : (typeof cfg.bottomBarPin === 'string' diff --git a/src/configs/content_favorites.json b/src/configs/content_favorites.json deleted file mode 100644 index c036334..0000000 --- a/src/configs/content_favorites.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "audios": [], - "blogs": [], - "bookmarks": [], - "calendars": [], - "chats": [], - "documents": [], - "events": [], - "forum": [], - "housing": [], - "images": [], - "jobs": [], - "logs": [], - "maps": [], - "market": [], - "pads": [], - "polls": [], - "projects": [], - "reports": [], - "shopProducts": [], - "shops": [], - "tasks": [], - "torrents": [], - "transfers": [], - "videos": [], - "votes": [] -} diff --git a/src/configs/media-favorites.json b/src/configs/media-favorites.json new file mode 100644 index 0000000..8c9d7c2 --- /dev/null +++ b/src/configs/media-favorites.json @@ -0,0 +1,13 @@ +{ + "audios": [], + "bookmarks": [], + "calendars": [], + "chats": [], + "documents": [], + "images": [], + "maps": [], + "pads": [], + "shops": [], + "torrents": [], + "videos": [] +} diff --git a/src/configs/oasis-config.json b/src/configs/oasis-config.json index ad8f220..4e835c6 100644 --- a/src/configs/oasis-config.json +++ b/src/configs/oasis-config.json @@ -6,14 +6,16 @@ "current": "blocks" }, "modules": { - "blogsMod": "on", - "pollsMod": "on", + "popularMod": "on", + "topicsMod": "on", + "summariesMod": "on", + "latestMod": "on", + "threadsMod": "on", + "multiverseMod": "on", "fediverseMod": "on", "invitesMod": "on", "walletMod": "off", - "legacyMod": "on", - "devMod": "off", - "industryMod": "on", + "legacyMod": "off", "cipherMod": "on", "bookmarksMod": "on", "videosMod": "on", @@ -30,7 +32,6 @@ "reportsMod": "on", "opinionsMod": "on", "padsMod": "on", - "karvanMod": "on", "calendarsMod": "on", "transfersMod": "off", "feedMod": "on", @@ -53,8 +54,7 @@ "chatsMod": "on", "torrentsMod": "off", "graphosMod": "on", - "larpMod": "on", - "housingMod": "off" + "larpMod": "on" }, "wallet": { "url": "http://localhost:7474", @@ -75,20 +75,5 @@ "language": "en", "wish": "whole", "pmVisibility": "whole", - "lanBroadcasting": true, - "bottomBarPins": [ - "search", - "inbox", - "publish", - "activity" - ], - "rtc": { - "stun": [], - "turn": { - "urls": [], - "secret": "", - "ttlSec": 3600 - }, - "relayOnly": false - } + "lanBroadcasting": true } \ No newline at end of file diff --git a/src/configs/shared-state.js b/src/configs/shared-state.js index 75ef3b3..dfed74b 100644 --- a/src/configs/shared-state.js +++ b/src/configs/shared-state.js @@ -9,9 +9,6 @@ let _ecoValue = null; let _lastActivity = null; let _maxBlockBytes = 0; let _inhabitantCount = 0; -let _mentionsCount = 0; -let _bestMatch = null; -let _dismissedSuggestion = null; module.exports = { getInboxCount: () => _inboxCount, setInboxCount: (n) => { _inboxCount = n; }, @@ -33,12 +30,6 @@ module.exports = { setLastActivity: (a) => { _lastActivity = a; }, getMaxBlockBytes: () => _maxBlockBytes, setMaxBlockBytes: (n) => { if (Number(n) > _maxBlockBytes) _maxBlockBytes = Number(n); }, - getMentionsCount: () => _mentionsCount, - setMentionsCount: (n) => { _mentionsCount = Math.max(0, Number(n) || 0); }, getInhabitantCount: () => _inhabitantCount, - setInhabitantCount: (n) => { _inhabitantCount = Math.max(0, Number(n) || 0); }, - getBestMatch: () => _bestMatch, - setBestMatch: (m) => { _bestMatch = m || null; }, - getDismissedSuggestion: () => _dismissedSuggestion, - setDismissedSuggestion: (href) => { _dismissedSuggestion = href || null; } + setInhabitantCount: (n) => { _inhabitantCount = Math.max(0, Number(n) || 0); } }; diff --git a/src/maps/map_renderer.js b/src/maps/map_renderer.js index d1e56e3..0a63299 100644 --- a/src/maps/map_renderer.js +++ b/src/maps/map_renderer.js @@ -81,21 +81,14 @@ pins = json.loads(sys.argv[1]) im = Image.open(sys.argv[2]).copy() draw = ImageDraw.Draw(im) -def draw_pin(draw, x, y, main): - sw = 7 if main else 5 - sh = 34 if main else 26 - head = 11 if main else 8 +for p in pins: + x, y, main = p['x'], p['y'], p.get('main', False) + sw = 3 if main else 2 + sh = 18 if main else 13 clr = '#e74c3c' if main else '#3498db' dark = '#c0392b' if main else '#2980b9' - cy = y - sh - draw.polygon([(x, y + 2), (x - sw - 1, cy + sw), (x + sw + 1, cy + sw)], fill='white') - draw.polygon([(x, y), (x - sw, cy + sw), (x + sw, cy + sw)], fill=clr) - draw.ellipse([x - head - 2, cy - head - 2, x + head + 2, cy + head + 2], fill='white') - draw.ellipse([x - head, cy - head, x + head, cy + head], fill=clr, outline=dark, width=2) - draw.ellipse([x - head // 3, cy - head // 3, x + head // 3, cy + head // 3], fill='white') - -for p in pins: - draw_pin(draw, p['x'], p['y'], p.get('main', False)) + draw.polygon([(x, y + 2), (x - sw, y - sh + sw * 2), (x + sw, y - sh + sw * 2)], fill=clr) + draw.ellipse([x - sw - 1, y - sh - sw, x + sw + 1, y - sh + sw], fill=dark, outline='white', width=1) im.save(sys.argv[3], optimize=True) `; @@ -200,23 +193,15 @@ cropped = canvas.crop((int(crop_x), int(crop_y), int(crop_x + vw), int(crop_y + result = cropped.resize((1024, 1024), Image.LANCZOS) draw = ImageDraw.Draw(result) -def draw_pin(draw, x, y, main): - sw = 7 if main else 5 - sh = 34 if main else 26 - head = 11 if main else 8 - clr = '#e74c3c' if main else '#3498db' - dark = '#c0392b' if main else '#2980b9' - cy = y - sh - draw.polygon([(x, y + 2), (x - sw - 1, cy + sw), (x + sw + 1, cy + sw)], fill='white') - draw.polygon([(x, y), (x - sw, cy + sw), (x + sw, cy + sw)], fill=clr) - draw.ellipse([x - head - 2, cy - head - 2, x + head + 2, cy + head + 2], fill='white') - draw.ellipse([x - head, cy - head, x + head, cy + head], fill=clr, outline=dark, width=2) - draw.ellipse([x - head // 3, cy - head // 3, x + head // 3, cy + head // 3], fill='white') - for p in pins: px, py, main = p['px'], p['py'], p.get('main', False) - if -40 <= px <= 1064 and -40 <= py <= 1064: - draw_pin(draw, px, py, main) + if -20 <= px <= 1044 and -20 <= py <= 1044: + sw = 3 if main else 2 + sh = 18 if main else 13 + clr = '#e74c3c' if main else '#3498db' + dark = '#c0392b' if main else '#2980b9' + draw.polygon([(px, py + 2), (px - sw, py - sh + sw * 2), (px + sw, py - sh + sw * 2)], fill=clr) + draw.ellipse([px - sw - 1, py - sh - sw, px + sw + 1, py - sh + sw], fill=dark, outline='white', width=1) result.save(out_file, optimize=True) `; diff --git a/src/models/activity_model.js b/src/models/activity_model.js index b70963a..6b51862 100644 --- a/src/models/activity_model.js +++ b/src/models/activity_model.js @@ -82,7 +82,7 @@ const HIDDEN_ENVELOPE_TYPES = new Set([ 'courts-key' ]); -module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel }) => { +module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }; @@ -143,74 +143,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel } return t.length > max ? t.slice(0, max - 1) + '…' : t; }; - // Builds self-contained chatThread items (title + recent messages embedded) for - // general (non-tribe) OPEN chats that have activity. Emitted as one item per chat - // with ts = latest message, so it survives the recent/all/mine filters intact. - const CHAT_THREAD_LIMIT = 6; - const buildChatThreads = async (ssbClient, idToAction, rootOf) => { - const replacedChatIds = new Set(); - for (const a of idToAction.values()) { - if (a.type !== 'chat') continue; - const rep = a.content && a.content.replaces; - if (rep) replacedChatIds.add(rep); - } - const stateByRoot = new Map(); - for (const a of idToAction.values()) { - if (a.type !== 'chat') continue; - const root = rootOf(a.id); - const isTip = !replacedChatIds.has(a.id); - const prev = stateByRoot.get(root); - // Resolve the chain tip by replaces (a non-replaced action wins), breaking ts - // ties correctly — the mock and real SSB can stamp equal timestamps. - if (prev && !((isTip && !prev.isTip) || (isTip === prev.isTip && (a.ts || 0) >= prev.ts))) continue; - const cc = a.content || {}; - stateByRoot.set(root, { ts: a.ts || 0, isTip, status: String(cc.status || '').toUpperCase(), tribeId: cc.tribeId || null, title: String(cc.title || ''), description: String(cc.description || ''), members: Array.isArray(cc.members) ? cc.members.length : 0 }); - } - const msgsByRoot = new Map(); - for (const a of idToAction.values()) { - if (a.type !== 'chatMessage') continue; - const c = a.content || {}; - if (c.tribeId || c.encryptedText) continue; - const root = c.chatId; - if (!root || typeof c.text !== 'string' || !c.text) continue; - if (!msgsByRoot.has(root)) msgsByRoot.set(root, []); - msgsByRoot.get(root).push(a); - } - for (const root of msgsByRoot.keys()) { - if (stateByRoot.has(root)) continue; - const val = await getMsg(ssbClient, root); - const cc = val && val.content; - if (cc && typeof cc === 'object' && cc.type === 'chat') { - stateByRoot.set(root, { ts: 0, status: String(cc.status || '').toUpperCase(), tribeId: cc.tribeId || null, title: String(cc.title || ''), description: String(cc.description || ''), members: Array.isArray(cc.members) ? cc.members.length : 0 }); - } else { - stateByRoot.set(root, null); - } - } - const items = []; - for (const [root, msgs] of msgsByRoot.entries()) { - const info = stateByRoot.get(root); - if (!info || info.tribeId || info.status !== 'OPEN') continue; - const asc = msgs.slice().sort((x, y) => (x.ts || 0) - (y.ts || 0)); - const last = asc[asc.length - 1]; - items.push({ - id: `chatthread:${root}`, - type: 'chatThread', - author: last.author, - ts: last.ts || 0, - content: { - type: 'chatThread', - chatRoot: root, - title: info.title || '', - description: info.description || '', - members: info.members || 0, - messageCount: asc.length, - replies: asc.slice(-CHAT_THREAD_LIMIT).map(m => ({ id: m.id, author: m.author, ts: m.ts || 0, text: (m.content && m.content.text) || '' })) - } - }); - } - return items; - }; - return { invalidateCache() { _feedCache = null; @@ -253,21 +185,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel } const idToAction = new Map(); const rawById = new Map(); const voteTally = buildVoteTally(results); - const pollTally = new Map(); - for (const m of results) { - const c = m && m.value && m.value.content; - if (!c || c.type !== 'pollVote' || typeof c.target !== 'string') continue; - if (!Array.isArray(c.choices)) continue; - const entry = pollTally.get(c.target) || { counts: {}, voters: new Map() }; - entry.voters.set(m.value.author, { ts: m.value.timestamp || 0, choices: c.choices }); - pollTally.set(c.target, entry); - } - for (const entry of pollTally.values()) { - entry.counts = {}; - for (const v of entry.voters.values()) { - for (const choice of v.choices) entry.counts[choice] = (entry.counts[choice] || 0) + 1; - } - } const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); @@ -339,7 +256,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel } const target = String(c.target || ''); const author = v.author; if (!target || !author) continue; - const key = `${target}\u0000${author}`; + const key = `${target}${author}`; const prev = aiHelpfulSeen.get(key); const curTs = v.timestamp || 0; if (!prev || curTs > prev.ts) aiHelpfulSeen.set(key, { target, ts: curTs, helpful: c.helpful !== false }); @@ -574,7 +491,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel } if (tombstoned.has(a.id)) continue; if (a.type === 'tribe' && parentOf.has(a.id)) continue; const c = a.content || {}; - if (c.root && tombstoned.has(c.root) && !c.replaces) continue; + if (c.root && tombstoned.has(c.root)) continue; if (a.type === 'vote' && tombstoned.has(c.vote?.link)) continue; if (a.type === 'spread' && (c.spreadTargetId || c.vote?.link) && tombstoned.has(c.spreadTargetId || c.vote?.link)) continue; if (c.key && tombstoned.has(c.key)) continue; @@ -594,11 +511,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel } const actionRoot = rootOf(a.id); const extra = a.type === 'aiExchange' ? { helpfulVotes: aiHelpfulCounts.get(a.id) || 0 } : {}; const voteAgg = a.type === 'votes' ? voteTally.get(a.id) : null; - if (a.type === 'poll') { - const tally = pollTally.get(a.id) || pollTally.get(actionRoot); - a.pollCounts = tally ? tally.counts : {}; - a.pollVoters = tally ? tally.voters.size : 0; - } let content = voteAgg ? { ...c, ...voteAgg } : a.content; if (a.type === 'feed') { const base = a.content || {}; @@ -686,37 +598,11 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel } deduped = Array.from(byKey.values()).map(x => { delete x.__effTs; delete x.__hasImage; return x }); - if (industryModel && deduped.some(a => a.type === 'industryBuild')) { - const builds = await industryModel.listAllBuilds().catch(() => []); - const buildStatusById = new Map(); - for (const b of (builds || [])) buildStatusById.set(b.id || b.key, String(b.status || 'PROPOSED').toUpperCase()); - deduped = deduped.filter(a => { - if (a.type !== 'industryBuild') return true; - const st = buildStatusById.get(a.id); - if (st === undefined || st === 'PROPOSED' || st === 'REJECTED') return false; - a.content = { ...a.content, buildStatus: st }; - return true; - }); - } - - if (industryModel && deduped.some(a => a.type === 'industryBlueprint')) { - const bps = await industryModel.listAllBlueprints().catch(() => []); - const bpById = new Map((bps || []).map(b => [b.id || b.key, b])); - for (const a of deduped) { - if (a.type !== 'industryBlueprint') continue; - const b = bpById.get(a.id); - if (b && b.estTotal != null) a.content = { ...a.content, estTotal: b.estTotal }; - } - } - const tribeInternalTypes = new Set(['tribe-content', 'tribeParliamentCandidature', 'tribeParliamentTerm', 'tribeParliamentProposal', 'tribeParliamentRule', 'tribeParliamentLaw', 'tribeParliamentRevocation']); - const hiddenTypes = new Set(['padEntry', 'chatMessage', 'calendarDate', 'calendarNote', 'calendarReminderSent', 'taskReminderSent', 'feed-action', 'pubBalance', 'pubAvailability', 'log', 'logPublic', 'gameScore', 'pollVote', 'pollClose', 'pollOpinion', 'curriculum']); - const chatThreadItems = await buildChatThreads(ssbClient, idToAction, rootOf, deduped); - deduped = deduped.concat(chatThreadItems); + const hiddenTypes = new Set(['padEntry', 'chatMessage', 'calendarDate', 'calendarNote', 'calendarReminderSent', 'taskReminderSent', 'feed-action', 'pubBalance', 'pubAvailability', 'log', 'gameScore']); const isAllowedTribeActivity = (a) => { if (tribeInternalTypes.has(a.type)) return false; const c = a.content || {}; - if (a.type === 'poll' && c.chatId) return false; if (c.tribeId) return false; if (a.type === 'tribe') { const isInitial = !c.replaces; @@ -727,8 +613,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel } } return true; }; - const itemVisible = (a, opts = {}) => { - if (hiddenTypes.has(a.type) && !(opts.allowHidden && opts.allowHidden.has(a.type))) return false; + const itemVisible = (a) => { + if (hiddenTypes.has(a.type)) return false; const c = a.content || {}; if (c.encryptedPayload) return false; if (a.type === 'pad' && c.status !== 'OPEN') return false; @@ -738,16 +624,14 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel } if (a.type === 'task' && String(c.isPublic || '').toUpperCase() === 'PRIVATE' && a.author !== userId && !(Array.isArray(c.assignees) && c.assignees.includes(userId))) return false; if (a.type === 'forum' && c.isPrivate === true && a.author !== userId) return false; if (a.type === 'job' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId && !(Array.isArray(c.subscribers) && c.subscribers.includes(userId))) return false; - if (a.type === 'housing' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId) return false; if (a.type === 'market' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && c.seller !== userId) return false; if (a.type === 'shop' && String(c.visibility || '').toUpperCase() === 'CLOSED' && a.author !== userId) return false; if (a.type === 'curriculum' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && a.author !== userId) return false; if (a.type === 'shopProduct' && c.shopVisibility && String(c.shopVisibility).toUpperCase() === 'CLOSED' && a.author !== userId) return false; - if (a.type === 'transfer' && String(c.status || '').toUpperCase() === 'UNCONFIRMED' && a.author !== userId && c.from !== userId && c.to !== userId) return false; return true; }; - const isVisible = (a, opts) => { - if (!itemVisible(a, opts)) return false; + const isVisible = (a) => { + if (!itemVisible(a)) return false; if (a.type === 'post' || a.type === 'opinion') { const c = a.content || {}; const ref = (typeof c.root === 'string' && c.root) @@ -766,14 +650,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel } if (filter === 'mine') out = deduped.filter(a => a.author === userId && isAllowedTribeActivity(a) && isVisible(a)); else if (filter === 'recent') { const cutoff = Date.now() - 24 * 60 * 60 * 1000; out = deduped.filter(a => (a.ts || 0) >= cutoff && isAllowedTribeActivity(a) && isVisible(a)) } else if (filter === 'all') out = deduped.filter(a => isAllowedTribeActivity(a) && isVisible(a)); - else if (filter === 'top') { - const visible = deduped.filter(a => isAllowedTribeActivity(a) && isVisible(a)); - const perAuthor = new Map(); - for (const a of visible) perAuthor.set(a.author, (perAuthor.get(a.author) || 0) + 1); - out = visible - .map(a => ({ ...a, authorActions: perAuthor.get(a.author) || 0 })) - .sort((x, y) => (y.authorActions - x.authorActions) || ((y.ts || 0) - (x.ts || 0))); - } else if (filter === 'banking') out = deduped.filter(a => a.type === 'bankWallet' || a.type === 'bankClaim' || a.type === 'ubiClaim'); else if (filter === 'tribe') out = deduped.filter(a => a.type === 'tribe' || String(a.type || '').startsWith('tribe')); else if (filter === 'spread') out = deduped.filter(a => a.type === 'spread'); @@ -788,17 +664,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel } }); else if (filter === 'task') out = deduped.filter(a => a.type === 'task' || a.type === 'taskAssignment'); - else if (filter === 'votes') - out = deduped.filter(a => (a.type === 'votes' || a.type === 'poll') && isAllowedTribeActivity(a) && isVisible(a)); - else if (filter === 'industry') - out = deduped.filter(a => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(a.type) && isVisible(a)); else if (filter === 'pad') out = deduped.filter(a => a.type === 'pad' && (a.content || {}).status === 'OPEN'); - else if (filter === 'chat') out = deduped.filter(a => (a.type === 'chat' || a.type === 'chatThread') && isAllowedTribeActivity(a) && isVisible(a)); + else if (filter === 'chat') out = deduped.filter(a => a.type === 'chat' && (a.content || {}).status === 'OPEN'); else if (filter === 'calendar') out = deduped.filter(a => a.type === 'calendar' && (a.content || {}).status === 'OPEN'); - else if (filter === 'transfer') out = deduped.filter(a => a.type === 'transfer' && isVisible(a)); else out = deduped.filter(a => a.type === filter); - if (filter !== 'top') out.sort((a, b) => (b.ts || 0) - (a.ts || 0)); + out.sort((a, b) => (b.ts || 0) - (a.ts || 0)); return out; })(); _feedCacheInflight.set(cacheKey, promise); diff --git a/src/models/agenda_model.js b/src/models/agenda_model.js index 9090b89..863b8a5 100644 --- a/src/models/agenda_model.js +++ b/src/models/agenda_model.js @@ -19,7 +19,7 @@ function writeAgendaConfig(cfg) { fs.writeFileSync(agendaConfigPath, JSON.stringify(cfg, null, 2)); } -module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel, jobsModel, projectsModel, industryModel, housingModel }) => { +module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel, jobsModel, projectsModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; @@ -184,15 +184,7 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel ? projectsModel.listProjects('all').then(normalize).catch(() => []) : fetchItems('project'); - const industryViaModel = industryModel && typeof industryModel.listMyBuilds === 'function' - ? industryModel.listMyBuilds().then(normalize).catch(() => []) - : []; - - const housingViaModel = housingModel && typeof housingModel.listHousing === 'function' - ? housingModel.listHousing('ALL', userId).then(normalize).catch(() => []) - : []; - - const [tasksAll, eventsAll, transfersAll, tribesAll, marketAll, reportsAll, jobsAll, projectsAll, calendarsAll, industryAll, housingAll] = await Promise.all([ + const [tasksAll, eventsAll, transfersAll, tribesAll, marketAll, reportsAll, jobsAll, projectsAll, calendarsAll] = await Promise.all([ tasksViaModel, eventsViaModel, fetchItems('transfer'), @@ -201,9 +193,7 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel fetchItems('report'), jobsViaModel, projectsViaModel, - calendarsViaModel, - industryViaModel, - housingViaModel + calendarsViaModel ]); const tasks = tasksAll.filter(c => Array.isArray(c.assignees) && c.assignees.includes(userId)).map(t => ({ ...t, type: 'task' })); @@ -216,10 +206,6 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel const reports = reportsAll.filter(c => c.author === userId || (Array.isArray(c.confirmations) && c.confirmations.includes(userId))).map(r => ({ ...r, type: 'report' })); const jobs = jobsAll.filter(c => c.author === userId || (Array.isArray(c.subscribers) && c.subscribers.includes(userId))).map(j => ({ ...j, type: 'job', title: j.title })); const projects = projectsAll.map(p => ({ ...p, type: 'project' })); - const industryBuilds = industryAll.map(b => ({ ...b, type: 'industry', title: b.title })); - const housingPlaces = housingAll - .filter(h => h.author === userId || (Array.isArray(h.requests) && h.requests.includes(userId))) - .map(h => ({ ...h, type: 'housing', date: h.availableFrom || h.createdAt, requested: Array.isArray(h.requests) && h.requests.includes(userId) })); const myCalendars = calendarsAll .filter(c => c.author === userId || (Array.isArray(c.participants) && c.participants.includes(userId))); const calendars = myCalendars.map(c => ({ ...c, type: 'calendar' })); @@ -255,8 +241,6 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel ...reports, ...jobs, ...projects, - ...industryBuilds, - ...housingPlaces, ...calendars, ...calendarDates ]; @@ -276,8 +260,6 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel else if (filter === 'closed') filtered = filtered.filter(i => String(i.status).toUpperCase() === 'CLOSED'); else if (filter === 'jobs') filtered = filtered.filter(i => i.type === 'job'); else if (filter === 'projects') filtered = filtered.filter(i => i.type === 'project'); - else if (filter === 'industry') filtered = filtered.filter(i => i.type === 'industry'); - else if (filter === 'housing') filtered = filtered.filter(i => i.type === 'housing'); else if (filter === 'calendars') filtered = filtered.filter(i => i.type === 'calendar' || i.type === 'calendarDate'); else if (filter === 'today') { const startOfDay = new Date(); startOfDay.setHours(0, 0, 0, 0); @@ -331,8 +313,6 @@ module.exports = ({ cooler, calendarsModel, eventsModel, tasksModel, marketModel reports: mainItems.filter(i => i.type === 'report').length, jobs: mainItems.filter(i => i.type === 'job').length, projects: mainItems.filter(i => i.type === 'project').length, - industry: mainItems.filter(i => i.type === 'industry').length, - housing: mainItems.filter(i => i.type === 'housing').length, calendars: mainItems.filter(i => i.type === 'calendar' || i.type === 'calendarDate').length, today: mainItems.filter(i => { const d = itemTs(i); return d >= startOfDay.getTime() && d <= endOfDay.getTime(); }).length, upcoming: mainItems.filter(i => itemTs(i) > now).length, diff --git a/src/models/banking_model.js b/src/models/banking_model.js index 46d93ff..4837168 100644 --- a/src/models/banking_model.js +++ b/src/models/banking_model.js @@ -589,14 +589,6 @@ function scoreFromActions(actions) { else if (t === "chat") score += 1 * decay; else if (t === "gamescore") score += 2 * decay; else if (t === "pixelia") score += 2 * decay; - else if (rawType === "industry") score += 8 * decay; - else if (rawType === "industryblueprint") score += 6 * decay; - else if (rawType === "industrybuild") score += 8 * decay; - else if (rawType === "industrycontribution") score += 4 * decay; - else if (rawType === "industryallocation") score += 10 * decay; - else if (rawType === "industryvote") score += 2 * decay; - else if (rawType === "industrymember") score += 2 * decay; - else if (rawType === "industryopinion") score += 1 * decay; } return Math.max(0, Math.round(score)); } @@ -762,35 +754,6 @@ async function getUserArchTax(userId) { return archTaxFromDays(ageDays); } -let _industryModel = null; -function industryModel() { - if (_industryModel) return _industryModel; - if (!services || !services.cooler) return null; - try { _industryModel = require("./industry_model")({ cooler: services.cooler }); } catch (_) { _industryModel = null; } - return _industryModel; -} - -async function getIndustryBalance(userId) { - const uid = resolveUserId(userId); - const industry = industryModel(); - if (!industry) return { received: 0, sent: 0, net: 0, networkTotal: 0 }; - const builds = await industry.listAllBuilds().catch(() => []); - let production = 0; - let earned = 0; - for (const b of (builds || [])) { - const est = parseFloat(b && b.estTotal); - if (Number.isFinite(est) && est > 0) production += est; - const mine = parseFloat(b && b.points && b.points[uid]); - if (Number.isFinite(mine) && mine > 0) earned += mine; - } - return { - received: Number(earned.toFixed(6)), - sent: 0, - net: Number(earned.toFixed(6)), - networkTotal: Number(production.toFixed(6)) - }; -} - async function getUserEngagementScore(userId) { const ssb = await openSsb(); const uid = resolveUserId(userId); @@ -1099,7 +1062,6 @@ async function getLastPublishedTimestamp(userId) { allocations = await getUbiAllocationsFromSSB(); } const userBalance = await safeGetBalance("user"); - const industryBal = await getIndustryBalance(uid).catch(() => ({ received: 0, sent: 0, net: 0, networkTotal: 0 })); const epochs = await epochsRepo.list(); let computed = null; try { computed = await computeEpoch({ epochId, userId: uid, rules: DEFAULT_RULES }); } catch {} @@ -1118,10 +1080,6 @@ async function getLastPublishedTimestamp(userId) { const hasValidWallet = !!(userAddress && isValidEcoinAddress(userAddress) && userWalletCfg.url); const summary = { userBalance, - industryBalance: industryBal.net, - industryNetworkTotal: industryBal.networkTotal, - industryReceived: industryBal.received, - industrySent: industryBal.sent, epochId, pool: poolForEpoch, weightsSum: computed?.epoch?.weightsSum || 0, diff --git a/src/models/blog_model.js b/src/models/blog_model.js deleted file mode 100644 index 8a7df90..0000000 --- a/src/models/blog_model.js +++ /dev/null @@ -1,168 +0,0 @@ -const pull = require('../server/node_modules/pull-stream'); -const { getConfig } = require('../configs/config-manager.js'); -const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const logLimit = getConfig().ssbLogStream?.limit || 1000; - -const OPINION_TYPE = 'blogOpinion'; - -module.exports = ({ cooler, isPublic = false }) => { - let ssb; - const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; - - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); - - const isRootPost = (c) => - !!c && c.type === 'post' && typeof c.text === 'string' && - !c.root && !c.fork && !c.branch && !c.about && !c.target; - - const buildIndex = (messages) => { - const tomb = buildValidatedTombstoneSet(messages); - const posts = new Map(); - const commentsByRoot = new Map(); - const opinionsByRoot = new Map(); - - for (const m of messages) { - const v = m && m.value; - const c = v && v.content; - if (!c || typeof c !== 'object') continue; - if (tomb.has(m.key)) continue; - - if (c.type === 'post' && typeof c.text === 'string') { - if (isRootPost(c)) { - posts.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); - } else if (typeof c.root === 'string') { - commentsByRoot.set(c.root, (commentsByRoot.get(c.root) || 0) + 1); - } - continue; - } - - if (c.type === OPINION_TYPE && typeof c.target === 'string') { - const entry = opinionsByRoot.get(c.target) || { counts: {}, voters: [] }; - if (!entry.voters.includes(v.author)) { - entry.voters.push(v.author); - entry.counts[c.category] = (entry.counts[c.category] || 0) + 1; - } - opinionsByRoot.set(c.target, entry); - } - } - - return { tomb, posts, commentsByRoot, opinionsByRoot }; - }; - - const buildBlog = (node, idx) => { - const c = node.c || {}; - const op = idx.opinionsByRoot.get(node.key) || { counts: {}, voters: [] }; - return { - id: node.key, - key: node.key, - author: node.author, - subject: typeof c.contentWarning === 'string' ? c.contentWarning : '', - text: c.text || '', - mentions: Array.isArray(c.mentions) ? c.mentions : [], - allowComments: c.allowComments !== false, - createdAt: new Date(node.ts).toISOString(), - commentCount: idx.commentsByRoot.get(node.key) || 0, - opinions: op.counts, - opinions_inhabitants: op.voters - }; - }; - - const collect = async () => { - const ssbClient = await openSsb(); - const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); - const list = []; - for (const node of idx.posts.values()) list.push(buildBlog(node, idx)); - return { list, idx, viewerId: ssbClient.id }; - }; - - const opinionScore = (blog) => - Object.values(blog.opinions || {}).reduce((n, v) => n + (Number(v) || 0), 0); - - return { - type: 'post', - - async listAll(filter = 'ALL', opts = {}) { - const { list, viewerId } = await collect(); - const f = String(filter || 'ALL').toUpperCase(); - const favorites = Array.isArray(opts.favorites) ? new Set(opts.favorites.map(String)) : null; - let out = list.slice(); - - if (f === 'MINE') out = out.filter(b => String(b.author) === String(viewerId)); - else if (f === 'FAVORITES') out = favorites ? out.filter(b => favorites.has(String(b.id))) : []; - - const q = String(opts.q || '').trim().toLowerCase(); - if (q) out = out.filter(b => - b.text.toLowerCase().includes(q) || b.subject.toLowerCase().includes(q)); - - if (f === 'TOP') { - out.sort((a, b) => (opinionScore(b) + b.commentCount) - (opinionScore(a) + a.commentCount) || - new Date(b.createdAt) - new Date(a.createdAt)); - } else { - out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - } - - return out; - }, - - async getBlogById(id) { - const { list } = await collect(); - const blog = list.find(b => b.id === id); - if (!blog) throw new Error('Blog not found'); - return blog; - }, - - async createBlog({ text, subject = '', mentions = [], allowComments = true } = {}) { - if (isPublic) throw new Error('Not available in public mode'); - const body = String(text || '').trim(); - if (!body) throw new Error('Blog text is required'); - const ssbClient = await openSsb(); - const content = { - type: 'post', - text: body, - allowComments: allowComments !== false, - ...(Array.isArray(mentions) && mentions.length ? { mentions } : {}), - ...(String(subject || '').trim() ? { contentWarning: String(subject).trim() } : {}) - }; - return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); - }, - - async createOpinion(id, category) { - const categories = require('../backend/opinion_categories'); - if (!categories.includes(category)) throw new Error('Invalid opinion category'); - const ssbClient = await openSsb(); - const userId = ssbClient.id; - const blog = await this.getBlogById(id); - if (blog.opinions_inhabitants.includes(userId)) throw new Error('Already opined'); - const content = { type: OPINION_TYPE, target: blog.id, category, createdAt: new Date().toISOString() }; - return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result))); - }, - - async resolveRootId(id) { - const ssbClient = await openSsb(); - const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m))); - const c = msg && (msg.content || (msg.value && msg.value.content)); - if (c && c.type === 'post' && typeof c.root === 'string') return c.root; - return id; - }, - - async blogHrefFor(id) { - const ssbClient = await openSsb(); - const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m))); - const c = msg && (msg.content || (msg.value && msg.value.content)); - if (!c || c.type !== 'post' || typeof c.text !== 'string') return null; - if (c.private === true || Array.isArray(c.recps)) return null; - if (typeof c.root === 'string') { - return `/blogs/${encodeURIComponent(c.root)}#${encodeURIComponent(id)}`; - } - if (c.fork || c.branch || c.about || c.target) return null; - return `/blogs/${encodeURIComponent(id)}`; - } - }; -}; diff --git a/src/models/bookmarking_model.js b/src/models/bookmarking_model.js index 86a2bea..144fb96 100644 --- a/src/models/bookmarking_model.js +++ b/src/models/bookmarking_model.js @@ -145,6 +145,7 @@ module.exports = ({ cooler }) => { updatedAt: c.updatedAt || null, lastVisit: c.lastVisit || null, tags: safeArr(c.tags), + category: c.category || "", description: c.description || "", opinions, opinions_inhabitants: voters, @@ -181,7 +182,7 @@ module.exports = ({ cooler }) => { return root; }, - async createBookmark(url, tagsRaw, description, lastVisit) { + async createBookmark(url, tagsRaw, description, category, lastVisit) { const ssbClient = await openSsb(); const now = new Date().toISOString(); @@ -194,6 +195,7 @@ module.exports = ({ cooler }) => { url: u, tags: normalizeTags(tagsRaw), description: description || "", + category: category || "", createdAt: now, updatedAt: now, lastVisit: coerceLastVisit(lastVisit), @@ -229,6 +231,7 @@ module.exports = ({ cooler }) => { url, tags: updatedData.tags !== undefined ? normalizeTags(updatedData.tags) : safeArr(oldMsg.content.tags), description: updatedData.description !== undefined ? updatedData.description || "" : oldMsg.content.description || "", + category: updatedData.category !== undefined ? updatedData.category || "" : oldMsg.content.category || "", lastVisit: coerceLastVisit(updatedData.lastVisit || oldMsg.content.lastVisit), createdAt: oldMsg.content.createdAt, updatedAt: now @@ -293,10 +296,11 @@ module.exports = ({ cooler }) => { if (q) { list = list.filter((b) => { const url = String(b.url || "").toLowerCase(); + const cat = String(b.category || "").toLowerCase(); const desc = String(b.description || "").toLowerCase(); const tags = safeArr(b.tags).join(" ").toLowerCase(); const author = String(b.author || "").toLowerCase(); - return url.includes(q) || desc.includes(q) || tags.includes(q) || author.includes(q); + return url.includes(q) || cat.includes(q) || desc.includes(q) || tags.includes(q) || author.includes(q); }); } diff --git a/src/models/calendars_model.js b/src/models/calendars_model.js index cb9089a..5eb49ce 100644 --- a/src/models/calendars_model.js +++ b/src/models/calendars_model.js @@ -14,26 +14,25 @@ const normalizeTags = (raw) => { return String(raw).split(",").map(t => t.trim()).filter(Boolean) } const hasAnyInterval = (w, m, y) => !!(w || m || y) -const { expandRecurrence } = require('./recurrence') - -const ts = (v) => { - const t = new Date(v).getTime() - return Number.isFinite(t) ? t : null -} - -const assertCalendarDates = ({ deadline, date, until }) => { - const now = Date.now() - const dl = deadline ? ts(deadline) : null - const dt = date ? ts(date) : null - const un = until ? ts(until) : null - if (deadline && dl === null) throw new Error("Deadline is not a valid date") - if (date && dt === null) throw new Error("Date is not a valid date") - if (until && un === null) throw new Error("The recurrence end is not a valid date") - if (dl !== null && dl <= now) throw new Error("Deadline must be in the future") - if (dt !== null && dt <= now) throw new Error("Date must be in the future") - if (dt !== null && dl !== null && dt > dl) throw new Error("Date cannot be later than the deadline") - if (un !== null && dt !== null && un < dt) throw new Error("The recurrence cannot end before it starts") - if (un !== null && dl !== null && un > dl) throw new Error("The recurrence cannot end after the deadline") +const expandRecurrence = (firstDate, deadline, weekly, monthly, yearly) => { + const start = new Date(firstDate) + const out = [start] + if (!deadline || !hasAnyInterval(weekly, monthly, yearly)) return out + const end = new Date(deadline).getTime() + const seen = new Set([start.getTime()]) + const walk = (mutate) => { + const n = new Date(start) + mutate(n) + while (n.getTime() <= end) { + const t = n.getTime() + if (!seen.has(t)) { seen.add(t); out.push(new Date(n)) } + mutate(n) + } + } + if (weekly) walk((d) => d.setDate(d.getDate() + 7)) + if (monthly) walk((d) => d.setMonth(d.getMonth() + 1)) + if (yearly) walk((d) => d.setFullYear(d.getFullYear() + 1)) + return out.sort((a, b) => a.getTime() - b.getTime()) } module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) => { @@ -109,12 +108,6 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) return tribeCrypto.encryptContent(content, [key], true) } - const decryptScoped = async (content, rootId) => { - if (!content) return content - if (content.tribeId) return await decryptIfTribe(content) - return decryptCalendarRoot(content, rootId) - } - const decryptCalendarRoot = (content, rootId) => { if (!content || !content.encryptedPayload) return content if (!tribeCrypto) return content @@ -266,7 +259,6 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: c.status || "OPEN", deadline: undec ? "" : (c.deadline || ""), tags: Array.isArray(c.tags) ? c.tags : [], - mapUrl: typeof c.mapUrl === "string" ? c.mapUrl : "", author: c.author || node.author, participants: Array.isArray(participants) ? participants : (Array.isArray(c.participants) ? c.participants : []), invites: Array.isArray(c.invites) ? c.invites : [], @@ -313,11 +305,17 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) try { const ssbClient = await openSsb() const messages = await readAll(ssbClient) + const live = new Set() const tomb = buildValidatedTombstoneSet(messages) + for (const m of messages) { + const c = m.value && m.value.content + if (!c) continue + if (c.type === "calendar") live.add(m.key) + } const all = ownCrypto.getAllRootIds() let removed = 0 for (const rid of all) { - if (tomb.has(rid)) { + if (!live.has(rid) || tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } } @@ -347,18 +345,14 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) return tip }, - async createCalendar({ title, status, deadline, tags, firstDate, firstDateLabel, firstNote, intervalWeekly, intervalMonthly, intervalYearly, intervalDeadline, mapUrl, tribeId }) { + async createCalendar({ title, status, deadline, tags, firstDate, firstDateLabel, firstNote, intervalWeekly, intervalMonthly, intervalYearly, tribeId }) { const ssbClient = await openSsb() const userId = ssbClient.id const now = new Date().toISOString() const validStatus = ["OPEN", "CLOSED"].includes(String(status).toUpperCase()) ? String(status).toUpperCase() : "OPEN" - if (!firstDate) throw new Error("First date must be in the future") - assertCalendarDates({ - deadline, - date: firstDate, - until: hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) ? intervalDeadline : "" - }) + if (deadline && new Date(deadline).getTime() <= Date.now()) throw new Error("Deadline must be in the future") + if (!firstDate || new Date(firstDate).getTime() <= Date.now()) throw new Error("First date must be in the future") let plainContent = { type: "calendar", @@ -366,7 +360,6 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: validStatus, deadline: deadline || "", tags: normalizeTags(tags), - mapUrl: safeText(mapUrl), author: userId, participants: [userId], invites: [], @@ -413,7 +406,6 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: validStatus, deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], - mapUrl: dec.mapUrl || "", author: userId, participants: [userId], invites: [{ code: pubCode, ek, salt: inviteSalt, gen: 1, public: true }], @@ -438,7 +430,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) ...(intervalWeekly ? { intervalWeekly: true } : {}), ...(intervalMonthly ? { intervalMonthly: true } : {}), ...(intervalYearly ? { intervalYearly: true } : {}), - ...(hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) && (intervalDeadline || deadline) ? { intervalDeadline: intervalDeadline || deadline } : {}), + ...(deadline && hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) ? { intervalDeadline: deadline } : {}), ...(tribeId ? { tribeId } : {}) } if (tribeId) dateContent = await encryptIfTribe(dateContent) @@ -481,15 +473,12 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) : decryptCalendarRoot(item.content, rootId) assertReadable(oldDec, "Calendar") if ((oldDec.author || item.content.author) !== userId) throw new Error("Not the author") - const nextDeadline = data.deadline !== undefined ? data.deadline : (oldDec.deadline || "") - if (nextDeadline !== (oldDec.deadline || "")) assertCalendarDates({ deadline: nextDeadline }) let updated = { type: "calendar", title: data.title !== undefined ? safeText(data.title) : (oldDec.title || ""), status: data.status !== undefined ? (["OPEN","CLOSED"].includes(String(data.status).toUpperCase()) ? String(data.status).toUpperCase() : oldDec.status) : (oldDec.status || "OPEN"), - deadline: nextDeadline, + deadline: data.deadline !== undefined ? data.deadline : (oldDec.deadline || ""), tags: data.tags !== undefined ? normalizeTags(data.tags) : (Array.isArray(oldDec.tags) ? oldDec.tags : []), - mapUrl: data.mapUrl !== undefined ? safeText(data.mapUrl) : (oldDec.mapUrl || ""), author: oldDec.author || userId, participants: oldDec.participants || [userId], invites: Array.isArray(oldDec.invites) ? oldDec.invites : [], @@ -512,8 +501,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const userId = ssbClient.id const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it))) if (!item || !item.content) throw new Error("Calendar not found") - const rootId = await this.resolveRootId(id) - const dec = await decryptScoped(item.content, rootId) + const dec = await decryptIfTribe(item.content) assertReadable(dec, "Calendar") const contentAuthor = (dec && dec.author) || (typeof item.content === 'object' && item.content.author) if (contentAuthor !== userId) throw new Error("Not the author") @@ -606,14 +594,9 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const cal = await this.getCalendarById(rootId) if (!cal) throw new Error("Calendar not found") if (cal.status === "CLOSED" && userId !== cal.author) throw new Error("Only the author can add dates to a CLOSED calendar") - if (!date) throw new Error("Date must be in the future") - const hasInterval = hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) - assertCalendarDates({ - deadline: cal.deadline || "", - date, - until: hasInterval ? intervalDeadline : "" - }) + if (!date || new Date(date).getTime() <= Date.now()) throw new Error("Date must be in the future") + const hasInterval = hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) const ruleDeadline = hasInterval ? (intervalDeadline || cal.deadline || "") : "" let dateContent = { type: "calendarDate", @@ -771,9 +754,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const userId = ssbClient.id const item = await new Promise((resolve, reject) => ssbClient.get(noteId, (e, it) => e ? reject(e) : resolve(it))) if (!item || !item.content) throw new Error("Note not found") - const noteRoot = item.content.calendarId || null - const dec = await decryptScoped(item.content, noteRoot) - if (((dec && dec.author) || item.content.author) !== userId) throw new Error("Not the author") + const dec = await decryptIfTribe(item.content) + if ((dec.author || item.content.author) !== userId) throw new Error("Not the author") return new Promise((resolve, reject) => { ssbClient.publish({ type: "tombstone", target: noteId, deletedAt: new Date().toISOString(), author: userId }, (e, msg) => e ? reject(e) : resolve(msg)) }) @@ -801,8 +783,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (tombstoned.has(m.key)) continue if (c.calendarId !== rootId || c.dateId !== dateId) continue let dec = c - if (c.encryptedPayload) { - const r = await decryptScoped(c, rootId) + if (c.encryptedPayload && tribeCrypto && tribesModel) { + const r = await tribeCrypto.decryptFromTribe(c, tribesModel) if (r && !r._undecryptable) dec = r else continue } @@ -953,7 +935,6 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: dec.status || "OPEN", deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], - mapUrl: dec.mapUrl || "", author: dec.author, participants: Array.isArray(dec.participants) ? dec.participants : [userId], invites, @@ -1000,7 +981,6 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: dec.status || "OPEN", deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], - mapUrl: dec.mapUrl || "", author: dec.author, participants: Array.isArray(dec.participants) ? dec.participants : [userId], invites, @@ -1035,7 +1015,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) let calKey = null if (tribeCrypto && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3) + const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt) if (Array.isArray(chain) && chain.length) { for (const entry of chain) { if (Array.isArray(entry.keys) && entry.keys.length) { @@ -1069,7 +1049,6 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) status: dec.status || "OPEN", deadline: dec.deadline || "", tags: Array.isArray(dec.tags) ? dec.tags : [], - mapUrl: dec.mapUrl || "", author: dec.author, participants: [...(Array.isArray(dec.participants) ? dec.participants : []), userId], invites, diff --git a/src/models/chats_model.js b/src/models/chats_model.js index 545e1fe..f4abc89 100644 --- a/src/models/chats_model.js +++ b/src/models/chats_model.js @@ -17,22 +17,10 @@ const normalizeTags = (raw) => { const INVITE_CODE_BYTES = 16 const VALID_STATUS = ["OPEN", "INVITE-ONLY", "CLOSED"] -const DEFAULT_MESSAGES_PER_HOUR = 60 - module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } - const MESSAGES_PER_HOUR = (() => { - try { - const raw = getConfig()?.chats?.messagesPerHour - const n = parseInt(raw, 10) - return Number.isFinite(n) && n > 0 ? n : DEFAULT_MESSAGES_PER_HOUR - } catch (_) { - return DEFAULT_MESSAGES_PER_HOUR - } - })() - const ownCrypto = chatCrypto || tribeCrypto const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null const lookupKeys = (rid) => { @@ -304,7 +292,6 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { chatId: c.chatId || "", text, image: c.image || null, - replyTo: c.replyTo || null, author: c.author || node.author, createdAt: c.createdAt || new Date(node.ts).toISOString() } @@ -370,15 +357,6 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { return { type: "chat", - async encryptionKeyFor(chatRootId, tribeId = null) { - if (!tribeCrypto) return null - if (tribeId) { - const k = await getTribeFirstKeyFor(tribeId) - if (k) return k - } - return lookupKey(chatRootId) || null - }, - async resolveRootId(id) { const ssbClient = await openSsb() const messages = await readAll(ssbClient) @@ -618,18 +596,6 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { let list = chatCollab.visibleThenCollapsed(collectChats(idx), uid) - const lastMsgAt = new Map() - const lastMineAt = new Map() - for (const node of idx.msgNodes.values()) { - const cid = node.c && node.c.chatId - if (!cid) continue - const r = idx.rawRootOf(cid) || cid - const t = node.ts || 0 - if (t > (lastMsgAt.get(r) || 0)) lastMsgAt.set(r, t) - if (node.author === uid && t > (lastMineAt.get(r) || 0)) lastMineAt.set(r, t) - } - list = list.map(c => ({ ...c, lastMsgAt: lastMsgAt.get(c.rootId) || 0, lastMineAt: lastMineAt.get(c.rootId) || 0 })) - if (filter === "mine") list = list.filter(c => c.author === uid) else if (filter === "recent") list = list.filter(c => new Date(c.createdAt).getTime() >= now - 86400000) else if (filter === "open") list = list.filter(c => c.status === "OPEN" || c.status === "INVITE-ONLY") @@ -740,7 +706,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { let chatKey = null if (tribeCrypto && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3) + const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt) if (Array.isArray(chain) && chain.length) { for (const entry of chain) { if (Array.isArray(entry.keys) && entry.keys.length) { @@ -813,11 +779,17 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { try { const ssbClient = await openSsb() const messages = await readAll(ssbClient) + const live = new Set() + for (const m of messages) { + const c = m.value && m.value.content + if (!c) continue + if (c.type === "chat") live.add(m.key) + } const tomb = buildValidatedTombstoneSet(messages) const all = ownCrypto.getAllRootIds() let removed = 0 for (const rid of all) { - if (tomb.has(rid)) { + if (!live.has(rid) || tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } } @@ -825,7 +797,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { } catch (_) { return 0 } }, - async sendMessage(chatId, text, image = null, replyTo = null) { + async sendMessage(chatId, text, image = null) { const ssbClient = await openSsb() const userId = ssbClient.id const chat = await this.getChatById(chatId) @@ -842,12 +814,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const c = m.value?.content return c?.type === "chatMessage" && c?.chatId === chat.rootId && m.value?.author === userId && (m.value?.timestamp || 0) >= oneHourAgo }).length - if (recentCount >= MESSAGES_PER_HOUR) { - const err = new Error(`Rate limit: max ${MESSAGES_PER_HOUR} messages per hour`) - err.code = "CHAT_RATE_LIMIT" - err.retryAfterMinutes = 60 - throw err - } + if (recentCount >= 3) throw new Error("Rate limit: max 3 messages per hour") const now = new Date().toISOString() let content = { @@ -857,7 +824,6 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { createdAt: now } if (image) content.image = image - if (replyTo && typeof replyTo === "string" && replyTo.startsWith("%")) content.replyTo = replyTo const chatIsEncrypted = !!(chat.tribeId) || !!lookupKey(chat.rootId) if (chatIsEncrypted && tribeCrypto) { diff --git a/src/models/crypto.js b/src/models/crypto.js index 13ebaca..10d7085 100644 --- a/src/models/crypto.js +++ b/src/models/crypto.js @@ -1,6 +1,7 @@ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); +const keylock = require('./keylock'); const SENSITIVE_FIELDS = [ 'title', 'description', 'location', 'price', 'salary', 'options', 'votes', @@ -45,7 +46,14 @@ module.exports = (configPath, namespace = 'tribes') => { const loadKeyring = () => { try { - keyring = JSON.parse(fs.readFileSync(keyringPath, 'utf8')); + const raw = fs.readFileSync(keyringPath); + if (keylock.isWrapped(raw)) { + if (!keylock.getKEK()) throw new Error('keyring cifrado pero keylock bloqueado'); + keyring = JSON.parse(keylock.unwrap(raw).toString('utf8')); + } else { + keyring = JSON.parse(raw.toString('utf8')); + if (keylock.getKEK()) { try { saveKeyring(); } catch (_) {} } + } try { fs.chmodSync(keyringPath, 0o600); } catch (_) {} } catch (e) { if (e.code !== 'ENOENT') throw e; @@ -55,8 +63,11 @@ module.exports = (configPath, namespace = 'tribes') => { }; const saveKeyring = () => { + const json = JSON.stringify(keyring, null, 2); + const kek = keylock.getKEK(); + const out = kek ? keylock.wrap(Buffer.from(json, 'utf8'), kek) : Buffer.from(json, 'utf8'); const tmp = keyringPath + '.tmp.' + process.pid + '.' + Date.now(); - fs.writeFileSync(tmp, JSON.stringify(keyring, null, 2), { encoding: 'utf8', mode: 0o600 }); + fs.writeFileSync(tmp, out, { mode: 0o600 }); fs.renameSync(tmp, keyringPath); try { fs.chmodSync(keyringPath, 0o600); } catch (_) {} }; @@ -223,27 +234,14 @@ module.exports = (configPath, namespace = 'tribes') => { return decryptWithKey(encryptedKey, derived.toString('hex'), inviteAad(inviteCode, salt)); }; - const INVITE_ENCRYPT_ATTEMPTS = 3; - const encryptChainForInvite = (ancestryRootIds, code, salt) => { const chain = ancestryRootIds.map(rid => ({ rootId: rid, keys: getKeys(rid), gen: getGen(rid) })); if (chain.some(e => !Array.isArray(e.keys) || !e.keys.length)) return null; - const plain = JSON.stringify(chain); - const aad = inviteAad(code, salt); - for (let attempt = 0; attempt < INVITE_ENCRYPT_ATTEMPTS; attempt++) { - const k = deriveInviteKey(code, salt); - const payload = encryptWithKey(plain, k.toString('hex'), aad); - if (!payload) continue; - const readBack = decryptChainFromInvite(payload, code, salt); - if (readBack && JSON.stringify(readBack.map(e => ({ rootId: e.rootId, keys: e.keys, gen: e.gen }))) - === JSON.stringify(chain.map(e => ({ rootId: e.rootId, keys: e.keys, gen: e.gen })))) { - return payload; - } - } - throw new Error('Could not produce a verifiable invite on this machine'); + const k = deriveInviteKey(code, salt); + return encryptWithKey(JSON.stringify(chain), k.toString('hex'), inviteAad(code, salt)); }; - const decryptChainOnce = (encryptedPayload, code, salt) => { + const decryptChainFromInvite = (encryptedPayload, code, salt) => { const k = deriveInviteKey(code, salt); try { const json = decryptWithKey(encryptedPayload, k.toString('hex'), inviteAad(code, salt)); @@ -260,15 +258,6 @@ module.exports = (configPath, namespace = 'tribes') => { return null; }; - const decryptChainFromInvite = (encryptedPayload, code, salt, attempts = 1) => { - const tries = Math.max(1, Number(attempts) || 1); - for (let i = 0; i < tries; i++) { - const chain = decryptChainOnce(encryptedPayload, code, salt); - if (chain) return chain; - } - return null; - }; - const inviteMatchesCode = (inv, code) => { if (!inv || typeof inv !== 'object' || !inv.codeHash) return false; return inv.codeHash === hashInviteCode(code, inv.salt); diff --git a/src/models/cv_model.js b/src/models/cv_model.js index fabbc6c..8e66e54 100644 --- a/src/models/cv_model.js +++ b/src/models/cv_model.js @@ -47,8 +47,6 @@ module.exports = ({ cooler }) => { status: data.status || 'LOOKING FOR WORK', preferences: data.preferences || 'REMOTE WORKING', visibility: String(data.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC', - aiManaged: data.aiManaged === undefined ? true : (data.aiManaged === true || data.aiManaged === '1' || data.aiManaged === 'true'), - matchThreshold: Math.min(100, Math.max(0, parseInt(data.matchThreshold, 10) || 80)), createdAt: new Date().toISOString() }; return new Promise((resolve, reject) => { @@ -87,7 +85,7 @@ module.exports = ({ cooler }) => { author: userId, name: data.name, description: data.description, - photo: extractBlobId(photoBlobId) || old.content.photo || null, + photo: extractBlobId(photoBlobId) || null, contact: userId, personalSkills: parseCSV(data.personalSkills), personalExperiences: data.personalExperiences || '', @@ -104,12 +102,6 @@ module.exports = ({ cooler }) => { visibility: data.visibility !== undefined ? (String(data.visibility).toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC') : (old.content.visibility || 'PUBLIC'), - aiManaged: data.aiManaged !== undefined - ? (data.aiManaged === true || data.aiManaged === '1' || data.aiManaged === 'true') - : (old.content.aiManaged !== false), - matchThreshold: data.matchThreshold !== undefined - ? Math.min(100, Math.max(0, parseInt(data.matchThreshold, 10) || 80)) - : (Number(old.content.matchThreshold) || 80), createdAt: old.content.createdAt, updatedAt: new Date().toISOString() }; diff --git a/src/models/data_model.js b/src/models/data_model.js deleted file mode 100644 index efa67dd..0000000 --- a/src/models/data_model.js +++ /dev/null @@ -1,292 +0,0 @@ -const pull = require('../server/node_modules/pull-stream'); -const { getConfig } = require('../configs/config-manager.js'); -const { buildValidatedTombstoneSet } = require('./tombstone_validator'); - -const logLimit = getConfig().ssbLogStream?.limit || 1000; - -const norm = (v) => String(v == null ? '' : v).trim().toLowerCase(); - -const KINDS = { - inhabitants: { type: 'curriculum', href: (id, c) => `/author/${encodeURIComponent(c.author)}` }, - jobs: { type: 'job', href: (id) => `/jobs/${encodeURIComponent(id)}` }, - projects: { type: 'project', href: (id) => `/projects/${encodeURIComponent(id)}` }, - events: { type: 'event', href: (id) => `/events/${encodeURIComponent(id)}` }, - tribes: { type: 'tribe', href: (id) => `/tribe/${encodeURIComponent(id)}` }, - market: { type: 'market', href: (id) => `/market/${encodeURIComponent(id)}` }, - housing: { type: 'housing', href: (id) => `/housing/${encodeURIComponent(id)}` }, - industry: { type: 'industry', href: (id) => `/industry/${encodeURIComponent(id)}` }, - tasks: { type: 'task', href: (id) => `/tasks/${encodeURIComponent(id)}` }, - reports: { type: 'report', href: (id) => `/reports/${encodeURIComponent(id)}` }, - votes: { type: 'poll', href: (id) => `/polls/${encodeURIComponent(id)}` }, - audios: { type: 'audio', href: (id) => `/audios/${encodeURIComponent(id)}` }, - videos: { type: 'video', href: (id) => `/videos/${encodeURIComponent(id)}` }, - images: { type: 'image', href: (id) => `/images/${encodeURIComponent(id)}` }, - documents: { type: 'document', href: (id) => `/documents/${encodeURIComponent(id)}` }, - bookmarks: { type: 'bookmark', href: (id) => `/bookmarks/${encodeURIComponent(id)}` }, - torrents: { type: 'torrent', href: (id) => `/torrents/${encodeURIComponent(id)}` }, - chats: { type: 'chat', href: (id) => `/chats/${encodeURIComponent(id)}` }, - pads: { type: 'pad', href: (id) => `/pads/${encodeURIComponent(id)}` }, - maps: { type: 'map', href: (id) => `/maps/${encodeURIComponent(id)}` }, - calendars: { type: 'calendar', href: (id) => `/calendars/${encodeURIComponent(id)}` }, - forum: { type: 'forum', href: (id) => `/forum/${encodeURIComponent(id)}` } -}; - -const KIND_BY_TYPE = Object.fromEntries(Object.entries(KINDS).map(([k, v]) => [v.type, k])); - -const cvSkills = (c) => [ - ...(c.personalSkills || []), - ...(c.oasisSkills || []), - ...(c.educationalSkills || []), - ...(c.professionalSkills || []) -]; - -const PLACEHOLDERS = new Set(['unknown', 'n/a', 'na', 'none', '-', 'other']); - -const TITLE_STOPWORDS = new Set([ - 'the', 'this', 'that', 'and', 'for', 'with', 'from', 'into', 'about', 'new', 'all', 'not', 'are', 'was', 'you', 'your', 'our', - 'los', 'las', 'del', 'una', 'unos', 'unas', 'este', 'esta', 'esto', 'que', 'con', 'para', 'por', 'sin', 'sobre', 'bajo', 'mas', 'más' -]); - -const titleTerms = (c) => String(c.title || c.name || c.question || c.concept || '') - .toLowerCase() - .split(/[^0-9A-Za-zÀ-￿]+/) // sin \p{}: nodejs-mobile no lleva ICU completo - .filter(w => w.length >= 3 && !TITLE_STOPWORDS.has(w) && !/^\d+$/.test(w)); - -const termsOf = (kind, c) => { - const out = []; - if (kind === 'inhabitants') { - out.push(...cvSkills(c)); - if (c.languages) out.push(...String(c.languages).split(/[,;]/)); - } else if (kind === 'jobs') { - out.push(...(c.tasks || []), ...(c.tags || []), c.job_type, c.location); - } else if (kind === 'industry') { - out.push(...(c.tags || []), c.sector, ...(c.skills || [])); - } else { - out.push(...(c.tags || [])); - if (c.category) out.push(c.category); - } - return Array.from(new Set(out.map(norm).filter(t => t && !PLACEHOLDERS.has(t)))); -}; - -const titleOf = (kind, c, author) => { - if (kind === 'inhabitants') return c.name || author || ''; - return c.title || c.name || c.question || c.concept || ''; -}; - -const jaccard = (a, b) => { - if (!a.size || !b.size) return { score: 0, common: [] }; - const common = [...b].filter(x => a.has(x)); - const union = a.size + b.size - common.length; - return { score: union > 0 ? common.length / union : 0, common }; -}; - -module.exports = ({ cooler }) => { - let ssb; - const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; - - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); - - const buildGraph = async () => { - const ssbClient = await openSsb(); - const viewerId = ssbClient.id; - const messages = await getAllMessages(ssbClient); - const tomb = buildValidatedTombstoneSet(messages); - - const latestByKey = new Map(); - const replaced = new Set(); - for (const m of messages) { - const v = m && m.value; - const c = v && v.content; - if (!c || typeof c !== 'object' || !c.type) continue; - if (tomb.has(m.key)) continue; - if (c.encryptedPayload || c.encryptedQuestion) continue; - if (c.tribeId && c.type !== 'tribe') continue; - const kind = KIND_BY_TYPE[c.type]; - if (!kind) continue; - if (typeof c.replaces === 'string') replaced.add(c.replaces); - latestByKey.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, kind, c }); - } - - const nodes = []; - const byAuthorCv = new Map(); - for (const node of latestByKey.values()) { - if (replaced.has(node.key)) continue; - const coreTerms = termsOf(node.kind, node.c); - const extra = node.kind === 'inhabitants' ? [] : titleTerms(node.c).map(norm).filter(t => t && !PLACEHOLDERS.has(t)); - const terms = Array.from(new Set([...coreTerms, ...extra])); - if (!terms.length) continue; - const entry = { - id: node.key, - kind: node.kind, - author: node.c.author || node.author, - title: titleOf(node.kind, node.c, node.c.author || node.author), - terms, - termSet: new Set(terms), - coreTermSet: new Set(coreTerms), - ts: node.ts, - createdAt: node.c.createdAt || new Date(node.ts).toISOString(), - href: KINDS[node.kind].href(node.key, node.c) - }; - if (node.kind === 'inhabitants') { - const prev = byAuthorCv.get(entry.author); - if (prev && prev.ts >= entry.ts) continue; - byAuthorCv.set(entry.author, entry); - continue; - } - nodes.push(entry); - } - for (const cv of byAuthorCv.values()) nodes.push(cv); - - return { viewerId, nodes, cvByAuthor: byAuthorCv }; - }; - - const MAX_ENTITIES = 400; - const MAX_PAIRS = 300; - - const strip = (n) => ({ - id: n.id, kind: n.kind, author: n.author, title: n.title, - href: n.href, createdAt: n.createdAt, ts: n.ts - }); - - return { - KINDS: Object.keys(KINDS), - - async listMatches(filter = 'ALL', opts = {}) { - const { viewerId, nodes, cvByAuthor } = await buildGraph(); - const use = nodes.slice(0, MAX_ENTITIES); - const f = String(filter || 'ALL').toUpperCase(); - - const myTermSet = new Set(); - const mineCv = cvByAuthor.get(viewerId); - if (mineCv) for (const t of mineCv.terms) myTermSet.add(t); - for (const n of use) { - if (String(n.author) === String(viewerId)) for (const t of n.terms) myTermSet.add(t); - } - - const df = new Map(); - for (const n of use) for (const t of n.termSet) df.set(t, (df.get(t) || 0) + 1); - const total = use.length || 1; - const weightOf = (t) => Math.log(1 + total / (df.get(t) || 1)); - - let out = []; - for (const n of use) { - if (String(n.author) === String(viewerId)) continue; - const common = [...n.termSet].filter(t => myTermSet.has(t)); - if (!common.length) continue; - let commonW = 0; - for (const t of common) commonW += weightOf(t); - let itemW = 0; - for (const t of n.termSet) itemW += weightOf(t); - const score = itemW > 0 ? Math.min(1, commonW / itemW) : 0; - if (score <= 0) continue; - common.sort((x, y) => weightOf(y) - weightOf(x) || x.localeCompare(y)); - out.push({ ...strip(n), score, common, connections: common.length }); - } - - if (f === 'RECENT') { - const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; - out = out.filter(s => s.ts >= cutoff); - } else if (f !== 'ALL' && f !== 'TOP') { - const kind = f.toLowerCase(); - if (KINDS[kind]) out = out.filter(s => s.kind === kind); - } - - const q = norm(opts.q); - if (q) out = out.filter(s => norm(s.title).includes(q) || s.common.some(t => t.includes(q))); - - if (f === 'RECENT') out.sort((x, y) => y.ts - x.ts || y.score - x.score); - else out.sort((x, y) => y.score - x.score || y.ts - x.ts); - - return { - matches: out.slice(0, MAX_PAIRS), - total: out.length, - hasProfile: myTermSet.size > 0, - myTerms: [...myTermSet] - }; - }, - - async jobMatchesFor(viewerId, { minScore = 0.8 } = {}) { - const { nodes, cvByAuthor } = await buildGraph(); - const mine = cvByAuthor.get(viewerId); - if (!mine) return []; - return nodes - .filter(n => n.kind === 'jobs' && String(n.author) !== String(viewerId)) - .map(n => { - const { score, common } = jaccard(mine.termSet, n.coreTermSet || n.termSet); - return { id: n.id, title: n.title, author: n.author, href: n.href, score, common }; - }) - .filter(m => m.score >= minScore) - .sort((a, b) => b.score - a.score); - }, - - async cohesion() { - const { cvByAuthor, nodes } = await buildGraph(); - const use = nodes.slice(0, MAX_ENTITIES); - let comparisons = 0; - let sum = 0; - let connectedPairs = 0; - const linkedEntities = new Set(); - for (let i = 0; i < use.length; i++) { - for (let j = i + 1; j < use.length; j++) { - const a = use[i], b = use[j]; - if (a.kind === 'inhabitants' && b.kind === 'inhabitants' && a.author === b.author) continue; - const { score } = jaccard(a.termSet, b.termSet); - comparisons += 1; - sum += score; - if (score > 0) { connectedPairs += 1; linkedEntities.add(a.id); linkedEntities.add(b.id); } - } - } - const coefficient = comparisons > 0 ? sum / comparisons : 0; - - const people = [...cvByAuthor.values()]; - const linked = new Set(); - let cvComparisons = 0; - let cvSum = 0; - for (let i = 0; i < people.length; i++) { - for (let j = i + 1; j < people.length; j++) { - const { score } = jaccard(people[i].termSet, people[j].termSet); - cvComparisons += 1; - cvSum += score; - if (score > 0) { linked.add(people[i].author); linked.add(people[j].author); } - } - } - const cvCoefficient = cvComparisons > 0 ? cvSum / cvComparisons : 0; - - const skillSet = new Set(); - for (const p of people) for (const t of p.terms) skillSet.add(t); - - const termCount = new Map(); - for (const n of nodes) for (const t of n.terms) termCount.set(t, (termCount.get(t) || 0) + 1); - const topTerms = [...termCount.entries()] - .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) - .slice(0, 12) - .map(([term, count]) => ({ term, count })); - - const perKind = {}; - for (const n of nodes) perKind[n.kind] = (perKind[n.kind] || 0) + 1; - - return { - coefficient, - percent: Math.round(coefficient * 1000) / 10, - comparisons, - pairs: connectedPairs, - entities: use.length, - distinctTerms: termCount.size, - topTerms, - perKind, - people: people.length, - skills: skillSet.size, - connected: linkedEntities.size, - isolated: Math.max(0, use.length - linkedEntities.size), - cvCoefficient, - cvPercent: Math.round(cvCoefficient * 1000) / 10 - }; - } - }; -}; diff --git a/src/models/dev_model.js b/src/models/dev_model.js deleted file mode 100644 index 8b18406..0000000 --- a/src/models/dev_model.js +++ /dev/null @@ -1,340 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { getConfig } = require('../configs/config-manager.js'); - -const ROOT = path.resolve(__dirname, '..', '..'); - -const ALLOWED_ROOTS = ['src', 'docs', 'scripts', 'test']; -const ALLOWED_ROOT_FILES = ['README.md', 'LICENSE', 'install.sh', 'oasis.sh']; -const DENIED_SEGMENTS = new Set(['node_modules', '.git', '.ssb', 'packages', 'configs', 'embeddings', 'tiles', 'cache', 'results']); -const DENIED_NAMES = new Set(['secret', '.env', '.npmrc', 'package-lock.json']); -const DENIED_EXTENSIONS = new Set(['.gguf', '.jks', '.keystore', '.enc', '.pem', '.key', '.p12', '.crt']); -const VIEWABLE_EXTENSIONS = new Set(['.js', '.mjs', '.json', '.css', '.md', '.sh', '.html', '.txt', '.yml', '.yaml']); - -const STATS_TTL = 60 * 1000; -let statsCache = null; -let statsCacheAt = 0; - -const MAX_VIEW_BYTES = 2 * 1024 * 1024; -const PAGE_LINES = 500; -const SEARCH_MAX = 200; -const SEARCH_SNIPPET = 200; - -const extOf = (name) => { - const i = String(name || '').lastIndexOf('.'); - return i > 0 ? String(name).slice(i).toLowerCase() : ''; -}; - -const isDeniedEntry = (name) => { - const n = String(name || ''); - if (!n || n === '.' || n === '..') return true; - if (n.toLowerCase().startsWith('.git')) return true; - if (DENIED_SEGMENTS.has(n)) return true; - if (DENIED_NAMES.has(n)) return true; - if (DENIED_EXTENSIONS.has(extOf(n))) return true; - return false; -}; - -const PLAIN_TEXT_NAME = /^[A-Z][A-Z0-9_-]*$/; - -const isViewable = (name) => (VIEWABLE_EXTENSIONS.has(extOf(name)) || PLAIN_TEXT_NAME.test(String(name || ''))) && !isDeniedEntry(name); - -function realRoot() { - try { return fs.realpathSync(ROOT); } catch (_) { return ROOT; } -} - -function splitPath(rel) { - const raw = String(rel || ''); - for (let i = 0; i < raw.length; i++) { if (raw.charCodeAt(i) < 32) throw new Error('Invalid path'); } - return raw.replace(/^[/\\]+/, '').split(/[/\\]+/).filter(s => s && s !== '.'); -} - -function resolveSafe(rel) { - const segments = splitPath(rel); - if (segments.some(s => s === '..')) throw new Error('Invalid path'); - if (segments.some(isDeniedEntry)) throw new Error('Path not available'); - if (segments.length) { - const head = segments[0]; - const isRootFile = segments.length === 1 && ALLOWED_ROOT_FILES.includes(head); - if (!ALLOWED_ROOTS.includes(head) && !isRootFile) throw new Error('Path not available'); - } - const target = path.resolve(ROOT, segments.join(path.sep)); - let real; - try { real = fs.realpathSync(target); } catch (_) { throw new Error('Not found'); } - const base = realRoot(); - if (real !== base && !real.startsWith(base + path.sep)) throw new Error('Path not available'); - return { abs: real, rel: segments.join('/'), segments }; -} - -function parentsOf(segments) { - const out = []; - let acc = ''; - for (const s of segments) { - acc = acc ? `${acc}/${s}` : s; - out.push({ name: s, path: acc }); - } - return out; -} - -function rootListing() { - const dirs = []; - const files = []; - for (const name of ALLOWED_ROOTS) { - const abs = path.join(ROOT, name); - if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) dirs.push({ name, path: name }); - } - for (const name of ALLOWED_ROOT_FILES) { - const abs = path.join(ROOT, name); - if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue; - files.push({ name, path: name, size: fs.statSync(abs).size, ext: extOf(name), viewable: isViewable(name) }); - } - return { path: '', parents: [], dirs, files, hidden: 0 }; -} - -function walkFiles(relDir, onFile) { - let entries; - try { entries = fs.readdirSync(path.resolve(ROOT, relDir), { withFileTypes: true }); } catch (_) { return true; } - for (const e of entries) { - if (isDeniedEntry(e.name)) continue; - const rel = relDir ? `${relDir}/${e.name}` : e.name; - if (e.isSymbolicLink()) continue; - if (e.isDirectory()) { if (walkFiles(rel, onFile) === false) return false; continue; } - if (!e.isFile()) continue; - if (onFile(rel, e.name) === false) return false; - } - return true; -} - -const MODULE_ALIASES = { - ai: { model: 'src/AI/ai_service.mjs', view: 'src/views/AI_view.js' }, - aiNav: { model: 'src/AI/routes_index.js', view: 'src/views/AI_view.js', paths: ['/ai/ask'] }, - docs: { paths: ['/documents'] }, - blogs: { model: 'src/models/blog_model.js', view: 'src/views/blog_view.js', paths: ['/blogs'] }, - polls: { model: 'src/models/polls_model.js', view: 'src/views/polls_view.js', paths: ['/polls'] }, - data: { model: 'src/models/data_model.js', view: 'src/views/data_view.js', paths: ['/data'] }, - invites: { model: 'src/models/main_models.js', view: 'src/views/invites_view.js' }, - graphos: { model: 'src/models/main_models.js', view: 'src/views/graphos_view.js' } -}; - -const GROUPS = { - translations: ['src/client/assets/translations'], - styles: ['src/client/assets/styles', 'src/client/assets/themes'], - tests: ['test'] -}; - -module.exports = { - ROOT, - PAGE_LINES, - GROUPS, - - listGroup(key) { - const roots = GROUPS[String(key || '')]; - if (!roots) throw new Error('Not found'); - const dirs = []; - const files = []; - for (const rel of roots) { - let listing; - try { listing = module.exports.listTree(rel); } catch (_) { continue; } - dirs.push(...listing.dirs); - files.push(...listing.files); - } - dirs.sort((a, b) => a.name.localeCompare(b.name)); - files.sort((a, b) => a.name.localeCompare(b.name)); - return { group: String(key), path: '', parents: [], dirs, files, hidden: 0 }; - }, - - listTree(relPath) { - const segments = splitPath(relPath); - if (!segments.length) return rootListing(); - const target = resolveSafe(relPath); - const stat = fs.statSync(target.abs); - if (!stat.isDirectory()) throw new Error('Not a directory'); - const entries = fs.readdirSync(target.abs, { withFileTypes: true }); - const dirs = []; - const files = []; - let hidden = 0; - for (const e of entries) { - if (isDeniedEntry(e.name) || e.isSymbolicLink()) { hidden++; continue; } - const rel = `${target.rel}/${e.name}`; - if (e.isDirectory()) { dirs.push({ name: e.name, path: rel }); continue; } - if (!e.isFile()) { hidden++; continue; } - let size = 0; - try { size = fs.statSync(path.join(target.abs, e.name)).size; } catch (_) { size = 0; } - files.push({ name: e.name, path: rel, size, ext: extOf(e.name), viewable: isViewable(e.name) }); - } - dirs.sort((a, b) => a.name.localeCompare(b.name)); - files.sort((a, b) => a.name.localeCompare(b.name)); - return { path: target.rel, parents: parentsOf(target.segments), dirs, files, hidden }; - }, - - readFile(relPath, opts = {}) { - const target = resolveSafe(relPath); - const stat = fs.statSync(target.abs); - if (!stat.isFile()) throw new Error('Not a file'); - if (!isViewable(path.basename(target.abs))) throw new Error('This file type cannot be displayed'); - if (stat.size > MAX_VIEW_BYTES) throw new Error('This file is too large to be displayed'); - const all = fs.readFileSync(target.abs, 'utf8').split('\n'); - const totalLines = all.length; - const requested = parseInt(opts.from, 10); - const from = Math.min(Math.max(Number.isFinite(requested) ? requested : 1, 1), Math.max(1, totalLines)); - const to = Math.min(from + PAGE_LINES - 1, totalLines); - const lines = []; - for (let n = from; n <= to; n++) lines.push({ n, text: all[n - 1] }); - return { - path: target.rel, - parents: parentsOf(target.segments), - name: path.basename(target.abs), - size: stat.size, - totalLines, - from, - to, - pageLines: PAGE_LINES, - hasPrev: from > 1, - hasNext: to < totalLines, - lines - }; - }, - - rawFile(relPath) { - const target = resolveSafe(relPath); - const stat = fs.statSync(target.abs); - if (!stat.isFile()) throw new Error('Not a file'); - if (!isViewable(path.basename(target.abs))) throw new Error('This file type cannot be displayed'); - if (stat.size > MAX_VIEW_BYTES) throw new Error('This file is too large to be displayed'); - return { name: path.basename(target.abs), content: fs.readFileSync(target.abs, 'utf8') }; - }, - - searchCode(query, opts = {}) { - const q = String(query || '').trim(); - if (q.length < 2) return { query: q, results: [], truncated: false, scanned: 0 }; - const wanted = String(opts.ext || '').trim().toLowerCase(); - const max = Math.min(Math.max(parseInt(opts.max, 10) || SEARCH_MAX, 1), SEARCH_MAX); - const needle = q.toLowerCase(); - const results = []; - let scanned = 0; - let truncated = false; - for (const root of ALLOWED_ROOTS) { - if (truncated) break; - walkFiles(root, (rel, name) => { - if (!isViewable(name)) return true; - if (wanted && extOf(name) !== wanted) return true; - let stat; - try { stat = fs.statSync(path.resolve(ROOT, rel)); } catch (_) { return true; } - if (stat.size > MAX_VIEW_BYTES) return true; - scanned++; - let content; - try { content = fs.readFileSync(path.resolve(ROOT, rel), 'utf8'); } catch (_) { return true; } - if (content.toLowerCase().indexOf(needle) < 0) return true; - const lines = content.split('\n'); - for (let i = 0; i < lines.length; i++) { - if (lines[i].toLowerCase().indexOf(needle) < 0) continue; - results.push({ path: rel, line: i + 1, text: lines[i].trim().slice(0, SEARCH_SNIPPET) }); - if (results.length >= max) { truncated = true; return false; } - } - return true; - }); - } - return { query: q, results, truncated, scanned }; - }, - - moduleMap() { - const cfg = getConfig(); - const mods = Object.keys((cfg && cfg.modules) || {}).map(k => k.replace(/Mod$/, '')).sort(); - let backend = ''; - try { backend = fs.readFileSync(path.join(ROOT, 'src', 'backend', 'backend.js'), 'utf8'); } catch (_) { backend = ''; } - const modelFiles = fs.existsSync(path.join(ROOT, 'src', 'models')) ? fs.readdirSync(path.join(ROOT, 'src', 'models')) : []; - const viewFiles = fs.existsSync(path.join(ROOT, 'src', 'views')) ? fs.readdirSync(path.join(ROOT, 'src', 'views')) : []; - const stem = (s) => String(s || '').toLowerCase().replace(/s$/, ''); - const pick = (files, name, suffixes) => { - for (const suf of suffixes) { - const exact = files.find(f => f.toLowerCase() === `${name.toLowerCase()}${suf}`); - if (exact) return exact; - } - const singular = name.replace(/s$/, ''); - for (const suf of suffixes) { - const short = files.find(f => f.toLowerCase() === `${singular.toLowerCase()}${suf}`); - if (short) return short; - } - const target = stem(name); - return files.find(f => { - const suf = suffixes.find(x => f.toLowerCase().endsWith(x)); - if (!suf) return false; - const base = stem(f.slice(0, f.length - suf.length)); - return base.startsWith(target) || target.startsWith(base); - }) || null; - }; - const testsIndex = new Map(); - const modsDir = path.join(ROOT, 'test', 'mods'); - const indexTests = (dir, prefix) => { - let entries; - try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; } - for (const e of entries) { - if (!e.isDirectory()) continue; - const rel = `${prefix}/${e.name}`; - if (!testsIndex.has(e.name)) testsIndex.set(e.name, rel); - indexTests(path.join(dir, e.name), rel); - } - }; - indexTests(modsDir, 'test/mods'); - return mods.map(name => { - const alias = MODULE_ALIASES[name] || {}; - const routePaths = Array.isArray(alias.paths) && alias.paths.length ? alias.paths : [`/${name}`]; - let routes = 0; - for (const routePath of routePaths) { - const escaped = routePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const re = new RegExp(`\\.(get|post)\\(\\s*["'\`]${escaped}(?=["'/?])`, 'g'); - routes += backend ? (backend.match(re) || []).length : 0; - } - const model = alias.model !== undefined ? alias.model : (pick(modelFiles, name, ['_model.js', '_models.js']) ? `src/models/${pick(modelFiles, name, ['_model.js', '_models.js'])}` : null); - const view = alias.view !== undefined ? alias.view : (pick(viewFiles, name, ['_view.js', '_views.js']) ? `src/views/${pick(viewFiles, name, ['_view.js', '_views.js'])}` : null); - return { - name, - enabled: (cfg.modules || {})[`${name}Mod`] !== 'off', - model, - view, - routes, - tests: testsIndex.get(name) || testsIndex.get(name.replace(/s$/, '')) || (() => { - const target = stem(name); - for (const [dir, rel] of testsIndex) { - const base = stem(dir); - if (base.startsWith(target) || target.startsWith(base)) return rel; - } - return null; - })() - }; - }); - }, - - projectStats() { - const now = Date.now(); - if (statsCache && now - statsCacheAt < STATS_TTL) return statsCache; - let files = 0; - let lines = 0; - let bytes = 0; - for (const root of ALLOWED_ROOTS) { - walkFiles(root, (rel, name) => { - if (!isViewable(name)) return true; - try { - const abs = path.resolve(ROOT, rel); - const stat = fs.statSync(abs); - if (stat.size > MAX_VIEW_BYTES) return true; - files++; - bytes += stat.size; - lines += fs.readFileSync(abs, 'utf8').split('\n').length; - } catch (_) {} - return true; - }); - } - const cfg = getConfig(); - const modules = Object.keys((cfg && cfg.modules) || {}).length; - const translations = path.join(ROOT, 'src', 'client', 'assets', 'translations'); - const languages = fs.existsSync(translations) ? fs.readdirSync(translations).filter(f => /^oasis_[a-z]{2}\.js$/.test(f)).length : 0; - const testsDir = path.join(ROOT, 'test', 'mods'); - const testMods = fs.existsSync(testsDir) ? fs.readdirSync(testsDir).length : 0; - let version = ''; - try { version = String(JSON.parse(fs.readFileSync(path.join(ROOT, 'src', 'server', 'package.json'), 'utf8')).version || ''); } catch (_) { version = ''; } - statsCache = { files, lines, bytes, modules, languages, testMods, version, node: process.version }; - statsCacheAt = now; - return statsCache; - } -}; diff --git a/src/models/documents_model.js b/src/models/documents_model.js index b6affb1..ada7c64 100644 --- a/src/models/documents_model.js +++ b/src/models/documents_model.js @@ -3,7 +3,7 @@ const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeBy, norm } = require('../backend/dedupe'); -const contentFavorites = require("../backend/content_favorites"); +const mediaFavorites = require("../backend/media-favorites"); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -154,7 +154,7 @@ module.exports = ({ cooler }) => { const favoritesSetForDocuments = async () => { try { - return await contentFavorites.getFavoriteSet("documents"); + return await mediaFavorites.getFavoriteSet("documents"); } catch { return new Set(); } diff --git a/src/models/events_model.js b/src/models/events_model.js index 9f5fd96..fb6513d 100644 --- a/src/models/events_model.js +++ b/src/models/events_model.js @@ -1,7 +1,5 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); -const { normalizeImages, normalizeVideo } = require('./media_gallery'); -const { truthy, hasAnyInterval, nextOccurrence, upcomingOccurrences } = require('./recurrence'); const crypto = require('crypto'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { getConfig } = require('../configs/config-manager.js'); @@ -74,22 +72,8 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { return m.toISOString(); }; - const recurrenceOf = (c) => ({ - weekly: truthy(c.intervalWeekly), - monthly: truthy(c.intervalMonthly), - yearly: truthy(c.intervalYearly), - until: c.recurrenceUntil || '' - }); - - const effectiveDate = (c) => { - const r = recurrenceOf(c); - if (!hasAnyInterval(r.weekly, r.monthly, r.yearly) || !r.until) return c.date; - const next = nextOccurrence(c.date, r.until, r.weekly, r.monthly, r.yearly); - return next ? next.toISOString() : c.date; - }; - const deriveStatus = (c) => { - const dateM = moment(effectiveDate(c)); + const dateM = moment(c.date); let status = String(c.status || 'OPEN').toUpperCase(); if (dateM.isValid() && dateM.isBefore(moment())) status = 'CLOSED'; if (status !== 'OPEN' && status !== 'CLOSED') status = 'OPEN'; @@ -187,7 +171,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { return idx.rootOf(id); }, - async createEvent(title, description, date, location, price = 0, url = "", attendees = [], tagsRaw = [], isPublic, mapUrl = "", clearnetPublic = false, media = {}, recurrence = {}) { + async createEvent(title, description, date, location, price = 0, url = "", attendees = [], tagsRaw = [], isPublic, mapUrl = "", clearnetPublic = false) { const ssbClient = await openSsb(); const userId = await me(); @@ -219,12 +203,6 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { isPublic: visibility, mapUrl: String(mapUrl || "").trim(), clearnetPublic: clearnetPublic === true || clearnetPublic === 'true' || clearnetPublic === 'on', - images: normalizeImages(media && media.images), - video: normalizeVideo(media && media.video), - intervalWeekly: truthy(recurrence && recurrence.weekly), - intervalMonthly: truthy(recurrence && recurrence.monthly), - intervalYearly: truthy(recurrence && recurrence.yearly), - recurrenceUntil: String((recurrence && recurrence.until) || '').trim(), opinions: {}, opinions_inhabitants: [] }; @@ -483,15 +461,6 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { status, isPublic: normalizePrivacy(c.isPublic), mapUrl: c.mapUrl || "", - images: normalizeImages(c.images), - video: normalizeVideo(c.video), - intervalWeekly: truthy(c.intervalWeekly), - intervalMonthly: truthy(c.intervalMonthly), - intervalYearly: truthy(c.intervalYearly), - recurrenceUntil: c.recurrenceUntil || '', - recurring: hasAnyInterval(truthy(c.intervalWeekly), truthy(c.intervalMonthly), truthy(c.intervalYearly)) && !!c.recurrenceUntil, - nextDate: effectiveDate(c), - occurrences: upcomingOccurrences(c.date, c.recurrenceUntil, truthy(c.intervalWeekly), truthy(c.intervalMonthly), truthy(c.intervalYearly)).map(d => d.toISOString()), clearnetPublic: !!c.clearnetPublic, encrypted: normalizePrivacy(c.isPublic) === 'private', opinions: agg.opinions, @@ -534,12 +503,6 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { isPublic: updatedData.isPublic !== undefined ? normalizePrivacy(updatedData.isPublic) : normalizePrivacy(c.isPublic), clearnetPublic: updatedData.clearnetPublic !== undefined ? (updatedData.clearnetPublic === true || updatedData.clearnetPublic === 'true' || updatedData.clearnetPublic === 'on') : !!c.clearnetPublic, attendees: uniq(Array.isArray(c.attendees) ? c.attendees : []), - images: normalizeImages(updatedData.images !== undefined ? updatedData.images : c.images), - video: updatedData.video !== undefined ? normalizeVideo(updatedData.video) : normalizeVideo(c.video), - intervalWeekly: updatedData.intervalWeekly !== undefined ? truthy(updatedData.intervalWeekly) : truthy(c.intervalWeekly), - intervalMonthly: updatedData.intervalMonthly !== undefined ? truthy(updatedData.intervalMonthly) : truthy(c.intervalMonthly), - intervalYearly: updatedData.intervalYearly !== undefined ? truthy(updatedData.intervalYearly) : truthy(c.intervalYearly), - recurrenceUntil: updatedData.recurrenceUntil !== undefined ? String(updatedData.recurrenceUntil || '').trim() : (c.recurrenceUntil || ''), updatedAt: new Date().toISOString(), replaces: eventId }; @@ -647,14 +610,6 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { status, isPublic: normalizePrivacy(c.isPublic), mapUrl: c.mapUrl || "", - images: normalizeImages(c.images), - video: normalizeVideo(c.video), - intervalWeekly: truthy(c.intervalWeekly), - intervalMonthly: truthy(c.intervalMonthly), - intervalYearly: truthy(c.intervalYearly), - recurrenceUntil: c.recurrenceUntil || '', - recurring: hasAnyInterval(truthy(c.intervalWeekly), truthy(c.intervalMonthly), truthy(c.intervalYearly)) && !!c.recurrenceUntil, - nextDate: effectiveDate(c), encrypted: normalizePrivacy(c.isPublic) === 'private', opinions: agg.opinions, opinions_inhabitants: agg.opinions_inhabitants diff --git a/src/models/exportmode_model.js b/src/models/exportmode_model.js index 52b20cb..cd535d5 100644 --- a/src/models/exportmode_model.js +++ b/src/models/exportmode_model.js @@ -6,8 +6,8 @@ const archiver = require('../server/node_modules/archiver'); module.exports = { exportSSB: async (outputPath) => { try { - // se exporta la cuenta activa, no siempre la de por defecto - const ssbPath = require('../server/ssb_config').path; + const homeDir = os.homedir(); + const ssbPath = path.join(homeDir, '.ssb'); const output = fs.createWriteStream(outputPath); const archive = archiver('zip', { zlib: { level: 9 } diff --git a/src/models/favorites_model.js b/src/models/favorites_model.js index 1eb5524..d0fd3f5 100644 --- a/src/models/favorites_model.js +++ b/src/models/favorites_model.js @@ -1,4 +1,4 @@ -const contentFavorites = require("../backend/content_favorites"); +const mediaFavorites = require("../backend/media-favorites"); const safeArr = (v) => (Array.isArray(v) ? v : []); const safeText = (v) => String(v || "").trim(); @@ -15,7 +15,7 @@ const toTs = (d) => { return Number.isFinite(t) ? t : 0; }; -module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, videosModel, mapsModel, padsModel, chatsModel, calendarsModel, torrentsModel, marketModel, shopsModel, eventsModel, tasksModel, reportsModel, votesModel, jobsModel, housingModel, projectsModel, transfersModel, forumModel, blogsModel, pollsModel }) => { +module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, videosModel, mapsModel, padsModel, chatsModel, calendarsModel, torrentsModel, marketModel, shopsModel }) => { const kindConfig = { audios: { base: "/audios/", @@ -64,58 +64,10 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi shopProducts: { base: "/shops/product/", getById: getFn(shopsModel, ["getProductById"]) - }, - events: { - base: "/events/", - getById: getFn(eventsModel, ["getEventById", "getById"]) - }, - tasks: { - base: "/tasks/", - getById: getFn(tasksModel, ["getTaskById", "getById"]) - }, - reports: { - base: "/reports/", - getById: getFn(reportsModel, ["getReportById", "getById"]) - }, - votes: { - base: "/votes/", - getById: getFn(votesModel, ["getVoteById", "getById"]) - }, - jobs: { - base: "/jobs/", - getById: getFn(jobsModel, ["getJobById", "getById"]) - }, - housing: { - base: "/housing/", - getById: getFn(housingModel, ["getHousingById", "getById"]) - }, - projects: { - base: "/projects/", - getById: getFn(projectsModel, ["getProjectById", "getById"]) - }, - transfers: { - base: "/transfers/", - getById: getFn(transfersModel, ["getTransferById", "getById"]) - }, - forum: { - base: "/forum/", - getById: getFn(forumModel, ["getForumById", "getById"]) - }, - blogs: { - base: "/blogs/", - getById: getFn(blogsModel, ["getBlogById", "getById"]) - }, - polls: { - base: "/polls/", - getById: getFn(pollsModel, ["getPollById", "getById"]) - }, - shops: { - base: "/shops/", - getById: getFn(shopsModel, ["getShopById", "getById"]) } }; - const kindOrder = ["audios", "blogs", "bookmarks", "calendars", "chats", "documents", "events", "forum", "housing", "images", "jobs", "maps", "market", "pads", "polls", "projects", "reports", "shopProducts", "shops", "tasks", "torrents", "transfers", "videos", "votes"]; + const kindOrder = ["audios", "bookmarks", "calendars", "chats", "documents", "images", "maps", "pads", "torrents", "videos", "market", "shopProducts"]; const hydrateKind = async (kind, ids) => { const cfg = kindConfig[kind]; @@ -127,11 +79,9 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi if (!id) return null; try { const obj = await cfg.getById(id); - if (!obj || typeof obj !== "object") return null; const viewId = safeText(obj?.key || obj?.id || id); return { - content: obj, kind, favId: id, viewHref: `${cfg.base}${encodeURIComponent(viewId)}`, @@ -154,7 +104,7 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi }; const loadAll = async () => { - const sets = await Promise.all(kindOrder.map((k) => contentFavorites.getFavoriteSet(k))); + const sets = await Promise.all(kindOrder.map((k) => mediaFavorites.getFavoriteSet(k))); const idsByKind = {}; kindOrder.forEach((k, i) => { idsByKind[k] = Array.from(sets[i] || []); @@ -168,8 +118,21 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi const flat = kindOrder.flatMap((k) => byKind[k]); - const counts = { all: flat.length }; - for (const k of kindOrder) counts[k] = (byKind[k] || []).length; + const counts = { + audios: byKind.audios.length, + bookmarks: byKind.bookmarks.length, + calendars: byKind.calendars.length, + chats: byKind.chats.length, + documents: byKind.documents.length, + images: byKind.images.length, + maps: byKind.maps.length, + pads: byKind.pads.length, + torrents: byKind.torrents.length, + videos: byKind.videos.length, + market: byKind.market.length, + shopProducts: byKind.shopProducts.length, + all: flat.length + }; const recentFlat = flat .slice() @@ -204,13 +167,11 @@ module.exports = ({ audiosModel, bookmarksModel, documentsModel, imagesModel, vi return { items: grouped, counts }; }, - kinds: kindOrder.slice(), - async removeFavorite(kind, id) { const k = safeText(kind); const favId = safeText(id); if (!k || !favId) return; - await contentFavorites.removeFavorite(k, favId); + await mediaFavorites.removeFavorite(k, favId); } }; }; diff --git a/src/models/fileshare_model.js b/src/models/fileshare_model.js deleted file mode 100644 index a275fa6..0000000 --- a/src/models/fileshare_model.js +++ /dev/null @@ -1,200 +0,0 @@ -const pull = require('../server/node_modules/pull-stream'); -const fs = require('fs'); -const crypto = require('crypto'); -const { Readable } = require('stream'); -const fsc = require('../backend/fileshare_crypto'); - -const MAX_CHUNK_SIZE = 5 * 1024 * 1024; - -module.exports = ({ cooler }) => { - let ssb; - const openSsb = async () => { - if (!ssb) ssb = await cooler.open(); - return ssb; - }; - - const addBlob = (client, buffer) => - new Promise((resolve, reject) => { - pull(pull.values([buffer]), client.blobs.add((err, ref) => err ? reject(err) : resolve(ref))); - }); - - const getBlob = (client, ref) => - new Promise((resolve, reject) => { - pull(client.blobs.get(ref), pull.collect((err, chunks) => err ? reject(err) : resolve(Buffer.concat(chunks)))); - }); - - const hasBlob = (client, ref) => - new Promise((resolve) => { - if (!client.blobs || typeof client.blobs.has !== 'function') return resolve(false); - client.blobs.has(ref, (err, has) => resolve(!err && !!has)); - }); - - const wantBlob = (client, ref, timeoutMs) => - new Promise((resolve) => { - if (typeof client.blobs.want !== 'function') return resolve(true); - let done = false; - const finish = (ok) => { if (done) return; done = true; if (timer) clearTimeout(timer); resolve(ok); }; - const timer = timeoutMs ? setTimeout(() => finish(false), timeoutMs) : null; - client.blobs.want(ref, (err) => finish(!err)); - }); - - const rmBlob = (client, ref) => - new Promise((resolve) => { - if (typeof client.blobs.rm !== 'function') return resolve(false); - client.blobs.rm(ref, (err) => resolve(!err)); - }); - - const createShareFromBuffer = async ({ buffer, filename, mime, chunkSize } = {}) => { - if (!Buffer.isBuffer(buffer)) throw new Error('buffer required'); - const client = await openSsb(); - const key = fsc.generateFileKey(); - const size = fsc.DEFAULT_CHUNK_SIZE; - const useChunk = Math.min(Number(chunkSize) || fsc.DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE); - - const chunkHashes = []; - for (const part of fsc.splitBuffer(buffer, useChunk)) { - const payload = fsc.encryptChunk(part, key); - chunkHashes.push(await addBlob(client, payload)); - } - - const manifest = fsc.buildManifest({ - filename, mime, size: buffer.length, chunkSize: useChunk, - chunkHashes, plainSha256: fsc.sha256Hex(buffer) - }); - const manifestBlobId = await addBlob(client, fsc.encryptManifest(manifest, key)); - - return { - type: 'fileShare', - v: 1, - key: fsc.keyToHex(key), - manifestBlobId, - filename: manifest.filename, - mime: manifest.mime, - size: manifest.size, - chunkCount: chunkHashes.length - }; - }; - - const createShareFromFile = async ({ filepath, filename, mime, chunkSize } = {}) => { - if (!filepath) throw new Error('filepath required'); - const client = await openSsb(); - const key = fsc.generateFileKey(); - const useChunk = Math.min(Number(chunkSize) || fsc.DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE); - const chunkHashes = []; - const hashAll = crypto.createHash('sha256'); - let total = 0; - let carry = Buffer.alloc(0); - const flush = async (part) => { - hashAll.update(part); - total += part.length; - chunkHashes.push(await addBlob(client, fsc.encryptChunk(part, key))); - }; - for await (const data of fs.createReadStream(filepath, { highWaterMark: useChunk })) { - carry = carry.length ? Buffer.concat([carry, data]) : data; - while (carry.length >= useChunk) { - await flush(carry.subarray(0, useChunk)); - carry = carry.subarray(useChunk); - } - } - if (carry.length || !chunkHashes.length) await flush(carry); - - const manifest = fsc.buildManifest({ - filename, mime, size: total, chunkSize: useChunk, - chunkHashes, plainSha256: hashAll.digest('hex') - }); - const manifestBlobId = await addBlob(client, fsc.encryptManifest(manifest, key)); - - return { - type: 'fileShare', v: 1, - key: fsc.keyToHex(key), manifestBlobId, - filename: manifest.filename, mime: manifest.mime, size: manifest.size, - chunkCount: chunkHashes.length - }; - }; - - const openManifest = async (pointer) => { - const client = await openSsb(); - const key = fsc.keyFromHex(pointer.key); - await wantBlob(client, pointer.manifestBlobId); - const payload = await getBlob(client, pointer.manifestBlobId); - return fsc.decryptManifest(payload, key); - }; - - const CHUNK_WANT_TIMEOUT = 30000; - const PREFLIGHT_TIMEOUT = 12000; - - const readShareStream = (pointer) => { - const key = fsc.keyFromHex(pointer.key); - async function* gen() { - const client = await openSsb(); - if (!(await wantBlob(client, pointer.manifestBlobId, CHUNK_WANT_TIMEOUT))) throw new Error('fileshare blob unavailable'); - const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key); - for (const ref of manifest.chunks) { - if (!(await wantBlob(client, ref, CHUNK_WANT_TIMEOUT))) throw new Error('fileshare blob unavailable'); - yield fsc.decryptChunk(await getBlob(client, ref), key); - } - } - return Readable.from(gen()); - }; - - const ensureAvailable = async (pointer, timeoutMs = PREFLIGHT_TIMEOUT) => { - try { - const client = await openSsb(); - const key = fsc.keyFromHex(pointer.key); - if (!(await wantBlob(client, pointer.manifestBlobId, timeoutMs))) return false; - const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key); - if (manifest.chunks.length && !(await wantBlob(client, manifest.chunks[0], timeoutMs))) return false; - return true; - } catch (_) { - return false; - } - }; - - const reassembleToBuffer = async (pointer) => { - const parts = []; - for await (const chunk of readShareStream(pointer)) parts.push(chunk); - return Buffer.concat(parts); - }; - - const isAvailable = async (pointer) => { - const client = await openSsb(); - if (!(await hasBlob(client, pointer.manifestBlobId))) return false; - try { - const key = fsc.keyFromHex(pointer.key); - const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key); - for (const ref of manifest.chunks) if (!(await hasBlob(client, ref))) return false; - return true; - } catch (_) { - return false; - } - }; - - const removeLocalBlobs = async (pointer) => { - const client = await openSsb(); - let removed = 0; - try { - const key = fsc.keyFromHex(pointer.key); - if (await hasBlob(client, pointer.manifestBlobId)) { - const manifest = fsc.decryptManifest(await getBlob(client, pointer.manifestBlobId), key); - for (const ref of manifest.chunks) if (await rmBlob(client, ref)) removed++; - } - } catch (_) {} - if (await rmBlob(client, pointer.manifestBlobId)) removed++; - return removed; - }; - - const pruneExpired = async (sentFileShares, ttlMs, now) => { - const cutoff = (Number.isFinite(now) ? now : Date.now()) - (Number(ttlMs) || 0); - let pruned = 0; - for (const item of (Array.isArray(sentFileShares) ? sentFileShares : [])) { - if (!item || !item.pointer) continue; - const sentAt = new Date(item.sentAt || 0).getTime(); - if (!(sentAt <= cutoff)) continue; - const n = await removeLocalBlobs(item.pointer); - if (n > 0) pruned++; - } - return pruned; - }; - - return { createShareFromBuffer, createShareFromFile, openManifest, readShareStream, reassembleToBuffer, isAvailable, ensureAvailable, removeLocalBlobs, pruneExpired }; -}; diff --git a/src/models/housing_model.js b/src/models/housing_model.js deleted file mode 100644 index 0b7bbb3..0000000 --- a/src/models/housing_model.js +++ /dev/null @@ -1,554 +0,0 @@ -const pull = require("../server/node_modules/pull-stream") -const moment = require("../server/node_modules/moment") -const categories = require("../backend/opinion_categories") -const { getConfig } = require("../configs/config-manager.js") -const { dedupeByPreferring } = require('../backend/dedupe') -const logLimit = getConfig().ssbLogStream?.limit || 1000 - -const HOUSING_TYPES = ["sale", "rent", "couchsurfing"] -const PROPERTY_TYPES = ["apartment", "house", "room", "land", "other"] - -const norm = (s) => String(s || "").trim().toLowerCase() -const safeArr = (v) => (Array.isArray(v) ? v : []) - -const toNum = (v) => { - const n = parseFloat(String(v ?? "").replace(",", ".")) - return Number.isFinite(n) ? n : NaN -} - -const toInt = (v, fallback = 0) => { - const n = parseInt(String(v ?? ""), 10) - return Number.isFinite(n) ? n : fallback -} - -const nonNeg = (v) => { - const n = toNum(v) - return Number.isFinite(n) && n >= 0 ? n : 0 -} - -const safeDate = (v) => { - const s = String(v || "").trim() - if (!s) return "" - const d = new Date(s) - return Number.isFinite(d.getTime()) ? s : "" -} - -const normalizeTags = (raw) => { - if (raw === undefined || raw === null) return [] - if (Array.isArray(raw)) return raw.map(t => String(t || "").trim()).filter(Boolean) - return String(raw).split(",").map(t => t.trim()).filter(Boolean) -} - -const safeType = (v) => { - const t = norm(v) - return HOUSING_TYPES.includes(t) ? t : "" -} - -const safeProperty = (v) => { - const t = norm(v) - return PROPERTY_TYPES.includes(t) ? t : "other" -} - -const { MAX_IMAGES, normalizeImages, normalizeVideo } = require('./media_gallery') - -const matchSearch = (item, q) => { - const qq = norm(q) - if (!qq) return true - const hay = [ - item.title, - item.description, - item.place, - item.rules, - item.housing_type, - item.property_type, - safeArr(item.tags).join(" ") - ].map(x => norm(x)).join(" ") - return hay.includes(qq) -} - -module.exports = ({ cooler, tribeCrypto }) => { - let ssb - const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } - - const isEncrypted = (c) => !!(c && c.encryptedPayload) - - const keysForRoot = (rootId) => { - if (!tribeCrypto || !rootId) return [] - const ks = (tribeCrypto.getKeys && tribeCrypto.getKeys(rootId)) || [] - if (ks.length) return ks - const k = tribeCrypto.getKey ? tribeCrypto.getKey(rootId) : null - return k ? [k] : [] - } - - const decryptHousing = (c, rootId) => { - if (!isEncrypted(c)) return c - if (!tribeCrypto) return { ...c, _undecryptable: true } - const keys = keysForRoot(rootId) - if (!keys.length) return { ...c, _undecryptable: true } - return tribeCrypto.decryptContent(c, keys.map(k => [k])) - } - - const publishHousing = (ssbClient, content, rootId) => new Promise((resolve, reject) => { - const hidden = String(content.visibility || "PUBLIC").toUpperCase() === "HIDDEN" - if (!hidden || !tribeCrypto) { - ssbClient.publish(content, (err, msg) => { - if (err) return reject(err) - if (msg && msg.key && tribeCrypto && !rootId) { - try { tribeCrypto.setKey(msg.key, tribeCrypto.generateTribeKey(), 1) } catch (_) {} - } - resolve(msg) - }) - return - } - let key = rootId ? (keysForRoot(rootId)[0] || null) : null - if (!key) key = tribeCrypto.generateTribeKey() - let envelope - try { envelope = tribeCrypto.encryptContent(content, [key], true) } catch (err) { return reject(err) } - ssbClient.publish(envelope, (err, msg) => { - if (err) return reject(err) - try { tribeCrypto.setKey(rootId || (msg && msg.key), key, 1) } catch (_) {} - resolve(msg) - }) - }) - - const readAll = async (ssbClient) => - new Promise((resolve, reject) => - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)) - ) - ) - - const buildIndex = (messages, ssbClient) => { - const tomb = new Set() - const nodes = new Map() - const parent = new Map() - const child = new Map() - const naiveReplaces = new Map() - const requestLatest = new Map() - const opinionMsgs = [] - - for (const m of messages) { - const key = m.key - const v = m.value || {} - const c = v.content - if (!c || typeof c !== "object") continue - - if (c.type === "tombstone" && c.target) { tomb.add(c.target); continue } - - if (c.type === "housing") { - nodes.set(key, { key, ts: v.timestamp || m.timestamp || 0, c, author: v.author }) - if (c.replaces) naiveReplaces.set(key, c.replaces) - continue - } - - if (c.type === "housingOpinion" && c.target) { - opinionMsgs.push({ target: c.target, author: v.author, category: c.category }) - continue - } - - if (c.type === "housingRequest" && c.housingId) { - const author = v.author - if (!author) continue - const ts = v.timestamp || m.timestamp || 0 - const k = `${c.housingId}::${author}` - const prev = requestLatest.get(k) - if (!prev || ts >= prev.ts) requestLatest.set(k, { ts, value: !!c.value, author, housingId: c.housingId }) - continue - } - } - - for (const [key, replacesId] of naiveReplaces.entries()) { - const node = nodes.get(key) - if (!node) continue - const orig = nodes.get(replacesId) - if (!orig) continue - if (String(orig.author) !== String(node.author)) { nodes.delete(key); continue } - parent.set(key, replacesId) - child.set(replacesId, key) - } - - const rootOf = (id) => { - let cur = id, guard = 0 - while (parent.has(cur) && guard++ < 100000) cur = parent.get(cur) - return cur - } - - const tipOf = (id) => { - let cur = id, guard = 0 - while (child.has(cur) && guard++ < 100000) cur = child.get(cur) - return cur - } - - const roots = new Set() - for (const id of nodes.keys()) roots.add(rootOf(id)) - - const tipByRoot = new Map() - for (const r of roots) tipByRoot.set(r, tipOf(r)) - - if (ssbClient) { - for (const m of messages) { - if (typeof m.value?.content !== 'string') continue - try { - const dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.value?.timestamp || m.timestamp || 0 }) - const c = dec?.value?.content - if (!c || c.type !== 'housingRequest' || !c.housingId) continue - const author = dec.value.author - if (!author) continue - const ts = dec.value.timestamp || m.timestamp || 0 - const k = `${c.housingId}::${author}` - const prev = requestLatest.get(k) - if (!prev || ts >= prev.ts) requestLatest.set(k, { ts, value: !!c.value, author, housingId: c.housingId }) - } catch {} - } - } - - const requestsByRoot = new Map() - const everRequestedByRoot = new Map() - for (const { housingId, author, value } of requestLatest.values()) { - if (!requestsByRoot.has(housingId)) requestsByRoot.set(housingId, new Set()) - if (!everRequestedByRoot.has(housingId)) everRequestedByRoot.set(housingId, new Set()) - everRequestedByRoot.get(housingId).add(author) - const set = requestsByRoot.get(housingId) - if (value) set.add(author) - else set.delete(author) - } - - const opinionsByRoot = new Map() - for (const op of opinionMsgs) { - if (!nodes.has(op.target)) continue - const r = rootOf(op.target) - if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []) - opinionsByRoot.get(r).push(op) - } - - const aggregateFor = (rootId, content, ownerId) => { - const opinions = { ...((content && content.opinions) || {}) } - const voters = safeArr(content && content.opinions_inhabitants).slice() - const voterSet = new Set(voters) - for (const op of (opinionsByRoot.get(rootId) || [])) { - if (!op.author || op.author === ownerId) continue - if (voterSet.has(op.author)) continue - if (!categories.includes(op.category)) continue - voterSet.add(op.author); voters.push(op.author) - opinions[op.category] = (opinions[op.category] || 0) + 1 - } - return { opinions, opinions_inhabitants: voters } - } - - return { tomb, nodes, parent, child, rootOf, tipOf, tipByRoot, requestsByRoot, everRequestedByRoot, aggregateFor } - } - - const buildObject = (node, rootId, idx, viewer) => { - const raw = node.c || {} - const c = decryptHousing(raw, rootId) - if (c._undecryptable) return null - const author = node.author || c.author - const requests = Array.from(idx.requestsByRoot.get(rootId) || []) - const visibleRequests = (author === viewer) ? requests : (requests.includes(viewer) ? [viewer] : []) - const agg = idx.aggregateFor(rootId, c, author) - return { - id: node.key, - rootId, - housing_type: safeType(c.housing_type), - property_type: safeProperty(c.property_type), - title: String(c.title || ""), - description: String(c.description || ""), - rules: String(c.rules || ""), - place: String(c.place || ""), - mapUrl: String(c.mapUrl || ""), - price: Number.isFinite(toNum(c.price)) ? toNum(c.price).toFixed(6) : "0.000000", - rooms: toInt(c.rooms, 0), - size: nonNeg(c.size), - capacity: toInt(c.capacity, 0), - availableFrom: safeDate(c.availableFrom), - availableTo: safeDate(c.availableTo), - images: normalizeImages(c.images && c.images.length ? c.images : c.image), - image: normalizeImages(c.images && c.images.length ? c.images : c.image)[0] || null, - video: normalizeVideo(c.video), - author, - createdAt: c.createdAt || new Date(node.ts).toISOString(), - updatedAt: c.updatedAt || null, - status: String(c.status || "OPEN").toUpperCase() === "CLOSED" ? "CLOSED" : "OPEN", - tags: Array.isArray(c.tags) ? c.tags : normalizeTags(c.tags), - requests: visibleRequests, - requestCount: requests.length, - opinions: agg.opinions, - opinions_inhabitants: agg.opinions_inhabitants, - requestedByViewer: requests.includes(viewer), - everRequestedByViewer: (idx.everRequestedByRoot.get(rootId) || new Set()).has(viewer), - ratedByViewer: agg.opinions_inhabitants.includes(viewer), - visibility: String(c.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC" - } - } - - const buildContent = (data, opts = {}) => { - const housing_type = safeType(data.housing_type) - if (!housing_type) throw new Error("Invalid housing type") - - const title = String(data.title || "").trim() - if (!title) throw new Error("Invalid title") - const description = String(data.description || "").trim() - if (!description) throw new Error("Invalid description") - - const availableFrom = safeDate(data.availableFrom) - const availableTo = safeDate(data.availableTo) - if (!availableFrom) throw new Error("The start date is required") - if (opts.enforceFutureStart) { - const [y, m, d] = String(availableFrom).split("-").map(Number) - const startsAt = new Date(y, (m || 1) - 1, d || 1) - const today = new Date() - today.setHours(0, 0, 0, 0) - if (startsAt < today) throw new Error("The start date cannot be earlier than today") - } - if (availableTo && new Date(availableTo) < new Date(availableFrom)) { - throw new Error("The end date cannot be earlier than the start date") - } - - const images = normalizeImages(data.images !== undefined ? data.images : data.image) - const video = normalizeVideo(data.video) - - return { - type: "housing", - housing_type, - property_type: safeProperty(data.property_type), - title, - description, - rules: String(data.rules || "").trim(), - place: String(data.place || "").trim(), - mapUrl: String(data.mapUrl || "").trim(), - price: housing_type === "couchsurfing" ? "0.000000" : nonNeg(data.price).toFixed(6), - rooms: Math.max(0, toInt(data.rooms, 0)), - size: nonNeg(data.size), - capacity: Math.max(0, toInt(data.capacity, 0)), - availableFrom, - availableTo, - images, - image: images[0] || null, - video, - tags: normalizeTags(data.tags), - visibility: String(data.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC" - } - } - - return { - type: "housing", - HOUSING_TYPES, - PROPERTY_TYPES, - MAX_IMAGES, - - async createHousing(data) { - const ssbClient = await openSsb() - const content = { - ...buildContent(data, { enforceFutureStart: true }), - author: ssbClient.id, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - status: "OPEN", - opinions: {}, - opinions_inhabitants: [] - } - return publishHousing(ssbClient, content, null) - }, - - async resolveCurrentId(id) { - const ssbClient = await openSsb() - const idx = buildIndex(await readAll(ssbClient), ssbClient) - const tip = idx.tipOf(id) - if (idx.tomb.has(tip)) throw new Error("Housing not found") - return tip - }, - - async resolveRootId(id) { - const ssbClient = await openSsb() - const idx = buildIndex(await readAll(ssbClient), ssbClient) - const tip = idx.tipOf(id) - if (idx.tomb.has(tip)) throw new Error("Housing not found") - return idx.rootOf(tip) - }, - - async updateHousing(id, data) { - const ssbClient = await openSsb() - const idx = buildIndex(await readAll(ssbClient), ssbClient) - const tipId = idx.tipOf(id) - if (idx.tomb.has(tipId)) throw new Error("Housing not found") - const node = idx.nodes.get(tipId) - if (!node || !node.c) throw new Error("Housing not found") - if ((node.author || node.c.author) !== ssbClient.id) throw new Error("Unauthorized") - - const rootId = idx.rootOf(tipId) - const current = decryptHousing(node.c, rootId) - if (current._undecryptable) throw new Error("Cannot decrypt this listing") - const startChanged = data.availableFrom !== undefined && safeDate(data.availableFrom) !== safeDate(current.availableFrom) - const merged = buildContent({ - housing_type: data.housing_type === undefined ? current.housing_type : data.housing_type, - property_type: data.property_type === undefined ? current.property_type : data.property_type, - title: data.title === undefined ? current.title : data.title, - description: data.description === undefined ? current.description : data.description, - rules: data.rules === undefined ? current.rules : data.rules, - place: data.place === undefined ? current.place : data.place, - mapUrl: data.mapUrl === undefined ? current.mapUrl : data.mapUrl, - price: data.price === undefined ? current.price : data.price, - rooms: data.rooms === undefined ? current.rooms : data.rooms, - size: data.size === undefined ? current.size : data.size, - capacity: data.capacity === undefined ? current.capacity : data.capacity, - availableFrom: data.availableFrom === undefined ? current.availableFrom : data.availableFrom, - availableTo: data.availableTo === undefined ? current.availableTo : data.availableTo, - images: data.images === undefined - ? (current.images && current.images.length ? current.images : current.image) - : data.images, - video: data.video === undefined ? current.video : data.video, - tags: data.tags === undefined ? current.tags : data.tags, - visibility: data.visibility === undefined ? current.visibility : data.visibility - }, { enforceFutureStart: startChanged }) - - let status = String(current.status || "OPEN").toUpperCase() - if (data.status !== undefined) { - const s = String(data.status || "").toUpperCase() - if (!["OPEN", "CLOSED"].includes(s)) throw new Error("Invalid status") - status = s - } - - const next = { - ...merged, - status, - author: current.author, - createdAt: current.createdAt, - updatedAt: new Date().toISOString(), - opinions: current.opinions || {}, - opinions_inhabitants: safeArr(current.opinions_inhabitants), - replaces: tipId - } - - const tomb = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: ssbClient.id } - await new Promise((res, rej) => ssbClient.publish(tomb, (e) => e ? rej(e) : res())) - return publishHousing(ssbClient, next, rootId) - }, - - async updateHousingStatus(id, status) { - return this.updateHousing(id, { status: String(status || "").toUpperCase() }) - }, - - async deleteHousing(id) { - const ssbClient = await openSsb() - const idx = buildIndex(await readAll(ssbClient), ssbClient) - const tipId = idx.tipOf(id) - if (idx.tomb.has(tipId)) throw new Error("Housing not found") - const node = idx.nodes.get(tipId) - if (!node) throw new Error("Housing not found") - if ((node.author || node.c.author) !== ssbClient.id) throw new Error("Unauthorized") - const tomb = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: ssbClient.id } - return new Promise((res, rej) => ssbClient.publish(tomb, (e, r) => e ? rej(e) : res(r))) - }, - - async requestHousing(id) { - const ssbClient = await openSsb() - const me = ssbClient.id - const item = await this.getHousingById(id) - if (item.author === me) throw new Error("Cannot request your own listing") - if (item.status !== "OPEN") throw new Error("This listing is closed") - if (safeArr(item.requests).includes(me)) return { alreadyRequested: true } - const msg = { type: "housingRequest", housingId: item.rootId, value: true, createdAt: new Date().toISOString() } - return new Promise((res, rej) => ssbClient.private.publish(msg, [me, item.author], (e, m) => e ? rej(e) : res(m))) - }, - - async cancelRequest(id) { - const ssbClient = await openSsb() - const me = ssbClient.id - const item = await this.getHousingById(id) - if (item.author === me) throw new Error("Cannot cancel a request on your own listing") - if (!safeArr(item.requests).includes(me)) return { notRequested: true } - const msg = { type: "housingRequest", housingId: item.rootId, value: false, createdAt: new Date().toISOString() } - return new Promise((res, rej) => ssbClient.private.publish(msg, [me, item.author], (e, m) => e ? rej(e) : res(m))) - }, - - async createOpinion(id, category) { - if (!categories.includes(category)) throw new Error("Invalid category") - const ssbClient = await openSsb() - const me = ssbClient.id - const idx = buildIndex(await readAll(ssbClient), ssbClient) - const tipId = idx.tipOf(id) - if (idx.tomb.has(tipId)) throw new Error("Housing not found") - const node = idx.nodes.get(tipId) - if (!node) throw new Error("Housing not found") - const rootId = idx.rootOf(tipId) - const owner = node.author || node.c.author - if (owner === me) throw new Error("You cannot rate your own listing") - - const agg = idx.aggregateFor(rootId, decryptHousing(node.c, rootId), owner) - if (agg.opinions_inhabitants.includes(me)) throw new Error("Already voted") - - const everRequested = idx.everRequestedByRoot.get(rootId) || new Set() - if (!everRequested.has(me)) throw new Error("You can rate only a place you have requested") - - const content = { type: "housingOpinion", target: rootId, category, createdAt: new Date().toISOString() } - return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) - }, - - async listHousing(filter = "ALL", viewerId = null, query = {}) { - const ssbClient = await openSsb() - const viewer = viewerId || ssbClient.id - const idx = buildIndex(await readAll(ssbClient), ssbClient) - - const items = [] - for (const [rootId, tipId] of idx.tipByRoot.entries()) { - if (idx.tomb.has(tipId)) continue - const node = idx.nodes.get(tipId) - if (!node) continue - const item = buildObject(node, rootId, idx, viewer) - if (!item) continue - if (item.visibility === "HIDDEN" && item.author !== viewer) continue - items.push(item) - } - - const F = String(filter || "ALL").toUpperCase() - let list = dedupeByPreferring( - items, - (h) => (h.author && h.createdAt) ? [norm(h.author), norm(h.createdAt), norm(h.title)].join("|") : null, - (h) => h.requestCount - ) - - if (F === "MINE") list = list.filter(h => h.author === viewer) - else if (F === "REQUESTED") list = list.filter(h => safeArr(h.requests).includes(viewer)) - else if (HOUSING_TYPES.includes(norm(F))) list = list.filter(h => h.housing_type === norm(F)) - else if (F === "OPEN") list = list.filter(h => h.status === "OPEN") - else if (F === "CLOSED") list = list.filter(h => h.status === "CLOSED") - else if (F === "RECENT") list = list.filter(h => moment(h.createdAt).isAfter(moment().subtract(24, "hours"))) - - const search = String(query.search || query.q || "").trim() - if (search) list = list.filter(h => matchSearch(h, search)) - - const minP = toNum(query.minPrice) - const maxP = toNum(query.maxPrice) - if (Number.isFinite(minP)) list = list.filter(h => toNum(h.price) >= minP) - if (Number.isFinite(maxP)) list = list.filter(h => toNum(h.price) <= maxP) - - const place = String(query.place || "").trim() - if (place) list = list.filter(h => norm(h.place).includes(norm(place))) - - const ratingOf = (h) => safeArr(h.opinions_inhabitants).length - const sort = String(query.sort || "").trim() - if (F === "TOP" || sort === "rating") list.sort((a, b) => ratingOf(b) - ratingOf(a)) - else if (sort === "price") list.sort((a, b) => toNum(a.price) - toNum(b.price)) - else if (sort === "requests") list.sort((a, b) => b.requestCount - a.requestCount) - else list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) - - return list - }, - - async getHousingById(id, viewerId = null) { - const ssbClient = await openSsb() - const viewer = viewerId || ssbClient.id - const idx = buildIndex(await readAll(ssbClient), ssbClient) - const tipId = idx.tipOf(id) - if (idx.tomb.has(tipId)) throw new Error("Housing not found") - const node = idx.nodes.get(tipId) - if (!node) throw new Error("Housing not found") - const rootId = idx.rootOf(tipId) - const item = buildObject(node, rootId, idx, viewer) - if (!item) throw new Error("Housing not found") - if (item.visibility === "HIDDEN" && item.author !== viewer) throw new Error("Housing not found") - return item - } - } -} diff --git a/src/models/images_model.js b/src/models/images_model.js index 494bd88..50c90e3 100644 --- a/src/models/images_model.js +++ b/src/models/images_model.js @@ -142,6 +142,7 @@ module.exports = ({ cooler }) => { title: c.title || "", description: c.description || "", mapUrl: c.mapUrl || "", + meme: !!c.meme, opinions: agg ? agg.opinions : (c.opinions || {}), opinions_inhabitants: voters, hasVoted: viewerId ? voters.includes(viewerId) : false, @@ -176,7 +177,7 @@ module.exports = ({ cooler }) => { return root; }, - async createImage(blobMarkdown, tagsRaw, title, description, mapUrl) { + async createImage(blobMarkdown, tagsRaw, title, description, memeBool, mapUrl) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); const tags = normalizeTags(tagsRaw) || []; @@ -192,6 +193,7 @@ module.exports = ({ cooler }) => { title: title || "", description: description || "", mapUrl: mapUrl || "", + meme: !!memeBool, opinions: {}, opinions_inhabitants: [] }; @@ -201,7 +203,7 @@ module.exports = ({ cooler }) => { }); }, - async updateImageById(id, blobMarkdown, tagsRaw, title, description, mapUrl) { + async updateImageById(id, blobMarkdown, tagsRaw, title, description, memeBool, mapUrl) { const ssbClient = await openSsb(); const userId = ssbClient.id; const tipId = await this.resolveCurrentId(id); @@ -224,6 +226,7 @@ module.exports = ({ cooler }) => { title: title !== undefined ? title || "" : oldMsg.content.title || "", description: description !== undefined ? description || "" : oldMsg.content.description || "", mapUrl: mapUrl !== undefined ? mapUrl || "" : oldMsg.content.mapUrl || "", + meme: typeof memeBool === "boolean" ? memeBool : !!oldMsg.content.meme, createdAt: oldMsg.content.createdAt, updatedAt: now }; @@ -277,6 +280,7 @@ module.exports = ({ cooler }) => { if (filter === "mine") list = list.filter((im) => String(im.author) === String(viewerId)); else if (filter === "recent") list = list.filter((im) => new Date(im.createdAt).getTime() >= now - 86400000); + else if (filter === "meme") list = list.filter((im) => im.meme === true); else if (filter === "top") { list = list .slice() diff --git a/src/models/industry_model.js b/src/models/industry_model.js deleted file mode 100644 index 5afb0fa..0000000 --- a/src/models/industry_model.js +++ /dev/null @@ -1,993 +0,0 @@ -const pull = require("../server/node_modules/pull-stream") -const { getConfig } = require("../configs/config-manager.js") -const { buildValidatedTombstoneSet } = require('./tombstone_validator') -const logLimit = (getConfig().ssbLogStream && getConfig().ssbLogStream.limit) || 1000 - -module.exports = ({ cooler }) => { - let ssb - const openSsb = async () => { - if (!ssb) ssb = await cooler.open() - return ssb - } - - const TYPE = "industry" - const SECTORS = ["software", "hardware", "agriculture", "textile", "energy", "food", "construction", "media", "services", "other"] - const POLICIES = ["open", "vote", "invite"] - - const clampPercent = (n) => { - const x = parseInt(n, 10) - if (!Number.isFinite(x)) return 0 - return Math.max(0, Math.min(100, x)) - } - const clampMajority = (n) => { - const x = parseFloat(n) - if (!Number.isFinite(x)) return 0.5 - return Math.max(0.5, Math.min(1, x)) - } - const clampQuorum = (n) => { - const x = parseInt(n, 10) - if (!Number.isFinite(x) || x < 1) return 1 - return Math.min(100000, x) - } - const safeSector = (s) => { - const v = String(s || "").toLowerCase().trim() - return SECTORS.includes(v) ? v : "other" - } - const safePolicy = (p) => { - const v = String(p || "").toLowerCase().trim() - return POLICIES.includes(v) ? v : "vote" - } - const nonNegNum = (v) => { - const x = parseFloat(v) - return Number.isFinite(x) && x >= 0 ? x : 0 - } - - async function getAllMsgs(ssbClient) { - return new Promise((r, j) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((e, m) => (e ? j(e) : r(m))) - ) - }) - } - - function chainMaps(nodeMap) { - const next = new Map(), prev = new Map(), strictNext = new Map() - for (const [key, n] of nodeMap) { - const t = n.c.replaces - if (!t) continue - next.set(t, key); prev.set(key, t) - const orig = nodeMap.get(t) - if (orig && orig.author === n.author) strictNext.set(t, key) - } - return { next, prev, strictNext } - } - - function buildIndex(messages) { - const tomb = buildValidatedTombstoneSet(messages) - const nodes = new Map() - const blueprints = new Map() - const builds = new Map() - const collab = { member: [], invite: [], apply: [], vote: [], opinion: [], buildStatus: [], buildDates: [], contribution: [], allocation: [] } - for (const m of messages) { - const c = m && m.value && m.value.content - if (!c) continue - if (c.type === "tombstone") continue - const ts = (m.value && m.value.timestamp) || 0 - const author = m.value && m.value.author - if (c.type === "industryMember") { collab.member.push({ target: c.target, author, action: c.action === "leave" ? "leave" : "join", ts }); continue } - if (c.type === "industryInvite") { collab.invite.push({ target: c.target, author, invitee: c.invitee, ts }); continue } - if (c.type === "industryApply") { collab.apply.push({ target: c.target, author, ts }); continue } - if (c.type === "industryVote") { collab.vote.push({ target: c.target, author, subject: String(c.subject || ""), ref: String(c.ref || ""), choice: c.choice === "no" ? "no" : "yes", ts }); continue } - if (c.type === "industryOpinion") { collab.opinion.push({ target: c.target, author, category: c.category, ts }); continue } - if (c.type === "industryBlueprint") { blueprints.set(m.key, { key: m.key, ts, c, author }); continue } - if (c.type === "industryBuild") { builds.set(m.key, { key: m.key, ts, c, author }); continue } - if (c.type === "industryBuildStatus") { collab.buildStatus.push({ target: c.target, author, status: String(c.status || "").toUpperCase(), ts }); continue } - if (c.type === "industryBuildDates" || c.type === "industryBuildUpdate") { collab.buildDates.push({ target: c.target, author, title: String(c.title || ""), notes: String(c.notes || ""), startDate: String(c.startDate || ""), endDate: String(c.endDate || ""), ts }); continue } - if (c.type === "industryContribution") { collab.contribution.push({ target: c.target, author, kind: c.kind, hours: nonNegNum(c.hours), item: String(c.item || ""), value: nonNegNum(c.value), eco: nonNegNum(c.eco), transferId: c.transferId || null, note: String(c.note || ""), ts }); continue } - if (c.type === "industryAllocation") { collab.allocation.push({ target: c.target, author, shares: (c.shares && typeof c.shares === "object") ? c.shares : {}, amounts: (c.amounts && typeof c.amounts === "object") ? c.amounts : {}, pot: nonNegNum(c.pot), outputValue: nonNegNum(c.outputValue), disposition: c.disposition || null, ts }); continue } - if (c.type !== TYPE) continue - nodes.set(m.key, { key: m.key, ts, c, author }) - } - const fc = chainMaps(nodes) - const bp = chainMaps(blueprints) - return { tomb, nodes, blueprints, builds, collab, naiveNext: fc.next, naivePrev: fc.prev, strictNext: fc.strictNext, bpNext: bp.next, bpPrev: bp.prev, bpStrictNext: bp.strictNext } - } - - const rootOfIdx = (idx, key) => { let x = key, g = 0; while (idx.naivePrev.has(x) && idx.nodes.has(idx.naivePrev.get(x)) && g++ < 100000) x = idx.naivePrev.get(x); return x } - const rootOfBlueprint = (idx, key) => { let x = key, g = 0; while (idx.bpPrev.has(x) && idx.blueprints.has(idx.bpPrev.get(x)) && g++ < 100000) x = idx.bpPrev.get(x); return x } - - function passesThreshold(yesCount, memberCount, quorum, majority) { - const size = Math.max(1, memberCount) - const need = Math.max(Math.min(clampQuorum(quorum), size), Math.ceil(size * clampMajority(majority))) - return yesCount >= need - } - - function actionVoteState(idx, groupRoot, members, subject, ref) { - const pref = new Map() - for (const v of idx.collab.vote.filter(x => x.target === groupRoot && x.subject === subject && x.ref === ref).sort((a, b) => a.ts - b.ts)) { - if (!members.has(v.author)) continue - pref.set(v.author, v.choice) - } - const voters = [...pref.entries()].filter(([, c]) => c === "yes").map(([m]) => m) - return { yes: voters.length, voters } - } - - function voteNeedOf(g) { - const size = Math.max(1, new Set(g.facility.members).size) - return Math.max(Math.min(clampQuorum(g.facility.quorum), size), Math.ceil(size * clampMajority(g.facility.majority))) - } - - function actionApproved(idx, g, subject, ref) { - const members = new Set(g.facility.members) - if (members.size <= 1) return true - const st = actionVoteState(idx, g.root, members, subject, ref) - return passesThreshold(st.yes, members.size, g.facility.quorum, g.facility.majority) - } - - function blueprintGovernance(idx, g, bpRoot) { - const members = new Set(g.facility.members) - const up = actionVoteState(idx, g.root, members, "bpUpdate", bpRoot) - const del = actionVoteState(idx, g.root, members, "bpDelete", bpRoot) - return { - root: bpRoot, - facilityMembers: members.size, - voteNeed: voteNeedOf(g), - updateYes: up.yes, - updateVoters: up.voters, - deleteYes: del.yes, - deleteVoters: del.voters, - updateApproved: actionApproved(idx, g, "bpUpdate", bpRoot), - deleteApproved: actionApproved(idx, g, "bpDelete", bpRoot) - } - } - - function buildGovernance(idx, g, buildId) { - const members = new Set(g.facility.members) - const up = actionVoteState(idx, g.root, members, "buildUpdate", buildId) - const del = actionVoteState(idx, g.root, members, "buildDelete", buildId) - return { - facilityMembers: members.size, - voteNeed: voteNeedOf(g), - updateYes: up.yes, - updateVoters: up.voters, - deleteYes: del.yes, - deleteVoters: del.voters, - updateApproved: actionApproved(idx, g, "buildUpdate", buildId), - deleteApproved: actionApproved(idx, g, "buildDelete", buildId) - } - } - - function resolveGroup(idx, root) { - const { nodes, collab, naiveNext, strictNext } = idx - const followStrict = (key) => { let x = key, g = 0; while (strictNext.has(x) && g++ < 100000) x = strictNext.get(x); return x } - const rootNode = nodes.get(root) - if (!rootNode) return null - const steward = rootNode.author - let tip = followStrict(root) - const tn = nodes.get(tip) - if (!tn || tn.author !== steward) tip = root - const best = nodes.get(tip) - if (!best) return null - - const groupKeys = [] - { let x = root, g = 0; groupKeys.push(x); while (naiveNext.has(x) && g++ < 100000) { x = naiveNext.get(x); groupKeys.push(x) } } - - const facility = { ...best.c, id: tip } - const policy = safePolicy(facility.membershipPolicy) - const quorum = clampQuorum(facility.quorum) - const majority = clampMajority(facility.majority) - - const KIND_ORDER = { invite: 0, apply: 1, member: 2, vote: 3 } - const events = [] - for (const e of collab.member.filter(x => x.target === root)) events.push({ kind: "member", ...e }) - for (const e of collab.invite.filter(x => x.target === root)) events.push({ kind: "invite", ...e }) - for (const e of collab.apply.filter(x => x.target === root)) events.push({ kind: "apply", ...e }) - for (const e of collab.vote.filter(x => x.target === root)) events.push({ kind: "vote", ...e }) - events.sort((a, b) => (a.ts - b.ts) || (KIND_ORDER[a.kind] - KIND_ORDER[b.kind])) - - const members = new Set([steward]) - const invites = new Set() - const applicants = new Set() - const admitVotes = new Map() - const dissolveVoters = new Set() - let dissolved = false - - const admitKey = (uid) => "admit:" + uid - for (const ev of events) { - if (ev.kind === "invite") { - if (members.has(ev.author) && ev.invitee) invites.add(ev.invitee) - continue - } - if (ev.kind === "apply") { - if (!members.has(ev.author)) applicants.add(ev.author) - continue - } - if (ev.kind === "member") { - if (ev.action === "leave") { if (ev.author !== steward) members.delete(ev.author); continue } - if (members.has(ev.author)) continue - if (policy === "open") { members.add(ev.author); applicants.delete(ev.author) } - else if (policy === "invite" && invites.has(ev.author)) { members.add(ev.author); applicants.delete(ev.author) } - continue - } - if (ev.kind === "vote") { - if (!members.has(ev.author)) continue - if (ev.subject === "admit" && ev.ref) { - const k = admitKey(ev.ref) - if (!admitVotes.has(k)) admitVotes.set(k, new Map()) - admitVotes.get(k).set(ev.author, ev.choice) - const yes = [...admitVotes.get(k).values()].filter(v => v === "yes").length - if (!members.has(ev.ref) && passesThreshold(yes, members.size, quorum, majority)) { - members.add(ev.ref); applicants.delete(ev.ref) - } - } else if (ev.subject === "dissolve") { - if (ev.choice === "yes") dissolveVoters.add(ev.author); else dissolveVoters.delete(ev.author) - if (passesThreshold(dissolveVoters.size, members.size, quorum, majority)) dissolved = true - } - continue - } - } - - const pendingApplicants = [...applicants].map(uid => { - const k = admitKey(uid) - const vm = admitVotes.get(k) || new Map() - let yes = 0, no = 0 - for (const v of vm.values()) { if (v === "yes") yes++; else no++ } - return { id: uid, yes, no, voters: [...vm.keys()] } - }) - - const opinions = {} - const opinionSet = new Set() - for (const k of groupKeys) { - const n = nodes.get(k); if (!n || n.author !== steward) continue - const c = n.c - if (c.opinions && typeof c.opinions === "object") { for (const kk of Object.keys(c.opinions)) opinions[kk] = Math.max(opinions[kk] || 0, Number(c.opinions[kk]) || 0) } - for (const v of (Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : [])) opinionSet.add(v) - } - for (const op of collab.opinion.filter(x => x.target === root)) { if (!opinionSet.has(op.author)) { opinionSet.add(op.author); opinions[op.category] = (opinions[op.category] || 0) + 1 } } - - const pausePref = new Map() - for (const ev of events) { - if (ev.kind !== "vote" || ev.subject !== "pause") continue - if (!members.has(ev.author)) continue - pausePref.set(ev.author, ev.choice) - } - const pauseVoters = [...pausePref.entries()].filter(([, c]) => c === "yes").map(([m]) => m) - const paused = passesThreshold(pauseVoters.length, members.size, quorum, majority) - - facility.steward = steward - facility.root = root - facility.membershipPolicy = policy - facility.quorum = quorum - facility.majority = majority - facility.members = [...members] - facility.memberCount = members.size - facility.pendingApplicants = pendingApplicants - facility.invites = [...invites] - facility.opinions = opinions - facility.opinions_inhabitants = [...opinionSet] - facility.pauseVoters = pauseVoters - facility.pauseYes = pauseVoters.length - facility.status = dissolved ? "DISSOLVED" : (paused ? "PAUSED" : "ACTIVE") - - return { tip, root, best, facility, tombstoned: idx.tomb.has(tip) } - } - - const BUILD_STATUSES = ["PROPOSED", "REJECTED", "APPROVED", "STOCKING", "IN_PRODUCTION", "COMPLETED", "FAILED"] - const BUILD_TRANSITIONS = { APPROVED: ["STOCKING", "FAILED"], STOCKING: ["IN_PRODUCTION", "FAILED"], IN_PRODUCTION: ["COMPLETED", "FAILED"], FAILED: ["APPROVED", "STOCKING", "IN_PRODUCTION"], COMPLETED: ["IN_PRODUCTION"] } - const CONTRIBUTABLE = new Set(["APPROVED", "STOCKING", "IN_PRODUCTION"]) - - const normalizeMaterials = (raw, text) => { - if (Array.isArray(raw)) return raw.map(m => ({ item: String((m && m.item) || "").trim(), qty: nonNegNum(m && m.qty), price: nonNegNum(m && m.price) })).filter(m => m.item) - const t = String(text || "") - if (!t.trim()) return [] - return t.split("\n").map(line => { const parts = line.split(/[:,]/).map(x => String(x || "").trim()); return { item: parts[0] || "", qty: nonNegNum(parts[1]), price: nonNegNum(parts[2]) } }).filter(m => m.item) - } - - const safeMaterials = (v) => (Array.isArray(v) ? v : []) - const estimateBlueprint = (bp, laborRate) => { - const materialsCost = safeMaterials(bp.materials).reduce((s, m) => s + nonNegNum(m.qty) * nonNegNum(m.price), 0) - const laborCost = nonNegNum(bp.laborHours) * nonNegNum(laborRate) - return { - estMaterialsCost: Number(materialsCost.toFixed(6)), - estLaborCost: Number(laborCost.toFixed(6)), - estTotal: Number((materialsCost + laborCost).toFixed(6)) - } - } - const estimateForBlueprintRef = (idx, bpRef, laborRate) => { - if (!bpRef) return null - const b = resolveBlueprint(idx, rootOfBlueprint(idx, bpRef)) - if (!b || b.tombstoned) return null - return { blueprintName: b.blueprint.name, blueprintImage: b.blueprint.image || null, blueprintKind: b.blueprint.outKind || null, blueprintLicense: b.blueprint.license || "copyleft", ...estimateBlueprint(b.blueprint, laborRate) } - } - const normalizeSkills = (raw) => { - if (Array.isArray(raw)) return raw.map(x => String(x || "").trim()).filter(Boolean) - return String(raw || "").split(",").map(x => x.trim()).filter(Boolean) - } - - function computeShares(contributions, laborRate) { - const rate = nonNegNum(laborRate) - const points = new Map() - for (const c of contributions) { - let p = 0 - if (c.kind === "labor") p = (c.hours || 0) * rate - else if (c.kind === "material") p = (c.value || 0) - else if (c.kind === "eco") p = (c.eco || 0) - if (p <= 0) continue - points.set(c.author, (points.get(c.author) || 0) + p) - } - let total = 0 - for (const v of points.values()) total += v - const shares = new Map() - if (total > 0) for (const [k, v] of points) shares.set(k, v / total) - return { points, total, shares } - } - - function resolveBlueprint(idx, root) { - const { blueprints, bpStrictNext } = idx - const followStrict = (key) => { let x = key, g = 0; while (bpStrictNext.has(x) && g++ < 100000) x = bpStrictNext.get(x); return x } - const rootNode = blueprints.get(root) - if (!rootNode) return null - const author = rootNode.author - let tip = followStrict(root) - const tn = blueprints.get(tip) - if (!tn || tn.author !== author) tip = root - const best = blueprints.get(tip) - if (!best) return null - return { tip, root, blueprint: { ...best.c, id: tip, author }, tombstoned: idx.tomb.has(tip) } - } - - function resolveBuild(idx, root, facility) { - const node = idx.builds.get(root) - if (!node) return null - const build = { ...node.c, id: root, proposer: node.author } - const steward = facility.steward - const members = new Set(facility.members) - const approveVotes = new Map() - for (const v of idx.collab.vote.filter(x => x.subject === "build" && x.ref === root).sort((a, b) => a.ts - b.ts)) { - if (!members.has(v.author)) continue - approveVotes.set(v.author, v.choice) - } - const approveYes = [...approveVotes.values()].filter(x => x === "yes").length - const approveNo = [...approveVotes.values()].filter(x => x === "no").length - let status = "PROPOSED" - if (passesThreshold(approveYes, members.size, facility.quorum, facility.majority)) status = "APPROVED" - else if (passesThreshold(approveNo, members.size, facility.quorum, facility.majority)) status = "REJECTED" - for (const ev of idx.collab.buildStatus.filter(x => x.target === root).sort((a, b) => a.ts - b.ts)) { - const allowed = BUILD_TRANSITIONS[status] || [] - if (!allowed.includes(ev.status)) continue - const isSteward = ev.author === steward - const isProposer = ev.author === build.proposer - const stewardOnly = ev.status === "COMPLETED" || ev.status === "FAILED" - if (stewardOnly && !isSteward) continue - if (!isSteward && !isProposer) continue - status = ev.status - } - for (const ev of idx.collab.buildDates.filter(x => x.target === root).sort((a, b) => a.ts - b.ts)) { - if (ev.author !== steward && ev.author !== build.proposer) continue - if (ev.title) build.title = ev.title - if (ev.notes) build.notes = ev.notes - if (ev.startDate) build.startDate = ev.startDate - if (ev.endDate) build.endDate = ev.endDate - } - build.status = status - const contributions = idx.collab.contribution.filter(x => x.target === root && members.has(x.author)).sort((a, b) => a.ts - b.ts) - build.contributions = contributions.map(c => ({ author: c.author, kind: c.kind, hours: c.hours, item: c.item, value: c.value, eco: c.eco, note: c.note, at: new Date(c.ts || 0).toISOString() })) - const cs = computeShares(contributions, facility.laborRate) - build.points = Object.fromEntries(cs.points) - build.totalPoints = cs.total - build.shares = Object.fromEntries(cs.shares) - build.treasury = contributions.reduce((s, c) => s + (c.eco || 0), 0) - const allocs = idx.collab.allocation.filter(x => x.target === root && x.author === steward).sort((a, b) => a.ts - b.ts) - const latest = allocs[allocs.length - 1] || null - build.distributed = !!latest - build.allocation = latest ? { shares: latest.shares, amounts: latest.amounts, pot: latest.pot, outputValue: latest.outputValue, disposition: latest.disposition, at: new Date(latest.ts || 0).toISOString() } : null - build.tombstoned = idx.tomb.has(root) - return build - } - - async function getById(id) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, id)) - if (!g || g.tombstoned) throw new Error("Facility not found") - return g.facility - } - - async function resolveTipId(id) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, id)) - if (!g || g.tombstoned) throw new Error("Facility not found") - return g.tip - } - - const CONTENT_COLLAB_FIELDS = ["id", "root", "steward", "members", "memberCount", "pendingApplicants", "invites", "opinions", "opinions_inhabitants", "pauseVoters", "pauseYes"] - function stripCollab(content) { - const out = { ...content } - for (const f of CONTENT_COLLAB_FIELDS) delete out[f] - out.opinions = {} - out.opinions_inhabitants = [] - return out - } - - async function publishReplace(ssbClient, currentId, content) { - const tomb = { type: "tombstone", target: currentId, deletedAt: new Date().toISOString(), author: ssbClient.id } - const updated = { ...stripCollab(content), type: TYPE, replaces: currentId, updatedAt: new Date().toISOString() } - await new Promise((res, rej) => ssbClient.publish(tomb, (e) => (e ? rej(e) : res()))) - return new Promise((res, rej) => ssbClient.publish(updated, (e, m) => (e ? rej(e) : res(m)))) - } - - const publish = (ssbClient, content) => new Promise((res, rej) => ssbClient.publish(content, (e, m) => (e ? rej(e) : res(m)))) - - return { - type: TYPE, - SECTORS, - POLICIES, - - async createFacility(data) { - const ssbClient = await openSsb() - const content = { - type: TYPE, - name: String(data.name || "").trim(), - sector: safeSector(data.sector), - description: String(data.description || "").trim(), - image: data.image || null, - mapUrl: String(data.mapUrl || "").trim(), - membershipPolicy: safePolicy(data.membershipPolicy), - quorum: clampQuorum(data.quorum), - majority: clampMajority(data.majority), - laborRate: nonNegNum(data.laborRate), - license: String(data.license || "copyleft").trim() || "copyleft", - tags: normalizeSkills(data.tags), - status: "ACTIVE", - author: ssbClient.id, - createdAt: new Date().toISOString(), - updatedAt: null, - opinions: {}, - opinions_inhabitants: [] - } - if (!content.name) throw new Error("Facility name is required") - return publish(ssbClient, content) - }, - - async updateFacility(id, patch) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, id)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (g.facility.steward !== ssbClient.id) throw new Error("Unauthorized") - const current = g.facility - const nextStatus = patch.status === undefined ? current.status : String(patch.status || "").toUpperCase() - if (nextStatus === "DISSOLVED" && current.status !== "DISSOLVED") throw new Error("Dissolution requires a governance vote") - const updated = { - ...current, - ...patch, - name: patch.name === undefined ? current.name : String(patch.name || "").trim(), - sector: patch.sector === undefined ? current.sector : safeSector(patch.sector), - description: patch.description === undefined ? current.description : String(patch.description || "").trim(), - image: patch.image === undefined ? current.image : (patch.image || null), - mapUrl: patch.mapUrl === undefined ? current.mapUrl : String(patch.mapUrl || "").trim(), - membershipPolicy: patch.membershipPolicy === undefined ? current.membershipPolicy : safePolicy(patch.membershipPolicy), - quorum: patch.quorum === undefined ? current.quorum : clampQuorum(patch.quorum), - majority: patch.majority === undefined ? current.majority : clampMajority(patch.majority), - laborRate: patch.laborRate === undefined ? current.laborRate : nonNegNum(patch.laborRate), - tags: patch.tags === undefined ? current.tags : normalizeSkills(patch.tags), - status: nextStatus - } - return publishReplace(ssbClient, current.id, updated) - }, - - async deleteFacility(id) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, id)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (g.facility.steward !== ssbClient.id) throw new Error("Unauthorized") - if (g.facility.memberCount > 1) throw new Error("A facility with members cannot be deleted; it must be dissolved by vote") - const tomb = { type: "tombstone", target: g.tip, deletedAt: new Date().toISOString(), author: ssbClient.id } - return publish(ssbClient, tomb) - }, - - async joinFacility(id) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, id)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (g.facility.status === "DISSOLVED") throw new Error("Facility is dissolved") - const uid = ssbClient.id - if (g.facility.members.includes(uid)) throw new Error("Already a member") - const policy = g.facility.membershipPolicy - if (policy === "vote") return publish(ssbClient, { type: "industryApply", target: g.root, createdAt: new Date().toISOString() }) - if (policy === "invite" && !g.facility.invites.includes(uid)) throw new Error("You need an invitation to join") - return publish(ssbClient, { type: "industryMember", target: g.root, action: "join", createdAt: new Date().toISOString() }) - }, - - async leaveFacility(id) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, id)) - if (!g || g.tombstoned) throw new Error("Facility not found") - const uid = ssbClient.id - if (uid === g.facility.steward) throw new Error("The steward cannot leave; dissolve the facility instead") - if (!g.facility.members.includes(uid)) throw new Error("Not a member") - return publish(ssbClient, { type: "industryMember", target: g.root, action: "leave", createdAt: new Date().toISOString() }) - }, - - async inviteToFacility(id, invitee) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, id)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can invite") - if (!/^@[A-Za-z0-9+/]+={0,2}\.ed25519$/.test(String(invitee || ""))) throw new Error("Invalid invitee id") - return publish(ssbClient, { type: "industryInvite", target: g.root, invitee: String(invitee), createdAt: new Date().toISOString() }) - }, - - async voteGovernance(id, subject, ref, choice) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, id)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can vote") - const subj = String(subject || "") - const SUBJECTS = ["admit", "dissolve", "pause", "bpUpdate", "bpDelete", "buildUpdate", "buildDelete"] - if (!SUBJECTS.includes(subj)) throw new Error("Invalid governance subject") - if (subj !== "admit" && subj !== "dissolve" && subj !== "pause" && !String(ref || "").trim()) throw new Error("Governance vote requires a target") - return publish(ssbClient, { type: "industryVote", target: g.root, subject: subj, ref: String(ref || ""), choice: choice === "no" ? "no" : "yes", createdAt: new Date().toISOString() }) - }, - - async createOpinion(id, category) { - const categories = require('../backend/opinion_categories') - if (!categories.includes(category)) throw new Error('Invalid opinion category') - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, id)) - if (!g || g.tombstoned) throw new Error('Facility not found') - if ((g.facility.opinions_inhabitants || []).includes(ssbClient.id)) throw new Error('Already opined') - return publish(ssbClient, { type: 'industryOpinion', target: g.root, category, createdAt: new Date().toISOString() }) - }, - - async getFacilityById(id) { - return getById(id) - }, - - async listFacilities(filter = "ALL") { - const ssbClient = await openSsb() - const uid = ssbClient.id - const idx = buildIndex(await getAllMsgs(ssbClient)) - const roots = new Set() - for (const key of idx.nodes.keys()) roots.add(rootOfIdx(idx, key)) - let list = [] - for (const root of roots) { - const g = resolveGroup(idx, root) - if (!g || g.tombstoned) continue - list.push(g.facility) - } - const f = String(filter || "ALL").toUpperCase() - if (f === "MINE") list = list.filter(x => x.steward === uid) - else if (f === "MEMBER") list = list.filter(x => Array.isArray(x.members) && x.members.includes(uid)) - else if (f === "ACTIVE") list = list.filter(x => x.status === "ACTIVE") - else if (f === "PAUSED") list = list.filter(x => x.status === "PAUSED") - else if (f === "DISSOLVED") list = list.filter(x => x.status === "DISSOLVED") - else if (SECTORS.includes(f.toLowerCase())) list = list.filter(x => x.sector === f.toLowerCase()) - list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) - return list - }, - - async facilitiesOf(feedId) { - const ssbClient = await openSsb() - const uid = feedId || ssbClient.id - const idx = buildIndex(await getAllMsgs(ssbClient)) - const roots = new Set() - for (const key of idx.nodes.keys()) roots.add(rootOfIdx(idx, key)) - const list = [] - for (const root of roots) { - const g = resolveGroup(idx, root) - if (!g || g.tombstoned) continue - if (Array.isArray(g.facility.members) && g.facility.members.includes(uid)) list.push(g.facility) - } - list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) - return list - }, - - async listMyBuilds() { - const ssbClient = await openSsb() - const uid = ssbClient.id - const idx = buildIndex(await getAllMsgs(ssbClient)) - const facRoots = new Set() - for (const key of idx.nodes.keys()) facRoots.add(rootOfIdx(idx, key)) - const myFacilities = new Map() - for (const root of facRoots) { - const g = resolveGroup(idx, root) - if (!g || g.tombstoned) continue - if (Array.isArray(g.facility.members) && g.facility.members.includes(uid)) myFacilities.set(g.root, g.facility) - } - const out = [] - for (const [key, node] of idx.builds) { - if (idx.tomb.has(key)) continue - const fac = myFacilities.get(node.c.facility) - if (!fac) continue - const build = resolveBuild(idx, key, fac) - build.facilityId = fac.root - build.facilityName = fac.name - out.push(build) - } - out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) - return out - }, - - async createBlueprint(facilityId, data) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, facilityId)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can add blueprints") - const content = { - type: "industryBlueprint", - facility: g.root, - name: String(data.name || "").trim(), - description: String(data.description || "").trim(), - image: data.image || null, - sector: safeSector(data.sector || g.facility.sector), - materials: normalizeMaterials(data.materials, data.materialsText), - laborHours: nonNegNum(data.laborHours), - skills: normalizeSkills(data.skills), - outItem: String(data.outItem || "").trim(), - outQty: nonNegNum(data.outQty), - outUnit: String(data.outUnit || "unit").trim() || "unit", - outKind: String(data.outKind || "physical").toLowerCase() === "digital" ? "digital" : "physical", - license: String(data.license || "copyleft").trim() || "copyleft", - createdAt: new Date().toISOString() - } - if (!content.name) throw new Error("Blueprint name is required") - return publish(ssbClient, content) - }, - - async updateBlueprint(id, patch) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const b = resolveBlueprint(idx, rootOfBlueprint(idx, id)) - if (!b || b.tombstoned) throw new Error("Blueprint not found") - if (b.blueprint.author !== ssbClient.id) throw new Error("Unauthorized") - const gb = resolveGroup(idx, rootOfIdx(idx, b.blueprint.facility)) - if (!gb || gb.tombstoned) throw new Error("Facility not found") - if (!gb.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") - if (!actionApproved(idx, gb, "bpUpdate", b.root)) throw new Error("Updating this blueprint requires a governance vote") - const cur = b.blueprint - const updated = { - type: "industryBlueprint", - facility: cur.facility, - name: patch.name === undefined ? cur.name : String(patch.name || "").trim(), - description: patch.description === undefined ? (cur.description || "") : String(patch.description || "").trim(), - image: patch.image === undefined ? (cur.image || null) : (patch.image || null), - sector: patch.sector === undefined ? cur.sector : safeSector(patch.sector), - materials: patch.materials === undefined && patch.materialsText === undefined ? cur.materials : normalizeMaterials(patch.materials, patch.materialsText), - laborHours: patch.laborHours === undefined ? cur.laborHours : nonNegNum(patch.laborHours), - skills: patch.skills === undefined ? cur.skills : normalizeSkills(patch.skills), - outItem: patch.outItem === undefined ? cur.outItem : String(patch.outItem || "").trim(), - outQty: patch.outQty === undefined ? cur.outQty : nonNegNum(patch.outQty), - outUnit: patch.outUnit === undefined ? cur.outUnit : String(patch.outUnit || "unit").trim() || "unit", - outKind: patch.outKind === undefined ? cur.outKind : (String(patch.outKind).toLowerCase() === "digital" ? "digital" : "physical"), - license: patch.license === undefined ? cur.license : String(patch.license || "copyleft").trim() || "copyleft", - replaces: b.tip, - createdAt: cur.createdAt, - updatedAt: new Date().toISOString() - } - const tomb = { type: "tombstone", target: b.tip, deletedAt: new Date().toISOString(), author: ssbClient.id } - await publish(ssbClient, tomb) - return publish(ssbClient, updated) - }, - - async deleteBlueprint(id) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const b = resolveBlueprint(idx, rootOfBlueprint(idx, id)) - if (!b || b.tombstoned) throw new Error("Blueprint not found") - const g = resolveGroup(idx, rootOfIdx(idx, b.blueprint.facility)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") - if (b.blueprint.author !== ssbClient.id) throw new Error("Only the author can delete a blueprint") - if (!actionApproved(idx, g, "bpDelete", b.root)) throw new Error("Deleting this blueprint requires a governance vote") - const chain = [] - { let x = b.root, guard = 0; chain.push(x); while (idx.bpNext.has(x) && guard++ < 100000) { x = idx.bpNext.get(x); chain.push(x) } } - const chainSet = new Set(chain) - let usedBy = 0 - for (const [key, node] of idx.builds) { - if (idx.tomb.has(key)) continue - if (chainSet.has(node.c.blueprint)) usedBy++ - } - if (usedBy > 0) throw new Error(`This blueprint is used by ${usedBy} build${usedBy > 1 ? "s" : ""}; delete them first`) - let last = null - for (const key of chain) { - const node = idx.blueprints.get(key) - if (!node || node.author !== ssbClient.id) continue - if (idx.tomb.has(key)) continue - last = await publish(ssbClient, { type: "tombstone", target: key, deletedAt: new Date().toISOString(), author: ssbClient.id }) - } - return last - }, - - async getBlueprint(id) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const b = resolveBlueprint(idx, rootOfBlueprint(idx, id)) - if (!b || b.tombstoned) throw new Error("Blueprint not found") - const g = resolveGroup(idx, rootOfIdx(idx, b.blueprint.facility)) - if (!g || g.tombstoned) throw new Error("Facility not found") - const builds = [] - for (const [key, node] of idx.builds) { - if (idx.tomb.has(key)) continue - if (node.c.blueprint !== b.tip && node.c.blueprint !== b.root) continue - const rb = resolveBuild(idx, key, g.facility) - if (rb) builds.push({ id: key, title: rb.title, status: rb.status, startDate: rb.startDate, endDate: rb.endDate }) - } - builds.sort((x, y) => new Date(y.startDate || 0) - new Date(x.startDate || 0)) - return { - ...b.blueprint, - facilityId: g.root, - facilityName: g.facility.name, - members: g.facility.members, - builds, - ...estimateBlueprint(b.blueprint, g.facility.laborRate), - ...blueprintGovernance(idx, g, b.root) - } - }, - - async listBlueprints(facilityId) { - const ssbClient = await openSsb() - const msgs = await getAllMsgs(ssbClient) - const idx = buildIndex(msgs) - const root = rootOfIdx(idx, facilityId) - const g = resolveGroup(idx, root) - const laborRate = g && !g.tombstoned ? g.facility.laborRate : 0 - const roots = new Set() - for (const key of idx.blueprints.keys()) roots.add(rootOfBlueprint(idx, key)) - const list = [] - for (const r of roots) { - const b = resolveBlueprint(idx, r) - if (!b || b.tombstoned) continue - if (b.blueprint.facility !== root) continue - list.push({ ...b.blueprint, ...estimateBlueprint(b.blueprint, laborRate), ...(g && !g.tombstoned ? blueprintGovernance(idx, g, b.root) : {}) }) - } - list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) - return list - }, - - async createBuild(facilityId, data) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, facilityId)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (g.facility.status !== "ACTIVE") throw new Error("Facility is not active") - if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can propose builds") - let blueprintId = String(data.blueprintId || "").trim() || null - if (!blueprintId) throw new Error("A build needs a blueprint") - { - const b = resolveBlueprint(idx, rootOfBlueprint(idx, blueprintId)) - if (!b || b.tombstoned || b.blueprint.facility !== g.root) throw new Error("Blueprint not found in this facility") - blueprintId = b.tip - } - const content = { type: "industryBuild", facility: g.root, blueprint: blueprintId, title: String(data.title || "").trim(), notes: String(data.notes || "").trim(), image: data.image || null, startDate: String(data.startDate || "").trim() || null, endDate: String(data.endDate || "").trim() || null, createdAt: new Date().toISOString() } - if (!content.title) throw new Error("Build title is required") - const today = new Date().toISOString().slice(0, 10) - if (!content.startDate) throw new Error("Build start date is required") - if (!content.endDate) throw new Error("Build end date is required") - if (content.startDate < today) throw new Error("Start date cannot be in the past") - if (content.startDate && content.endDate && new Date(content.endDate) < new Date(content.startDate)) throw new Error("End date must be after start date") - return publish(ssbClient, content) - }, - - async voteBuild(buildId, choice) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const bnode = idx.builds.get(buildId) - if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") - const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can vote") - return publish(ssbClient, { type: "industryVote", target: g.root, subject: "build", ref: buildId, choice: choice === "no" ? "no" : "yes", createdAt: new Date().toISOString() }) - }, - - async setBuildStatus(buildId, status) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const bnode = idx.builds.get(buildId) - if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") - const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) - if (!g || g.tombstoned) throw new Error("Facility not found") - const build = resolveBuild(idx, buildId, g.facility) - const next = String(status || "").toUpperCase() - if (!BUILD_STATUSES.includes(next)) throw new Error("Invalid status") - const allowed = BUILD_TRANSITIONS[build.status] || [] - if (!allowed.includes(next)) throw new Error(`Cannot move build from ${build.status} to ${next}`) - if (g.facility.status !== "ACTIVE") throw new Error("Facility is not active") - if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") - const isSteward = g.facility.steward === ssbClient.id - const isProposer = build.proposer === ssbClient.id - const isReopen = build.status === "COMPLETED" || build.status === "FAILED" - if (build.status === "COMPLETED" && build.distributed) throw new Error("A distributed build cannot be reopened") - if ((next === "COMPLETED" || next === "FAILED" || isReopen) && !isSteward) throw new Error("Only the steward can complete, fail or reopen a build") - if (!isSteward && !isProposer) throw new Error("Only the steward or the proposer can advance the build") - return publish(ssbClient, { type: "industryBuildStatus", target: buildId, status: next, createdAt: new Date().toISOString() }) - }, - - async contribute(buildId, data) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const bnode = idx.builds.get(buildId) - if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") - const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members can contribute") - if (g.facility.status !== "ACTIVE") throw new Error("Facility is not active") - const build = resolveBuild(idx, buildId, g.facility) - if (!CONTRIBUTABLE.has(build.status)) throw new Error("This build is not accepting contributions") - const kind = ["labor", "material", "eco"].includes(String(data.kind)) ? String(data.kind) : "labor" - const content = { type: "industryContribution", target: buildId, kind, hours: nonNegNum(data.hours), item: String(data.item || "").trim(), value: nonNegNum(data.value), eco: nonNegNum(data.eco), transferId: data.transferId || null, note: String(data.note || "").trim(), createdAt: new Date().toISOString() } - if (kind === "labor") { - if (content.hours <= 0) throw new Error("Labor contributions require hours above zero") - content.item = ""; content.value = 0; content.eco = 0 - } else if (kind === "material") { - if (!content.item) throw new Error("Material contributions require an item") - if (content.value <= 0) throw new Error("Material contributions require a value above zero") - content.hours = 0; content.eco = 0 - } else { - if (content.eco <= 0) throw new Error("ECO contributions require an amount above zero") - content.hours = 0; content.item = ""; content.value = 0 - } - return publish(ssbClient, content) - }, - - async getBuild(buildId) { - const ssbClient = await openSsb() - const msgs = await getAllMsgs(ssbClient) - const idx = buildIndex(msgs) - const bnode = idx.builds.get(buildId) - if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") - const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) - if (!g) throw new Error("Facility not found") - const build = resolveBuild(idx, buildId, g.facility) - build.facilityId = g.root - build.facilityName = g.facility.name - build.steward = g.facility.steward - build.members = g.facility.members - const est = estimateForBlueprintRef(idx, build.blueprint, g.facility.laborRate) - if (est) Object.assign(build, est) - Object.assign(build, buildGovernance(idx, g, buildId)) - return build - }, - - async updateBuild(buildId, data) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const bnode = idx.builds.get(buildId) - if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") - const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") - const build = resolveBuild(idx, buildId, g.facility) - const isSteward = g.facility.steward === ssbClient.id - if (build.proposer !== ssbClient.id && !isSteward) throw new Error("Only the steward or the proposer can update a build") - if (build.distributed) throw new Error("A distributed build cannot be updated") - if (!actionApproved(idx, g, "buildUpdate", buildId)) throw new Error("Updating this build requires a governance vote") - const startDate = String(data.startDate || "").trim() - const endDate = String(data.endDate || "").trim() - if (!startDate) throw new Error("Build start date is required") - if (!endDate) throw new Error("Build end date is required") - if (new Date(endDate) < new Date(startDate)) throw new Error("End date must be after start date") - const today = new Date().toISOString().slice(0, 10) - if (endDate < today) throw new Error("End date cannot be in the past") - const title = data.title === undefined ? String(build.title || "") : String(data.title || "").trim() - if (!title) throw new Error("Build title is required") - const notes = data.notes === undefined ? String(build.notes || "") : String(data.notes || "").trim() - return publish(ssbClient, { type: "industryBuildUpdate", target: buildId, title, notes, startDate, endDate, createdAt: new Date().toISOString() }) - }, - - async deleteBuild(buildId) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const bnode = idx.builds.get(buildId) - if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") - const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (!g.facility.members.includes(ssbClient.id)) throw new Error("Only members of the facility can do this") - if (bnode.author !== ssbClient.id) throw new Error("Only the proposer can delete a build") - if (!actionApproved(idx, g, "buildDelete", buildId)) throw new Error("Deleting this build requires a governance vote") - return publish(ssbClient, { type: "tombstone", target: buildId, deletedAt: new Date().toISOString(), author: ssbClient.id }) - }, - - async listBuilds(facilityId, filter = "ALL") { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const g = resolveGroup(idx, rootOfIdx(idx, facilityId)) - if (!g || g.tombstoned) throw new Error("Facility not found") - let list = [] - for (const [key, node] of idx.builds) { - if (idx.tomb.has(key)) continue - if (node.c.facility !== g.root) continue - const b = resolveBuild(idx, key, g.facility) - if (!b) continue - const est = estimateForBlueprintRef(idx, b.blueprint, g.facility.laborRate) - list.push({ ...b, ...(est || {}), ...buildGovernance(idx, g, key) }) - } - const f = String(filter || "ALL").toUpperCase() - if (BUILD_STATUSES.includes(f)) list = list.filter(x => x.status === f) - list.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) - return list - }, - - async listAllBlueprints() { - const ssbClient = await openSsb() - const msgs = await getAllMsgs(ssbClient) - const idx = buildIndex(msgs) - const roots = new Set() - for (const key of idx.blueprints.keys()) roots.add(rootOfBlueprint(idx, key)) - const facCache = new Map() - const list = [] - for (const root of roots) { - const b = resolveBlueprint(idx, root) - if (!b || b.tombstoned) continue - const facRoot = rootOfIdx(idx, b.blueprint.facility) - let g = facCache.get(facRoot) - if (g === undefined) { g = resolveGroup(idx, facRoot); facCache.set(facRoot, g) } - if (!g || g.tombstoned) continue - list.push({ ...b.blueprint, facilityId: g.root, facilityName: g.facility.name, members: g.facility.members, ...estimateBlueprint(b.blueprint, g.facility.laborRate), ...blueprintGovernance(idx, g, b.root) }) - } - list.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)) - return list - }, - - async listAllBuilds() { - const ssbClient = await openSsb() - const msgs = await getAllMsgs(ssbClient) - const idx = buildIndex(msgs) - const facCache = new Map() - const list = [] - for (const [key, node] of idx.builds) { - if (idx.tomb.has(key)) continue - const facRoot = rootOfIdx(idx, node.c.facility) - let g = facCache.get(facRoot) - if (g === undefined) { g = resolveGroup(idx, facRoot); facCache.set(facRoot, g) } - if (!g || g.tombstoned) continue - const b = resolveBuild(idx, key, g.facility) - if (!b) continue - const est = estimateForBlueprintRef(idx, b.blueprint, g.facility.laborRate) - list.push({ ...b, facilityId: g.root, facilityName: g.facility.name, members: g.facility.members, ...(est || {}), ...buildGovernance(idx, g, key) }) - } - list.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)) - return list - }, - - computeDistributionPlan(build, outputValue) { - const pot = nonNegNum(build.treasury) + nonNegNum(outputValue) - const distributable = pot - const allocations = [] - const amounts = {} - for (const [uid, share] of Object.entries(build.shares || {})) { - const amount = distributable * share - amounts[uid] = amount - if (amount > 0) allocations.push({ to: uid, amount, share }) - } - return { pot, distributable, allocations, amounts } - }, - - async distributeBuild(buildId, data) { - const ssbClient = await openSsb() - const idx = buildIndex(await getAllMsgs(ssbClient)) - const bnode = idx.builds.get(buildId) - if (!bnode || idx.tomb.has(buildId)) throw new Error("Build not found") - const g = resolveGroup(idx, rootOfIdx(idx, bnode.c.facility)) - if (!g || g.tombstoned) throw new Error("Facility not found") - if (g.facility.steward !== ssbClient.id) throw new Error("Only the steward can distribute") - const build = resolveBuild(idx, buildId, g.facility) - if (build.status !== "COMPLETED") throw new Error("Only a completed build can be distributed") - if (build.distributed) throw new Error("This build has already been distributed") - if (build.totalPoints <= 0) throw new Error("There are no contributions to distribute") - const plan = this.computeDistributionPlan(build, data && data.outputValue) - const content = { type: "industryAllocation", target: buildId, shares: build.shares, amounts: plan.amounts, pot: plan.pot, outputValue: nonNegNum(data && data.outputValue), disposition: (data && data.disposition) || null, createdAt: new Date().toISOString() } - await publish(ssbClient, content) - return { plan, facility: g.facility, build } - } - } -} diff --git a/src/models/inhabitants_model.js b/src/models/inhabitants_model.js index ce98ee7..2b3c0dd 100644 --- a/src/models/inhabitants_model.js +++ b/src/models/inhabitants_model.js @@ -130,9 +130,9 @@ module.exports = ({ cooler }) => { return filterInactive(users); } - if (filter === 'all' || filter === 'TOP KARMA' || filter === 'TOP ACTIVITY' || filter === 'TOP INACTIVITY' || filter === 'TOP ECO') { + if (filter === 'all' || filter === 'TOP KARMA' || filter === 'TOP ACTIVITY' || filter === 'TOP ECO') { let users = await listAllBase(ssbClient); - if (filter !== 'TOP ACTIVITY' && filter !== 'TOP INACTIVITY') { + if (filter !== 'TOP ACTIVITY') { users = filterInactive(users); } if (search) { @@ -170,7 +170,6 @@ module.exports = ({ cooler }) => { })); if (filter === 'TOP KARMA') return withMetrics.sort((a, b) => (b.karmaScore || 0) - (a.karmaScore || 0)); if (filter === 'TOP ACTIVITY') return withMetrics.sort((a, b) => (b.lastActivityTs || 0) - (a.lastActivityTs || 0)); - if (filter === 'TOP INACTIVITY') return withMetrics.sort((a, b) => (a.lastActivityTs || 0) - (b.lastActivityTs || 0)); if (filter === 'TOP ECO') return withMetrics.sort((a, b) => (b.ecoScore || 0) - (a.ecoScore || 0)); return withMetrics; } @@ -255,7 +254,7 @@ module.exports = ({ cooler }) => { ); } - if (filter === 'CVs') { + if (filter === 'CVs' || filter === 'MATCHSKILLS') { const records = await new Promise((res, rej) => { pull( ssbClient.createLogStream({ limit: logLimit, reverse: true}), @@ -296,13 +295,54 @@ module.exports = ({ cooler }) => { return out; } + if (filter === 'MATCHSKILLS') { + let base = await Promise.all(cvs.map(async c => { + const photo = await fetchUserImageUrl(c.author, 256); + const lastActivityTs = await getLastActivityTimestamp(c.author); + const { bucket, range } = bucketLastActivity(lastActivityTs); + const norm = this._normalizeCurriculum(c, photo); + const karmaScore = await getLastKarmaScore(c.author).catch(() => 0); + return { ...norm, lastActivityTs, lastActivityBucket: bucket, lastActivityRange: range, karmaScore }; + })); + base = filterInactive(base); + const mecv = await this.getCVByUserId(); + const userSkills = Array.from(new Set( + (mecv + ? [ + ...(mecv.personalSkills || []), + ...(mecv.oasisSkills || []), + ...(mecv.educationalSkills || []), + ...(mecv.professionalSkills || []) + ] + : []).map(s => String(s || '').toLowerCase()).filter(Boolean) + )); + if (!userSkills.length) return []; + const userSet = new Set(userSkills); + const matches = base.map(c => { + if (c.id === userId) return null; + const theirSkillsRaw = (c.skills || []).map(s => String(s || '').toLowerCase()).filter(Boolean); + const theirSet = new Set(theirSkillsRaw); + const common = Array.from(theirSet).filter(s => userSet.has(s)); + if (!common.length) return null; + const unionSize = userSet.size + theirSet.size - common.length; + const matchScore = unionSize > 0 ? common.length / unionSize : 0; + const matchCoverage = userSet.size > 0 ? common.length / userSet.size : 0; + return { ...c, commonSkills: common, matchScore, matchCoverage }; + }).filter(Boolean); + return matches.sort((a, b) => + (b.matchScore - a.matchScore) || + (b.commonSkills.length - a.commonSkills.length) || + ((b.karmaScore || 0) - (a.karmaScore || 0)) || + ((b.lastActivityTs || 0) - (a.lastActivityTs || 0)) + ); + } } return []; }, _normalizeCurriculum(c, photoUrl) { - const photo = c.photo ? toImageUrl(c.photo, 256) : (photoUrl || toImageUrl(null, 256)); + const photo = photoUrl || toImageUrl(c.photo, 256); return { id: c.author, name: c.name, @@ -318,8 +358,6 @@ module.exports = ({ cooler }) => { languages: typeof c.languages === 'string' ? c.languages.split(',').map(x => x.trim()) : Array.isArray(c.languages) ? c.languages : [], - status: c.status, - preferences: c.preferences, createdAt: c.createdAt }; }, @@ -369,7 +407,7 @@ module.exports = ({ cooler }) => { const isOwner = viewer === target; const arr = (v) => Array.isArray(v) ? v : []; const up = (v) => String(v || '').toUpperCase(); - const COUNTED = new Set(['post','event','task','forum','tribe','market','job','housing','project','industry','shop','image','video','audio','document','bookmark','transfer','map']); + const COUNTED = new Set(['post','event','task','forum','tribe','market','job','project','shop','image','video','audio','document','bookmark','transfer','map']); const accessible = (type, c) => { if (c.encryptedPayload) return false; switch (type) { @@ -377,7 +415,6 @@ module.exports = ({ cooler }) => { case 'event': return String(c.isPublic || '').toLowerCase() !== 'private' || isOwner || arr(c.attendees).includes(viewer); case 'forum': return c.isPrivate !== true || isOwner; case 'job': return up(c.visibility) !== 'HIDDEN' || isOwner || arr(c.subscribers).includes(viewer); - case 'housing': return up(c.visibility) !== 'HIDDEN' || isOwner; case 'market': return up(c.visibility) !== 'HIDDEN' || isOwner; case 'shop': return up(c.visibility) !== 'CLOSED' || isOwner; case 'tribe': { const st = up(c.status); return !(st === 'PRIVATE' || st === 'INVITE-ONLY') || isOwner || arr(c.members).includes(viewer); } diff --git a/src/models/jobs_model.js b/src/models/jobs_model.js index c1c4180..3cf3ff6 100644 --- a/src/models/jobs_model.js +++ b/src/models/jobs_model.js @@ -79,7 +79,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const jobId = c.jobId const k = `${jobId}::${author}` const prev = jobSubLatest.get(k) - if (!prev || ts >= prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId }) + if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId }) continue } } @@ -134,7 +134,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const jobId = c.jobId const k = `${jobId}::${author}` const prev = jobSubLatest.get(k) - if (!prev || ts >= prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId }) + if (!prev || ts > prev.ts) jobSubLatest.set(k, { ts, value: !!c.value, author, jobId }) } catch {} } } @@ -177,7 +177,6 @@ module.exports = ({ cooler, tribeCrypto }) => { status: c.status || "OPEN", tags: Array.isArray(c.tags) ? c.tags : normalizeTags(c.tags), subscribers: Array.isArray(visibleSubs) ? visibleSubs : [], - industry: c.industry || "", mapUrl: c.mapUrl || "", visibility: String(c.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC", clearnetPublic: !!c.clearnetPublic @@ -236,7 +235,6 @@ module.exports = ({ cooler, tribeCrypto }) => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), status: "OPEN", - industry: String(jobData.industry || "").trim(), mapUrl: String(jobData.mapUrl || "").trim(), visibility: String(jobData.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC", clearnetPublic: jobData.clearnetPublic === true || jobData.clearnetPublic === 'true' || jobData.clearnetPublic === 'on' diff --git a/src/models/karvan_model.js b/src/models/karvan_model.js deleted file mode 100644 index 529e650..0000000 --- a/src/models/karvan_model.js +++ /dev/null @@ -1,198 +0,0 @@ -"use strict"; -// Módulo KARVAN (self-contained) para Oasis Mobile — inspirado en karvan-protocol -// (packages/core/addons/ephemeral.js): salas EFÍMERAS en RAM que se AUTODESTRUYEN por TTL. -// - Nada se escribe a disco. Los mensajes viven solo en memoria y desaparecen con la sala. -// - TTL de inactividad (idle) que se resetea con cada actividad + TTL absoluto que nunca se resetea. -// - Mailbox RAM para señalización WebRTC (data-channel) entre pares. -// No depende de @karvan/core ni de cripto nativa: es una capa local sobre el backend de Oasis. - -const crypto = require("crypto"); - -const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000; // 30 min sin actividad -> autodestrucción -const DEFAULT_ABS_TTL_MS = 2 * 60 * 60 * 1000; // 2 h absoluto (tope) -const MAX_ABS_TTL_MS = 24 * 60 * 60 * 1000; // nunca más de 24 h -const MAX_MSGS = 250; // anillo: solo los últimos N en RAM -const MAX_SIGNALS = 500; -const MAX_ROOMS = 100; -const MAX_MEMBERS = 50; // tope de miembros por sala (evita crecimiento sin límite del Set) -const MAX_SIGNAL_BYTES = 16384; // SDP/ICE son pequeños; rechaza payloads gigantes (anti-DoS de memoria) - -module.exports = ({ idleTtlMs = DEFAULT_IDLE_TTL_MS, absTtlMs = DEFAULT_ABS_TTL_MS } = {}) => { - const rooms = new Map(); // id -> room - - const publicRoom = (r) => ({ - id: r.id, title: r.title, createdAt: r.createdAt, expiresAt: r.expiresAt, - count: r.messages.length, members: r.members.size, - }); - - const resetIdle = (room) => { - if (room.idleTimer) clearTimeout(room.idleTimer); - room.idleTimer = setTimeout(() => destroy(room.id), idleTtlMs); - if (room.idleTimer.unref) room.idleTimer.unref(); - }; - - const destroy = (id) => { - const room = rooms.get(id); - if (!room) return; - clearTimeout(room.idleTimer); - clearTimeout(room.absTimer); - // AUTODESTRUCCIÓN: vaciar todo el material en RAM - room.messages.length = 0; - room.signals.length = 0; - room.members.clear(); - room.remoteFeeds.clear(); - rooms.delete(id); - }; - - const createRoom = ({ title = "", ttlMinutes } = {}) => { - if (rooms.size >= MAX_ROOMS) { - // desalojar la más antigua para no crecer sin límite - const oldest = [...rooms.values()].sort((a, b) => a.createdAt - b.createdAt)[0]; - if (oldest) destroy(oldest.id); - } - const id = crypto.randomBytes(9).toString("hex"); // 18 hex - const createdAt = Date.now(); - let absMs = absTtlMs; - const t = parseInt(ttlMinutes, 10); - if (Number.isFinite(t) && t > 0) absMs = Math.min(t * 60000, MAX_ABS_TTL_MS); - const room = { - id, - title: String(title || "").slice(0, 80), - createdAt, - expiresAt: createdAt + absMs, - messages: [], - signals: [], - members: new Set(), - remoteFeeds: new Set(), // feeds SSB de otros nodos con esta sala (para relay cross-device por privados) - seq: 0, - sigSeq: 0, - idleTimer: null, - absTimer: null, - }; - rooms.set(id, room); - room.absTimer = setTimeout(() => destroy(id), absMs); - if (room.absTimer.unref) room.absTimer.unref(); - resetIdle(room); - return publicRoom(room); - }; - - // Fase 2 (cross-device): registra el feed SSB de otro participante (de una invitación) para relayarle. - const addRemoteFeed = (id, feed) => { - const room = rooms.get(id); - if (!room || typeof feed !== "string" || !/^@[A-Za-z0-9+/=]{20,}\.ed25519$/.test(feed)) return false; - if (room.remoteFeeds.has(feed) || room.remoteFeeds.size < MAX_MEMBERS) { room.remoteFeeds.add(feed); return true; } - return false; - }; - const getRemoteFeeds = (id) => { const r = rooms.get(id); return r ? [...r.remoteFeeds] : []; }; - // ¿hay ALGUNA sala con feeds remotos? (para no montar el relay si no hace falta) - const anyRemoteRooms = () => { for (const r of rooms.values()) if (r.remoteFeeds.size) return true; return false; }; - - // Fase 2 (invitar): unirse a una sala por su id creando un ESPEJO local (desde un enlace/invitación). - // El id es la "capacidad": quien lo tiene puede unirse. Aún NO conecta con la sala del otro nodo - // (eso es la señalización cross-device, pendiente); crea la sala local para poder abrir el enlace. - const adoptRoom = ({ id, title = "", ttlMinutes } = {}) => { - if (!id || typeof id !== "string" || !/^[0-9a-f]{6,64}$/.test(id)) return null; - const existing = rooms.get(id); - if (existing) return publicRoom(existing); - if (rooms.size >= MAX_ROOMS) { - const oldest = [...rooms.values()].sort((a, b) => a.createdAt - b.createdAt)[0]; - if (oldest) destroy(oldest.id); - } - const createdAt = Date.now(); - let absMs = absTtlMs; - const t = parseInt(ttlMinutes, 10); - if (Number.isFinite(t) && t > 0) absMs = Math.min(t * 60000, MAX_ABS_TTL_MS); - const room = { - id, title: String(title || "").slice(0, 80), createdAt, expiresAt: createdAt + absMs, - messages: [], signals: [], members: new Set(), remoteFeeds: new Set(), seq: 0, sigSeq: 0, idleTimer: null, absTimer: null, - }; - rooms.set(id, room); - room.absTimer = setTimeout(() => destroy(id), absMs); - if (room.absTimer.unref) room.absTimer.unref(); - resetIdle(room); - return publicRoom(room); - }; - - const listRooms = () => - [...rooms.values()].map(publicRoom).sort((a, b) => b.createdAt - a.createdAt); - - const getRoom = (id) => { - const r = rooms.get(id); - return r ? publicRoom(r) : null; - }; - - const postMessage = (id, { from = "?", text = "", mid = "" } = {}) => { - const room = rooms.get(id); - if (!room) return null; - const clean = String(text || "").slice(0, 2000); - if (!clean) return null; - const msg = { - seq: ++room.seq, - mid: String(mid || "").slice(0, 40), - from: String(from || "?").slice(0, 80), - text: clean, - ts: Date.now(), - }; - room.messages.push(msg); - if (room.messages.length > MAX_MSGS) room.messages.shift(); - if (room.members.has(msg.from) || room.members.size < MAX_MEMBERS) room.members.add(msg.from); - resetIdle(room); - return msg; - }; - - const listMessages = (id, since = 0) => { - const room = rooms.get(id); - if (!room) return null; - const s = parseInt(since, 10) || 0; - return room.messages.filter((m) => m.seq > s); - }; - - // --- señalización WebRTC (mailbox RAM) --- - const postSignal = (id, { from = "", to = "", payload = null } = {}) => { - const room = rooms.get(id); - if (!room) return null; - if (payload != null) { // rechaza payloads gigantes (SDP/ICE son pequeños) - try { if (JSON.stringify(payload).length > MAX_SIGNAL_BYTES) return null; } - catch (e) { return null; } - } - const sig = { - seq: ++room.sigSeq, - from: String(from || "").slice(0, 80), - to: String(to || "").slice(0, 80), - payload, - ts: Date.now(), - }; - room.signals.push(sig); - if (room.signals.length > MAX_SIGNALS) room.signals.shift(); - if (sig.from && (room.members.has(sig.from) || room.members.size < MAX_MEMBERS)) room.members.add(sig.from); - resetIdle(room); - return sig; - }; - - const getSignals = (id, { forId = "", since = 0 } = {}) => { - const room = rooms.get(id); - if (!room) return null; - const s = parseInt(since, 10) || 0; - return { - signals: room.signals.filter( - (x) => x.seq > s && x.from !== forId && (!x.to || x.to === forId) - ), - members: [...room.members].filter((m) => m !== forId), - }; - }; - - return { - createRoom, - adoptRoom, - addRemoteFeed, - getRemoteFeeds, - anyRemoteRooms, - listRooms, - getRoom, - postMessage, - listMessages, - postSignal, - getSignals, - _destroy: destroy, - }; -}; diff --git a/src/models/keylock.js b/src/models/keylock.js new file mode 100644 index 0000000..a50d7e8 --- /dev/null +++ b/src/models/keylock.js @@ -0,0 +1,129 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const ssbKeys = require('../server/node_modules/ssb-keys'); + +const MAGIC = Buffer.from('OAES1\n', 'utf8'); +const KEYLEN = 32; +const DEFAULT_SCRYPT = { N: 16384, r: 8, p: 1 }; +const VERIFIER_PLAINTEXT = Buffer.from('oasis-keylock-v1', 'utf8'); + +let KEK = null; // SOLO en memoria + +const maxmemFor = (s) => Math.max(64 * 1024 * 1024, 256 * s.N * s.r); + +const deriveKEK = (password, salt, scrypt = DEFAULT_SCRYPT) => + crypto.scryptSync(Buffer.from(String(password), 'utf8'), salt, KEYLEN, + { N: scrypt.N, r: scrypt.r, p: scrypt.p, maxmem: maxmemFor(scrypt) }); + +const isWrapped = (buf) => + Buffer.isBuffer(buf) && buf.length >= MAGIC.length && buf.subarray(0, MAGIC.length).equals(MAGIC); + +const wrap = (buf, kek = KEK) => { + if (!kek) throw new Error('keylock: locked (no KEK)'); + const iv = crypto.randomBytes(12); + const c = crypto.createCipheriv('aes-256-gcm', kek, iv); + const ct = Buffer.concat([c.update(buf), c.final()]); + const tag = c.getAuthTag(); + return Buffer.concat([MAGIC, iv, tag, ct]); +}; + +const unwrap = (blob, kek = KEK) => { + if (!kek) throw new Error('keylock: locked (no KEK)'); + if (!isWrapped(blob)) throw new Error('keylock: not a wrapped blob'); + let o = MAGIC.length; + const iv = blob.subarray(o, o += 12); + const tag = blob.subarray(o, o += 16); + const ct = blob.subarray(o); + const d = crypto.createDecipheriv('aes-256-gcm', kek, iv); + d.setAuthTag(tag); + return Buffer.concat([d.update(ct), d.final()]); +}; + +const lockPath = (configPath) => path.join(configPath, 'keylock.json'); + +const isConfigured = (configPath) => { + try { return fs.existsSync(lockPath(configPath)); } catch (_) { return false; } +}; + +const setPassword = (configPath, password) => { + const salt = crypto.randomBytes(16); + const scrypt = DEFAULT_SCRYPT; + const kek = deriveKEK(password, salt, scrypt); + const verifier = wrap(VERIFIER_PLAINTEXT, kek); + const data = { + v: 1, kdf: 'scrypt', N: scrypt.N, r: scrypt.r, p: scrypt.p, + salt: salt.toString('hex'), verifier: verifier.toString('base64'), + }; + fs.mkdirSync(configPath, { recursive: true, mode: 0o700 }); + const tmp = lockPath(configPath) + '.tmp'; + fs.writeFileSync(tmp, JSON.stringify(data), { mode: 0o600 }); + fs.renameSync(tmp, lockPath(configPath)); + KEK = kek; + return kek; +}; + +const verifyPassword = (configPath, password) => { + let data; + try { data = JSON.parse(fs.readFileSync(lockPath(configPath), 'utf8')); } catch (_) { return null; } + const salt = Buffer.from(data.salt, 'hex'); + const scrypt = { N: data.N || DEFAULT_SCRYPT.N, r: data.r || DEFAULT_SCRYPT.r, p: data.p || DEFAULT_SCRYPT.p }; + const kek = deriveKEK(password, salt, scrypt); + try { + if (unwrap(Buffer.from(data.verifier, 'base64'), kek).equals(VERIFIER_PLAINTEXT)) { + KEK = kek; + return kek; + } + } catch (_) {} + return null; +}; + +const getKEK = () => KEK; +const setKEK = (k) => { KEK = k; }; +const isUnlocked = () => KEK != null; +const lock = () => { try { if (KEK) KEK.fill(0); } catch (_) {} KEK = null; }; + +const secretEncPath = (configPath) => path.join(configPath, 'secret.enc'); +const legacySecretPath = (configPath) => path.join(configPath, 'secret'); + +const shred = (p) => { + try { fs.writeFileSync(p, crypto.randomBytes(Math.max(1, fs.statSync(p).size))); } catch (_) {} + try { fs.unlinkSync(p); } catch (_) {} +}; + +const writeSecret = (configPath, keys) => { + const blob = wrap(Buffer.from(JSON.stringify(keys), 'utf8')); + fs.mkdirSync(configPath, { recursive: true, mode: 0o700 }); + const tmp = secretEncPath(configPath) + '.tmp'; + fs.writeFileSync(tmp, blob, { mode: 0o600 }); + fs.renameSync(tmp, secretEncPath(configPath)); +}; + +const loadOrCreateSecret = (configPath) => { + if (!KEK) throw new Error('keylock: locked'); + fs.mkdirSync(configPath, { recursive: true, mode: 0o700 }); + const encP = secretEncPath(configPath); + if (fs.existsSync(encP)) { + return JSON.parse(unwrap(fs.readFileSync(encP)).toString('utf8')); + } + const legP = legacySecretPath(configPath); + if (fs.existsSync(legP)) { + const keys = ssbKeys.loadSync(legP); + writeSecret(configPath, keys); + shred(legP); + return keys; + } + const keys = ssbKeys.generate('ed25519'); // primer arranque + writeSecret(configPath, keys); + return keys; +}; + +const hasIdentity = (configPath) => + fs.existsSync(secretEncPath(configPath)) || fs.existsSync(legacySecretPath(configPath)); + +module.exports = { + MAGIC, deriveKEK, wrap, unwrap, isWrapped, + isConfigured, setPassword, verifyPassword, + getKEK, setKEK, isUnlocked, lock, + loadOrCreateSecret, writeSecret, hasIdentity, +}; diff --git a/src/models/larp_model.js b/src/models/larp_model.js index e927fc9..e495d69 100644 --- a/src/models/larp_model.js +++ b/src/models/larp_model.js @@ -165,11 +165,6 @@ function getGoverningHouseKey(now = new Date()) { return HOUSE_KEYS[now.getMonth() % HOUSE_KEYS.length]; } -function getGoverningPeriodId(now = new Date()) { - const month = String(now.getMonth() + 1).padStart(2, '0'); - return `${getGoverningHouseKey(now)}:${now.getFullYear()}-${month}`; -} - module.exports = ({ cooler, tribesModel, tribeCrypto }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; @@ -573,14 +568,10 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { }); } - function wallIsPublic(houseKey, { isGoverning = false } = {}) { - return houseKey === 'academia' || isGoverning === true; - } - async function listHousePosts(houseKey, { viewerHouse = null, isGoverning = false } = {}) { if (!VALID_KEY(houseKey)) return []; const viewerIsMember = viewerHouse === houseKey; - if (!viewerIsMember && !wallIsPublic(houseKey, { isGoverning })) return []; + if (!viewerIsMember && !isGoverning) return []; const client = await openSsb(); const memberships = await listAllMemberships(); return new Promise((resolve) => { @@ -921,13 +912,11 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { return { HOUSES, HOUSE_KEYS, - wallIsPublic, TEST_COOLDOWN_MS, TEST_QUESTIONS_COUNT, PROFILE_QUESTIONS, computeCycle, getGoverningHouseKey, - getGoverningPeriodId, publishJoin, publishLeaveLarp, getUserHouse, diff --git a/src/models/legacy_model.js b/src/models/legacy_model.js index b612890..3499811 100644 --- a/src/models/legacy_model.js +++ b/src/models/legacy_model.js @@ -49,8 +49,8 @@ module.exports = { exportData: async (password) => { try { const pw = normalizePassword(password); - // identidad de la cuenta activa - const secretFilePath = path.join(require('../server/ssb_config').path, 'secret'); + const homeDir = os.homedir(); + const secretFilePath = path.join(homeDir, '.ssb', 'secret'); if (!fs.existsSync(secretFilePath)) { throw new Error(".ssb/secret file doesn't exist"); } @@ -58,9 +58,11 @@ module.exports = { const encrypted = encryptBuffer(original, pw); const roundtrip = decryptBuffer(encrypted, pw); if (!roundtrip.equals(original)) { - throw new Error('Backup verification failed; nothing was exported.'); + throw new Error('Backup verification failed; nothing was written.'); } - return { filename: 'oasis.enc', data: encrypted }; + const encryptedFilePath = path.join(homeDir, 'oasis.enc'); + fs.writeFileSync(encryptedFilePath, encrypted, { mode: 0o600 }); + return encryptedFilePath; } catch (error) { throw new Error("Error exporting data: " + error.message); } @@ -78,8 +80,8 @@ module.exports = { } catch (_) { throw new Error('Wrong password or corrupt backup file.'); } - // identidad de la cuenta activa - const ssbDir = require('../server/ssb_config').path; + const homeDir = os.homedir(); + const ssbDir = path.join(homeDir, '.ssb'); fs.mkdirSync(ssbDir, { recursive: true }); const secretPath = path.join(ssbDir, 'secret'); if (fs.existsSync(secretPath)) { diff --git a/src/models/logs_model.js b/src/models/logs_model.js index 7816900..88bade7 100644 --- a/src/models/logs_model.js +++ b/src/models/logs_model.js @@ -6,7 +6,6 @@ const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const logLimit = getConfig().ssbLogStream?.limit || 1000; - const DAY_MS = 24 * 60 * 60 * 1000; const WEEK_MS = 7 * DAY_MS; const MONTH_MS = 30 * DAY_MS; @@ -24,7 +23,7 @@ const ACTION_TYPES = new Set([ 'post', 'about', 'contact', 'feed', 'bookmark', 'image', 'audio', 'video', 'document', 'torrent', 'event', 'task', 'taskAssignment', 'votes', 'vote', 'report', 'tribe', 'chat', 'chatMessage', 'pad', 'padEntry', - 'forum', 'market', 'job', 'project', 'industry', 'industryBuild', 'pixelia', 'map', 'mapMarker', + 'forum', 'market', 'job', 'project', 'pixelia', 'map', 'mapMarker', 'shop', 'shopProduct', 'curriculum', 'gameScore', 'calendar', 'calendarDate', 'calendarNote', 'transfer', 'bankClaim', 'ubiClaim', @@ -62,8 +61,6 @@ const ACTION_PHRASES = { forum: 'posted in the forum', job: 'posted a job opportunity', project: 'advanced a project', - industry: 'founded a network-owned facility', - industryBuild: 'proposed a production build', pixelia: 'placed a pixel in pixelia', map: 'contributed to a map', mapMarker: 'placed a marker on a map', @@ -294,10 +291,7 @@ module.exports = ({ cooler }) => { const c = v?.content; if (!c) continue; if (v.author !== userId) continue; - if (c.type === 'tombstone') { - if (typeof c.target === 'string' && c.target) tombstoned.add(c.target); - continue; - } + if (c.type === 'tombstone') continue; if (c.type !== 'log') continue; if (c.replaces) replaced.set(c.replaces, dec.key || keyIn); items.push({ @@ -312,16 +306,7 @@ module.exports = ({ cooler }) => { ref: c.ref || null }); } - const parentOf = new Map(); - for (const i of items) if (i.replaces) parentOf.set(i.key, i.replaces); - const rootOf = (key) => { - let cur = key; - const seen = new Set(); - while (parentOf.has(cur) && !seen.has(cur)) { seen.add(cur); cur = parentOf.get(cur); } - return cur; - }; const survivors = items.filter(i => !tombstoned.has(i.key) && !replaced.has(i.key)); - for (const i of survivors) i.rootId = rootOf(i.key); survivors.sort((a, b) => b.ts - a.ts); return survivors; } @@ -342,14 +327,11 @@ module.exports = ({ cooler }) => { async function updateLog(id, { text, label, mode }) { const current = await getLogById(id); if (!current) return { status: 'not_found' }; - const next = { - text: text !== undefined ? text : current.text, - label: label !== undefined ? label : current.label, - mode: mode || current.mode - }; await republishLog({ replaces: current.key, - ...next, + text: text !== undefined ? text : current.text, + label: label !== undefined ? label : current.label, + mode: mode || current.mode, createdAt: current.createdAt }); return { status: 'ok' }; diff --git a/src/models/main_models.js b/src/models/main_models.js index a92f365..d53de86 100644 --- a/src/models/main_models.js +++ b/src/models/main_models.js @@ -66,8 +66,8 @@ const configure = (...customOptions) => Object.assign({}, defaultOptions, ...customOptions); // PEERS -const ebtDir = path.join(require('../server/ssb_config').path, 'ebt'); -const unfollowedPath = path.join(require('../server/ssb_config').path, 'gossip_unfollowed.json'); +const ebtDir = path.join(os.homedir(), '.ssb', 'ebt'); +const unfollowedPath = path.join(os.homedir(), '.ssb', 'gossip_unfollowed.json'); async function loadPeersFromEbt() { let result = []; @@ -501,31 +501,6 @@ async function checkLocalBlob(blobId) { } models.blob = { - getCached: async ({ blobId }) => { - const local = await checkLocalBlob(blobId); - if (local) return local; - const ssb = await cooler.open(); - const has = await new Promise((resolve) => ssb.blobs.has(blobId, (err, v) => resolve(!err && !!v))); - if (!has) { - try { ssb.blobs.want(blobId, () => {}); } catch (_) {} - return null; - } - return new Promise((resolve) => { - pull( - ssb.blobs.get(blobId), - pull.collect(async (err, bufs) => { - if (err || !bufs || !bufs.length) return resolve(null); - const buffer = Buffer.concat(bufs); - try { - const filePath = blobIdToHexPath(blobId); - await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile(filePath, buffer); - } catch (e) { /* ignore */ } - resolve(buffer); - }) - ); - }); - }, getResolved: async ({ blobId, timeout = 30000 }) => { let buf = await checkLocalBlob(blobId); if (buf) return buf; @@ -789,7 +764,7 @@ models.meta = { discovered: async () => { const ssb = await cooler.open(); const snapshot = await ssb.conn.dbPeers(); - const gossipPath = path.join(require('../server/ssb_config').path, 'gossip.json'); + const gossipPath = path.join(os.homedir(), '.ssb', 'gossip.json'); let gossipMap = new Map(); try { const gossipData = JSON.parse(await fs.readFile(gossipPath, 'utf8')); diff --git a/src/models/maps_model.js b/src/models/maps_model.js index 628507f..b1bd77c 100644 --- a/src/models/maps_model.js +++ b/src/models/maps_model.js @@ -412,11 +412,17 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { try { const ssbClient = await openSsb(); const messages = await new Promise((resolve, reject) => pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((e, m) => e ? reject(e) : resolve(m)))); + const live = new Set(); const tomb = buildValidatedTombstoneSet(messages); + for (const m of messages) { + const c = m.value && m.value.content; + if (!c) continue; + if (c.type === "map") live.add(m.key); + } const all = ownCrypto.getAllRootIds(); let removed = 0; for (const rid of all) { - if (tomb.has(rid)) { + if (!live.has(rid) || tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1; } catch (_) {} } } @@ -864,7 +870,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { let mapKey = null; if (tribeCrypto && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3); + const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt); if (Array.isArray(chain) && chain.length) { for (const entry of chain) { if (Array.isArray(entry.keys) && entry.keys.length) { diff --git a/src/models/market_model.js b/src/models/market_model.js index ed2a038..f4143a7 100644 --- a/src/models/market_model.js +++ b/src/models/market_model.js @@ -1,7 +1,6 @@ const pull = require("../server/node_modules/pull-stream") const moment = require("../server/node_modules/moment") const { getConfig } = require("../configs/config-manager.js") -const categories = require("../backend/opinion_categories") const { buildValidatedTombstoneSet } = require('./tombstone_validator') const { dedupeByPreferring, norm } = require('../backend/dedupe') const logLimit = getConfig().ssbLogStream?.limit || 1000 @@ -74,14 +73,12 @@ module.exports = ({ cooler, tribeCrypto }) => { const nodes = new Map() const bids = [] const purchases = [] - const opinionMsgs = [] for (const m of messages) { const c = m.value && m.value.content if (!c) continue if (c.type === "tombstone") continue if (c.type === "marketBid") { bids.push({ target: c.target, author: m.value.author, amount: c.amount, time: c.time, ts: (m.value && m.value.timestamp) || 0 }); continue } if (c.type === "marketPurchase") { purchases.push({ target: c.target, author: m.value.author, ts: (m.value && m.value.timestamp) || 0 }); continue } - if (c.type === "marketOpinion" && c.target) { opinionMsgs.push({ target: c.target, author: m.value.author, category: c.category }); continue } if (c.type !== "market") continue nodes.set(m.key, { key: m.key, ts: (m.value && m.value.timestamp) || m.timestamp || 0, c, author: m.value.author }) } @@ -98,11 +95,11 @@ module.exports = ({ cooler, tribeCrypto }) => { if (!cn || cn.author !== pn.author) { naivePrev.delete(child); nodes.delete(child); continue } strictNext.set(parent, child) } - return { tomb, nodes, bids, purchases, opinionMsgs, naivePrev, strictNext } + return { tomb, nodes, bids, purchases, naivePrev, strictNext } } const resolveGroups = (idx) => { - const { tomb, nodes, bids, purchases, opinionMsgs, naivePrev, strictNext } = idx + const { tomb, nodes, bids, purchases, naivePrev, strictNext } = idx const rootOf = (key) => { let x = key, g = 0; while (naivePrev.has(x) && nodes.has(naivePrev.get(x)) && g++ < 100000) x = naivePrev.get(x); return x } const followStrict = (key) => { let x = key, g = 0; while (strictNext.has(x) && g++ < 100000) x = strictNext.get(x); return x } @@ -111,21 +108,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const bidsByRoot = new Map() for (const bd of bids) { if (!nodes.has(bd.target)) continue; const r = rootOf(bd.target); if (!bidsByRoot.has(r)) bidsByRoot.set(r, []); bidsByRoot.get(r).push(bd) } const soldByRoot = new Map() - const buyersByRoot = new Map() - for (const pu of purchases) { - if (!nodes.has(pu.target)) continue - const r = rootOf(pu.target) - soldByRoot.set(r, (soldByRoot.get(r) || 0) + 1) - if (!buyersByRoot.has(r)) buyersByRoot.set(r, new Set()) - buyersByRoot.get(r).add(pu.author) - } - const opinionsByRoot = new Map() - for (const op of (opinionMsgs || [])) { - if (!nodes.has(op.target)) continue - const r = rootOf(op.target) - if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []) - opinionsByRoot.get(r).push(op) - } + for (const pu of purchases) { if (!nodes.has(pu.target)) continue; const r = rootOf(pu.target); soldByRoot.set(r, (soldByRoot.get(r) || 0) + 1) } const out = new Map() for (const [root, keys] of groups) { @@ -148,16 +131,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const addLine = (line) => { const b = parseBidEntry(line); if (!b) return; const kk = `${b.bidder}|${b.amount}|${b.time}`; if (pollSet.has(kk)) return; pollSet.add(kk); poll.push(line) } for (const k of (sellerKeys.length ? sellerKeys : keys)) { const n = nodes.get(k); if (n && Array.isArray(n.c.auctions_poll)) for (const line of n.c.auctions_poll) addLine(line) } for (const bd of (bidsByRoot.get(root) || [])) { const amt = Number(bd.amount); addLine(`${bd.author}|${Number.isFinite(amt) ? amt.toFixed(6) : bd.amount}|${bd.time}`) } - const opinions = {} - const voters = [] - for (const op of (opinionsByRoot.get(root) || [])) { - if (!op.author || op.author === sellerId) continue - if (voters.includes(op.author)) continue - if (!categories.includes(op.category)) continue - voters.push(op.author) - opinions[op.category] = (opinions[op.category] || 0) + 1 - } - out.set(root, { tip, rootId: root, best, statusN: bestS, poll, soldCount: soldByRoot.get(root) || 0, buyers: Array.from(buyersByRoot.get(root) || []), opinions, voters }) + out.set(root, { tip, rootId: root, best, statusN: bestS, poll, soldCount: soldByRoot.get(root) || 0 }) } return out } @@ -206,7 +180,6 @@ module.exports = ({ cooler, tribeCrypto }) => { shopProductId: shopOpts.shopProductId || "", shopId: shopOpts.shopId || "", shopTitle: shopOpts.shopTitle || "", - industry: shopOpts.industry || "", visibility: String(visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? "HIDDEN" : "PUBLIC" } @@ -254,13 +227,9 @@ module.exports = ({ cooler, tribeCrypto }) => { normalized.price = p.toFixed(6) } - const currentItem = await new Promise((resolve) => ssbClient.get(tipId, (e, m) => resolve(e ? null : (m && m.content) || null))) - if (normalized.deadline !== undefined && normalized.deadline !== null && normalized.deadline !== "") { const dl = moment(normalized.deadline, moment.ISO_8601, true) if (!dl.isValid()) throw new Error("Invalid deadline") - const changed = !currentItem || !currentItem.deadline || !moment(currentItem.deadline).isSame(dl) - if (changed && dl.isBefore(moment(), "minute")) throw new Error("The deadline cannot be in the past") normalized.deadline = dl.toISOString() } @@ -328,7 +297,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const items = [] const now = moment() - for (const { tip, rootId, best, statusN, poll, soldCount, buyers, opinions, voters } of resolveGroups(buildMarketIndex(messages)).values()) { + for (const { tip, rootId, best, statusN, poll, soldCount } of resolveGroups(buildMarketIndex(messages)).values()) { const leaf = tip const c = best.c let status = D(statusN) @@ -375,12 +344,7 @@ module.exports = ({ cooler, tribeCrypto }) => { mapUrl: c.mapUrl || "", shopProductId: c.shopProductId || "", shopId: c.shopId || "", - shopTitle: c.shopTitle || "", - industry: c.industry || "", - opinions: opinions || {}, - opinions_inhabitants: voters || [], - purchasedByViewer: Array.isArray(buyers) && buyers.includes(userId), - ratedByViewer: Array.isArray(voters) && voters.includes(userId) + shopTitle: c.shopTitle || "" }) } @@ -435,7 +399,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const rootOf = (key) => { let x = key, g = 0; while (idx.naivePrev.has(x) && idx.nodes.has(idx.naivePrev.get(x)) && g++ < 100000) x = idx.naivePrev.get(x); return x } const grp = resolveGroups(idx).get(rootOf(itemId)) if (!grp) return null - const { tip, rootId, best, statusN, poll, soldCount, buyers, opinions, voters } = grp + const { tip, rootId, best, statusN, poll, soldCount } = grp const c = best.c let status = D(statusN) @@ -481,12 +445,7 @@ module.exports = ({ cooler, tribeCrypto }) => { mapUrl: c.mapUrl || "", shopProductId: c.shopProductId || "", shopId: c.shopId || "", - shopTitle: c.shopTitle || "", - industry: c.industry || "", - opinions: opinions || {}, - opinions_inhabitants: voters || [], - purchasedByViewer: Array.isArray(buyers) && buyers.includes(userId), - ratedByViewer: Array.isArray(voters) && voters.includes(userId) + shopTitle: c.shopTitle || "" } }, @@ -527,24 +486,6 @@ module.exports = ({ cooler, tribeCrypto }) => { return items.find((i) => i.shopProductId === shopProductId) || null }, - async createOpinion(itemId, category) { - if (!categories.includes(category)) throw new Error("Invalid category") - const ssbClient = await openSsb() - const userId = ssbClient.id - const messages = await readAll(ssbClient) - const groups = resolveGroups(buildMarketIndex(messages)) - let group = null - for (const g of groups.values()) { - if (g.rootId === itemId || g.tip === itemId) { group = g; break } - } - if (!group) throw new Error("Item not found") - if (group.best.author === userId) throw new Error("You cannot rate your own item") - if ((group.voters || []).includes(userId)) throw new Error("Already voted") - if (!(group.buyers || []).includes(userId)) throw new Error("You can rate only an item you have bought") - const content = { type: "marketOpinion", target: group.rootId, category, createdAt: new Date().toISOString() } - return new Promise((res, rej) => ssbClient.publish(content, (e, m) => e ? rej(e) : res(m))) - }, - async setItemAsSold(itemId) { const tipId = await this.resolveCurrentId(itemId) const ssbClient = await openSsb() diff --git a/src/models/media_gallery.js b/src/models/media_gallery.js deleted file mode 100644 index 3d8c7b4..0000000 --- a/src/models/media_gallery.js +++ /dev/null @@ -1,42 +0,0 @@ -const MAX_IMAGES = 8 - -const MEDIA_MARKDOWN = /\[(image|video|audio):[^\]]*\]\(/ - -const blobOf = (value) => { - let blobId = value || null - if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1] - return blobId || null -} - -const keepMarkdown = (value, id) => - MEDIA_MARKDOWN.test(value) || value.startsWith("![") ? value : id - -const normalizeImages = (raw, max = MAX_IMAGES) => { - const list = Array.isArray(raw) ? raw : (raw ? [raw] : []) - const out = [] - const seen = new Set() - for (const entry of list) { - const value = String(entry || "").trim() - const id = blobOf(value) - if (!id || seen.has(id)) continue - seen.add(id) - out.push(keepMarkdown(value, id)) - if (out.length >= max) break - } - return out -} - -const normalizeVideo = (raw) => { - const value = String(Array.isArray(raw) ? raw[0] || "" : raw || "").trim() - const id = blobOf(value) - if (!id) return "" - return keepMarkdown(value, id) -} - -const mergeGallery = (current, uploaded, removeIndex = -1, max = MAX_IMAGES) => { - let list = normalizeImages(current, max) - if (removeIndex >= 0) list = list.filter((_, i) => i !== removeIndex) - return normalizeImages([...list, ...normalizeImages(uploaded, max)], max) -} - -module.exports = { MAX_IMAGES, blobOf, normalizeImages, normalizeVideo, mergeGallery } diff --git a/src/models/mentions_model.js b/src/models/mentions_model.js deleted file mode 100644 index 3f1054b..0000000 --- a/src/models/mentions_model.js +++ /dev/null @@ -1,171 +0,0 @@ -const fs = require('fs'); -const pull = require('../server/node_modules/pull-stream'); - -const SEEN_FILE = 'oasis-mentions-seen'; - -const seenPath = () => { - try { return require('../server/ssb_config').statePath(SEEN_FILE); } catch (_) { return null; } -}; - -const MAX_SEEN_KEYS = 500; - -const readSeen = () => { - const p = seenPath(); - if (!p) return { ts: 0, keys: [] }; - let raw = ''; - try { raw = fs.readFileSync(p, 'utf8').trim(); } catch (_) { return { ts: 0, keys: [] }; } - if (!raw) return { ts: 0, keys: [] }; - try { - const parsed = JSON.parse(raw); - if (parsed && typeof parsed === 'object') { - return { ts: Number(parsed.ts) || 0, keys: Array.isArray(parsed.keys) ? parsed.keys : [] }; - } - } catch (_) {} - return { ts: Number(raw) || 0, keys: [] }; -}; - -const writeSeen = (keys, ts) => { - const p = seenPath(); - if (!p) return false; - const trimmed = (Array.isArray(keys) ? keys : []).filter(Boolean).slice(0, MAX_SEEN_KEYS); - try { fs.writeFileSync(p, JSON.stringify({ ts: Number(ts) || Date.now(), keys: trimmed })); return true; } catch (_) { return false; } -}; -const { getConfig } = require('../configs/config-manager.js'); -const { buildValidatedTombstoneSet } = require('./tombstone_validator'); -const logLimit = getConfig().ssbLogStream?.limit || 1000; - -const TEXT_FIELDS = ['text', 'description', 'title', 'concept', 'question', 'rules', 'message', 'body', 'subject']; - -const SKIP_TYPES = new Set([ - 'about', 'contact', 'vote', 'pub', 'tombstone', 'karmaScore', - 'tribe-invite-msg', 'tribe-invite-tombstone', 'tribe-key-distrib' -]); - -module.exports = ({ cooler }) => { - let ssb; - const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; - - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); - - const mentionsList = (content, feedId) => { - const m = content && content.mentions; - if (!m) return false; - const values = Array.isArray(m) ? m : (typeof m === 'object' ? Object.values(m).flat() : []); - return values.some(v => v && (v.link === feedId || v === feedId)); - }; - - const mentionsText = (content, feedId) => { - const bare = feedId.startsWith('@') ? feedId.slice(1) : feedId; - for (const field of TEXT_FIELDS) { - const v = content[field]; - if (typeof v === 'string' && (v.includes(feedId) || v.includes(bare))) return true; - } - if (Array.isArray(content.tags)) { - if (content.tags.some(t => typeof t === 'string' && (t.includes(feedId) || t.includes(bare)))) return true; - } - return false; - }; - - const snippet = (content) => { - for (const field of TEXT_FIELDS) { - const v = content[field]; - if (typeof v === 'string' && v.trim()) return v.trim(); - } - return ''; - }; - - const titleOf = (content) => { - for (const field of ['title', 'concept', 'question', 'subject']) { - const v = content[field]; - if (typeof v === 'string' && v.trim()) return v.trim(); - } - return ''; - }; - - return { - async listMentions(filter = 'ALL', opts = {}) { - const ssbClient = await openSsb(); - const myFeedId = ssbClient.id; - const messages = await getAllMessages(ssbClient); - const tomb = buildValidatedTombstoneSet(messages); - - const out = []; - const seen = new Set(); - const replaced = new Set(); - for (const m of messages) { - const v = m && m.value; - const c = v && v.content; - if (!c || typeof c !== 'object' || !c.type) continue; - if (tomb.has(m.key)) continue; - if (SKIP_TYPES.has(c.type)) continue; - if (c.encryptedPayload || c.encryptedQuestion) continue; - if (Array.isArray(c.recps) || Array.isArray(v.content && v.content.recps)) continue; - if (c.tribeId && c.type !== 'tribe') continue; - if (String(v.author) === String(myFeedId)) continue; - if (typeof c.replaces === 'string') replaced.add(c.replaces); - if (!mentionsList(c, myFeedId) && !mentionsText(c, myFeedId)) continue; - if (seen.has(m.key)) continue; - seen.add(m.key); - out.push({ - id: m.key, - key: m.key, - author: v.author, - type: c.type, - title: titleOf(c), - text: snippet(c), - content: c, - createdAt: new Date(v.timestamp || m.timestamp || 0).toISOString(), - ts: v.timestamp || m.timestamp || 0 - }); - } - - const fresh = out.filter(x => !replaced.has(x.key)); - const q = String(opts.q || '').trim().toLowerCase(); - let list = q - ? fresh.filter(x => x.text.toLowerCase().includes(q) || x.title.toLowerCase().includes(q) || x.type.toLowerCase().includes(q)) - : fresh; - - const f = String(filter || 'ALL').toUpperCase(); - if (f !== 'ALL' && f !== 'RECENT') list = list.filter(x => x.type === filter); - - list.sort((a, b) => b.ts - a.ts); - return list; - }, - - seenAt: () => readSeen().ts, - - markSeen(list) { - const items = Array.isArray(list) ? list : []; - const keys = items.map(x => x && x.key).filter(Boolean); - const newest = items.reduce((max, x) => Math.max(max, Number(x && x.ts) || 0), 0); - return writeSeen(keys, newest || Date.now()); - }, - - unseenOf(list) { - const seen = readSeen(); - const known = new Set(seen.keys); - const items = Array.isArray(list) ? list : []; - if (known.size) return items.filter(x => x && !known.has(x.key)); - if (seen.ts) return items.filter(x => x && (Number(x.ts) || 0) > seen.ts); - return items; - }, - - async countUnseen() { - return this.unseenOf(await this.listMentions('ALL')).length; - }, - - async countTypes(list) { - const counts = {}; - for (const item of (Array.isArray(list) ? list : [])) { - counts[item.type] = (counts[item.type] || 0) + 1; - } - return counts; - } - }; -}; diff --git a/src/models/onboarding_model.js b/src/models/onboarding_model.js deleted file mode 100644 index 8520a69..0000000 --- a/src/models/onboarding_model.js +++ /dev/null @@ -1,182 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const pull = require('../server/node_modules/pull-stream'); - -const STEPS = ['language', 'ux', 'profile', 'federation', 'larp', 'greeting', 'backup']; -const FLAG = 'oasis-first-contact'; -const PENDING = 'welcome=pending'; -const DISMISSED = 'welcome=dismissed'; -const DONE = 'welcome=done'; - -const ssbPathOf = (given) => { - if (given) return given; - try { return require('../server/ssb_config').path; } catch (_) { return null; } -}; - -const flagPath = (dir) => path.join(dir, FLAG); - -function readFlag(dir) { - if (!dir) return { exists: false, id: '', markers: new Set() }; - let raw = ''; - try { raw = fs.readFileSync(flagPath(dir), 'utf8'); } catch (_) { return { exists: false, id: '', markers: new Set() }; } - const lines = raw.split('\n').map(l => l.trim()).filter(Boolean); - const isMarker = (l) => /^(welcome|step)=/.test(l); - const markers = new Set(lines.filter(isMarker)); - return { exists: true, id: lines.find(l => !isMarker(l)) || '', markers }; -} - -function appendMarker(dir, marker, createIfMissing) { - if (!dir) return false; - const flag = readFlag(dir); - if (flag.markers.has(marker)) return true; - if (!flag.exists && !createIfMissing) return false; - try { - fs.mkdirSync(dir, { recursive: true }); - fs.appendFileSync(flagPath(dir), `${marker}\n`); - return true; - } catch (_) { - return false; - } -} - -const isPending = (dir) => { - if (!dir) return false; - const flag = readFlag(dir); - if (!flag.exists) return true; - if (flag.markers.has(DISMISSED) || flag.markers.has(DONE)) return false; - return flag.markers.has(PENDING); -}; - -const bannerVisible = (ssbPath) => isPending(ssbPathOf(ssbPath)); - -module.exports = ({ cooler, ssbPath } = {}) => { - const dir = () => ssbPathOf(ssbPath); - - const ownMessages = async () => { - const ssb = await cooler.open(); - const mine = ssb.id; - const all = await new Promise((resolve, reject) => - pull(ssb.createLogStream({ reverse: true, limit: 2000 }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs || []))) - ); - return { mine, messages: all.filter(m => m && m.value && m.value.author === mine) }; - }; - - const hasProfile = (mine, messages) => messages.some(m => { - const c = m.value.content; - if (!c || typeof c !== 'object' || c.type !== 'about' || c.about !== mine) return false; - return String(c.name || '').trim().length > 0 || String(c.image || '').trim().length > 0; - }); - - const hasGreeted = (messages) => messages.some(m => { - const c = m.value.content; - return c && typeof c === 'object' && c.type === 'feed' && String(c.text || '').trim().length > 0; - }); - - const hasJoinedLarp = (messages) => { - for (const m of messages) { - const c = m.value.content; - if (!c || typeof c !== 'object') continue; - if (c.type === 'larpJoinHouse' && c.house) return true; - if (c.type === 'larpLeaveLarp') return false; - } - return false; - }; - - const hasFederated = (mine, messages) => { - try { - const gossip = JSON.parse(fs.readFileSync(path.join(dir(), 'gossip.json'), 'utf8')); - if (Array.isArray(gossip) && gossip.length > 0) return true; - } catch (_) {} - return messages.some(m => { - const c = m.value.content; - return c && typeof c === 'object' && c.type === 'contact' && c.following === true && c.contact && c.contact !== mine; - }); - }; - - return { - STEPS, - - flagPath() { - return flagPath(dir()); - }, - - firstContactSeen(feedId) { - const flag = readFlag(dir()); - return flag.exists && (!feedId || flag.id === String(feedId)); - }, - - begin(feedId) { - const target = dir(); - if (!target) return false; - const flag = readFlag(target); - const closed = flag.markers.has(DISMISSED) || flag.markers.has(DONE); - const markers = closed ? [...flag.markers] : [...new Set([PENDING, ...flag.markers])]; - try { - fs.mkdirSync(target, { recursive: true }); - fs.writeFileSync(flagPath(target), `${String(feedId || '')}\n${new Date().toISOString()}\n${markers.join('\n')}\n`); - return true; - } catch (_) { - return false; - } - }, - - markStep(step) { - if (!STEPS.includes(step)) return false; - if (step !== 'language' && step !== 'backup' && step !== 'ux') return true; - if (!isPending(dir())) return true; - return appendMarker(dir(), `step=${step}`); - }, - - dismiss() { - return appendMarker(dir(), DISMISSED, true); - }, - - isVisible() { - return isPending(dir()); - }, - - async status(available) { - const target = dir(); - const flag = readFlag(target); - const usable = STEPS.filter(s => !available || available[s] !== false); - let mine = ''; - let messages = []; - try { ({ mine, messages } = await ownMessages()); } catch (_) { mine = ''; messages = []; } - const steps = { - language: flag.markers.has('step=language'), - profile: hasProfile(mine, messages), - federation: hasFederated(mine, messages), - larp: hasJoinedLarp(messages), - ux: flag.markers.has('step=ux'), - backup: flag.markers.has('step=backup'), - greeting: hasGreeted(messages) - }; - const profile = { id: mine, name: '', description: '', image: '' }; - for (const m of messages) { - const c = m.value.content; - if (!c || typeof c !== 'object' || c.type !== 'about' || c.about !== mine) continue; - if (!profile.name && String(c.name || '').trim()) profile.name = String(c.name).trim(); - if (!profile.description && String(c.description || '').trim()) profile.description = String(c.description).trim(); - if (!profile.image && String(c.image || '').trim()) profile.image = String(c.image).trim(); - if (profile.name && profile.description && profile.image) break; - } - const done = usable.filter(s => steps[s]).length; - const complete = done === usable.length; - if (complete && isPending(target)) appendMarker(target, DONE); - return { - id: flag.id, - pending: isPending(target), - dismissed: flag.markers.has(DISMISSED), - steps, - profile, - usable, - done, - total: usable.length, - complete - }; - } - }; -}; - -module.exports.bannerVisible = bannerVisible; -module.exports.STEPS = STEPS; diff --git a/src/models/opinions_model.js b/src/models/opinions_model.js index 1df58e1..4ffb636 100644 --- a/src/models/opinions_model.js +++ b/src/models/opinions_model.js @@ -22,13 +22,12 @@ module.exports = ({ cooler }) => { const validTypes = [ 'bookmark', 'votes', 'transfer', - 'feed', 'image', 'audio', 'video', 'document', 'torrent', - 'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market' + 'feed', 'image', 'audio', 'video', 'document', 'torrent' ]; const getPreview = c => { if (c.type === 'bookmark' && c.bookmark) return `🔖 ${c.bookmark}`; - return c.text || c.description || c.title || c.name || ''; + return c.text || c.description || c.title || ''; }; const OPINION_MSG_TYPE = { @@ -40,15 +39,7 @@ module.exports = ({ cooler }) => { audio: 'audioOpinion', video: 'videoOpinion', document: 'documentOpinion', - torrent: 'torrentOpinion', - industry: 'industryOpinion', - project: 'projectOpinion', - report: 'reportOpinion', - task: 'taskOpinion', - event: 'eventOpinion', - shopProduct: 'shopOpinion', - housing: 'housingOpinion', - market: 'marketOpinion' + torrent: 'torrentOpinion' }; const createVote = async (contentId, category) => { @@ -81,7 +72,6 @@ module.exports = ({ cooler }) => { const tombstoned = buildValidatedTombstoneSet(messages); const replaces = new Map(); const byId = new Map(); - const authorOf = new Map(); const opinionMsgs = []; for (const msg of messages) { @@ -92,7 +82,6 @@ module.exports = ({ cooler }) => { if (tombstoned.has(c.target)) byId.delete(c.target); continue; } - if (key) authorOf.set(key, msg.value.author); if (typeof c.type === 'string' && c.type.endsWith('Opinion') && c.target) { opinionMsgs.push({ target: c.target, author: msg.value.author, category: c.category }); continue; @@ -115,10 +104,10 @@ module.exports = ({ cooler }) => { } for (const [oldId, newId] of Array.from(replaces.entries())) { - const oldAuthor = authorOf.get(oldId); + const oldM = byId.get(oldId); const newM = byId.get(newId); - if (oldAuthor === undefined) { replaces.delete(oldId); continue; } - if (!newM || String(newM.value.author) !== String(oldAuthor)) { + if (!oldM) { replaces.delete(oldId); continue; } + if (!newM || String(newM.value.author) !== String(oldM.value.author)) { replaces.delete(oldId); byId.delete(newId); } @@ -237,11 +226,6 @@ module.exports = ({ cooler }) => { } filtered = Array.from(bySig.values()); - filtered = filtered.filter(m => { - const ops = m.value?.content?.opinions || {}; - return Object.values(ops).reduce((acc, x) => acc + (Number(x) || 0), 0) > 0; - }); - if (filter === 'MINE') { filtered = filtered.filter(m => m.value.author === userId); } else if (filter === 'RECENT') { diff --git a/src/models/pads_model.js b/src/models/pads_model.js index f6a42a8..6390e46 100644 --- a/src/models/pads_model.js +++ b/src/models/pads_model.js @@ -615,11 +615,17 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) try { const ssbClient = await openSsb() const messages = await readAll(ssbClient) + const live = new Set() const tomb = buildValidatedTombstoneSet(messages) + for (const m of messages) { + const c = m.value && m.value.content + if (!c) continue + if (c.type === "pad") live.add(m.key) + } const all = ownCrypto.getAllRootIds() let removed = 0 for (const rid of all) { - if (tomb.has(rid)) { + if (!live.has(rid) || tomb.has(rid)) { try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {} } } diff --git a/src/models/panicmode_model.js b/src/models/panicmode_model.js index 0c11a0d..95933fc 100644 --- a/src/models/panicmode_model.js +++ b/src/models/panicmode_model.js @@ -5,8 +5,8 @@ const path = require('path'); module.exports = { removeSSB: async () => { try { - // ruta de la cuenta activa: con varias identidades, ~/.ssb fijo borraria otra - const ssbPath = require('../server/ssb_config').path; + const homeDir = os.homedir(); + const ssbPath = path.join(homeDir, '.ssb'); await fs.promises.rm(ssbPath, { recursive: true, force: true }); } catch (error) { throw new Error("Error deleting data: " + error.message); diff --git a/src/models/parliament_model.js b/src/models/parliament_model.js index 18e78f7..83dc837 100644 --- a/src/models/parliament_model.js +++ b/src/models/parliament_model.js @@ -12,20 +12,6 @@ const QUORUM_RATIO = 0.25; const VOTE_METHODS = new Set(['DEMOCRACY', 'ANARCHY', 'MAJORITY', 'MINORITY']); const METHODS = ['DEMOCRACY', 'MAJORITY', 'MINORITY', 'DICTATORSHIP', 'KARMATOCRACY']; const FEED_ID_RE = /^@.+\.ed25519$/; -const TERM_EPOCH = Date.UTC(2020, 0, 1); -const TERM_SPAN_MS = TERM_DAYS * 86400000; - -const termWindowFor = (at = Date.now()) => { - const ms = at instanceof Date ? at.getTime() : Number(at); - const base = Number.isFinite(ms) ? ms : Date.now(); - const index = Math.floor((base - TERM_EPOCH) / TERM_SPAN_MS); - const startMs = TERM_EPOCH + index * TERM_SPAN_MS; - return { - cycle: index, - startAt: new Date(startMs).toISOString(), - endAt: new Date(startMs + TERM_SPAN_MS).toISOString() - }; -}; module.exports = ({ cooler, services = {} }) => { let ssb; @@ -265,13 +251,7 @@ if (c.type === type) { async function listCandidaturesOpenRaw() { const all = await listByType('parliamentCandidature'); - const window = termWindowFor(Date.now()); - const from = new Date(window.startAt).getTime(); - const filtered = all.filter(c => { - if ((c.status || 'OPEN') !== 'OPEN') return false; - const created = new Date(c.createdAt).getTime(); - return Number.isFinite(created) ? created >= from : true; - }); + const filtered = all.filter(c => (c.status || 'OPEN') === 'OPEN'); return filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } @@ -365,28 +345,6 @@ if (c.type === type) { return isExpiredTerm(t) ? null : t; } - function virtualAnarchyTerm(window = termWindowFor(Date.now())) { - return { - id: `anarchy:${window.cycle}`, - virtual: true, - type: 'parliamentTerm', - cycle: window.cycle, - method: 'ANARCHY', - powerType: 'none', - powerId: null, - powerTitle: 'ANARCHY', - winnerTribeId: null, - winnerInhabitantId: null, - winnerVotes: 0, - totalVotes: 0, - population: 0, - startAt: window.startAt, - endAt: window.endAt, - createdBy: null, - createdAt: window.startAt - }; - } - function currentCycleStart(term) { return term ? term.startAt : moment().subtract(TERM_DAYS, 'days').toISOString(); } @@ -394,7 +352,6 @@ if (c.type === type) { async function archiveAllCandidatures() { const all = await listCandidaturesOpenRaw(); for (const c of all) { - if (String(c.proposer || '') !== String(userId)) continue; const updated = { ...stripId(c), replaces: c.id, status: 'DISCARDED', updatedAt: nowISO() }; await publishMsg(updated); } @@ -517,12 +474,7 @@ if (c.type === type) { const pol = await summarizePoliciesForTerm({ ...term }); const eff = pol.proposed > 0 ? Math.round((pol.approved / pol.proposed) * 100) : 0; const cands = await listByType('parliamentCandidature'); - const cycleStartMs = term.startAt ? new Date(term.startAt).getTime() : 0; - const cycleEndMs = term.endAt ? new Date(term.endAt).getTime() : Number.MAX_SAFE_INTEGER; - const presented = cands.filter(c => { - const t = c.createdAt ? new Date(c.createdAt).getTime() : 0; - return t >= cycleStartMs && t <= cycleEndMs; - }).length; + const presented = cands.length; return { method, powerType: term.powerType, @@ -756,7 +708,10 @@ if (c.type === type) { async function createProposal({ title, description }) { let term = await getCurrentTermBase(); - if (!term) term = await resolveElection(); + if (!term) { + await resolveElection(); + term = await getCurrentTermBase(); + } if (!term) throw new Error('No active government'); const allowed = await canPropose(); if (!allowed) throw new Error('You are not in the goverment, yet.'); @@ -873,11 +828,6 @@ if (c.type === type) { } async function getCurrentTerm() { - const published = await getCurrentTermBase(); - return published || virtualAnarchyTerm(); - } - - async function getPublishedTerm() { return await getCurrentTermBase(); } @@ -1180,20 +1130,44 @@ if (c.type === type) { } } - const window = termWindowFor(now.valueOf()); - const startAt = window.startAt; - const endAt = window.endAt; - const population = await inhabitantsCount(); + const startAt = now.toISOString(); + const endAt = moment(startAt).add(TERM_DAYS, 'days').toISOString(); if (!chosen) { + const termAnarchy = { + type: 'parliamentTerm', + method: 'ANARCHY', + powerType: 'none', + powerId: null, + powerTitle: 'ANARCHY', + winnerTribeId: null, + winnerInhabitantId: null, + winnerVotes: 0, + totalVotes: 0, + startAt, + endAt, + createdBy: userId, + createdAt: nowISO() + }; + + const resAnarchy = await publishMsg(termAnarchy); + try { + await sleep(250); + const canonical = await getCurrentTermBase(); + const myId = resAnarchy.key || resAnarchy.id; + if (canonical && canonical.id && myId && canonical.id !== myId) { + const tomb = { type: 'tombstone', target: myId, deletedAt: nowISO(), author: userId }; + await publishMsg(tomb); + await archiveAllCandidatures(); + return canonical; + } + } catch {} await archiveAllCandidatures(); - return virtualAnarchyTerm(window); + return resAnarchy; } const term = { type: 'parliamentTerm', - cycle: window.cycle, - population, method: chosen.method, powerType: chosen.targetType, powerId: chosen.targetId, @@ -1236,45 +1210,6 @@ if (c.type === type) { return await computeGovernmentCard({ ...term, id: term.id || term.startAt }); } - async function getLatestTerm() { - return await getLatestTermAny(); - } - - async function getLatestGovernmentCard() { - const term = await getLatestTermAny(); - if (!term) return null; - const card = await computeGovernmentCard({ ...term, id: term.id || term.startAt }); - const current = termWindowFor(Date.now()); - const termEnd = term.endAt ? new Date(term.endAt).getTime() : NaN; - const rolled = Number.isFinite(termEnd) && termEnd <= Date.now(); - if (rolled) { - return { - ...card, - id: term.id || term.startAt, - cycle: current.cycle, - since: current.startAt, - end: current.endAt, - method: 'ANARCHY', - powerType: 'none', - powerId: null, - powerTitle: 'ANARCHY', - winnerVotes: 0, - totalVotes: 0, - votesReceived: 0, - presented: 0, - proposed: 0, - approved: 0, - declined: 0, - discarded: 0, - revocated: 0, - efficiency: 0, - expired: false, - rolled: true - }; - } - return { ...card, id: term.id || term.startAt, since: term.startAt, end: term.endAt, expired: false, rolled: false }; - } - async function listLaws() { const items = await listByType('parliamentLaw'); return items.sort((a, b) => new Date(b.enactedAt) - new Date(a.enactedAt)); @@ -1529,9 +1464,6 @@ if (c.type === type) { listCandidatures, listTerms, getCurrentTerm, - getPublishedTerm, - getLatestTerm, - getLatestGovernmentCard, listLeaders, sweepProposals, getActorMeta, @@ -1560,29 +1492,6 @@ if (c.type === type) { function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } -function compareTermsForWindow(a, b) { - const num = (t, field) => Number(t && t[field]) || 0; - const anarchy = (t) => (String(t && t.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0); - - const populationA = num(a, 'population'); - const populationB = num(b, 'population'); - if (populationA !== populationB) return populationB - populationA; - - const turnoutA = num(a, 'totalVotes'); - const turnoutB = num(b, 'totalVotes'); - if (turnoutA !== turnoutB) return turnoutB - turnoutA; - - const votesA = num(a, 'winnerVotes'); - const votesB = num(b, 'winnerVotes'); - if (votesA !== votesB) return votesB - votesA; - - const anarchyA = anarchy(a); - const anarchyB = anarchy(b); - if (anarchyA !== anarchyB) return anarchyA - anarchyB; - - return String(a && a.id || '').localeCompare(String(b && b.id || '')); -} - function collapseOverlappingTerms(terms = []) { if (!terms.length) return []; const sorted = [...terms].sort((a, b) => new Date(a.startAt) - new Date(b.startAt)); @@ -1603,13 +1512,20 @@ function collapseOverlappingTerms(terms = []) { if (!placed) groups.push({ items: [t], minStart: tStart, maxEnd: tEnd }); } const winners = groups.map(g => { - g.items.sort(compareTermsForWindow); + g.items.sort((a, b) => { + const aAn = String(a.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0; + const bAn = String(b.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0; + if (aAn !== bAn) return aAn - bAn; + const aC = new Date(a.createdAt || a.startAt).getTime(); + const bC = new Date(b.createdAt || b.startAt).getTime(); + if (aC !== bC) return aC - bC; + const aS = new Date(a.startAt).getTime(); + const bS = new Date(b.startAt).getTime(); + if (aS !== bS) return aS - bS; + return String(a.id || '').localeCompare(String(b.id || '')); + }); return g.items[0]; }); return winners.sort((a, b) => new Date(b.startAt) - new Date(a.startAt)); } -module.exports.termWindowFor = termWindowFor; -module.exports.compareTermsForWindow = compareTermsForWindow; -module.exports.collapseOverlappingTerms = collapseOverlappingTerms; -module.exports.TERM_DAYS = TERM_DAYS; diff --git a/src/models/pm_model.js b/src/models/pm_model.js index 2fe5b46..03be1d3 100644 --- a/src/models/pm_model.js +++ b/src/models/pm_model.js @@ -28,7 +28,7 @@ module.exports = ({ cooler }) => { return { type: 'post', - async sendMessage(recipients = [], subject = '', text = '', crypter = false, ref = '') { + async sendMessage(recipients = [], subject = '', text = '', crypter = false) { const ssbClient = await openSsb(); const recps = uniqueRecps([userId, ...recipients]); const content = { @@ -39,39 +39,6 @@ module.exports = ({ cooler }) => { text, sentAt: new Date().toISOString(), private: true, - ...(ref ? { ref: String(ref) } : {}), - ...(crypter ? { crypter: true } : {}) - }; - const publishAsync = util.promisify(ssbClient.private.publish); - return publishAsync(content, recps); - }, - - async sentRefs() { - const out = new Set(); - let messages = []; - try { messages = await this.listAllPrivate({ includeDeleted: true }); } catch (_) { return out; } - for (const m of (messages || [])) { - const c = m && m.value && m.value.content; - if (!c || !c.ref) continue; - if (m.value.author !== userId) continue; - out.add(`${String(c.subject || '')}|${String(c.ref)}`); - } - return out; - }, - - async sendFileShare(recipients = [], subject = '', fileShare = null, crypter = false) { - const ssbClient = await openSsb(); - const recps = uniqueRecps([userId, ...recipients]); - if (!fileShare || typeof fileShare !== 'object') throw new Error('Invalid file share'); - const content = { - type: 'post', - from: userId, - to: recps, - subject, - text: '', - sentAt: new Date().toISOString(), - private: true, - fileShare, ...(crypter ? { crypter: true } : {}) }; const publishAsync = util.promisify(ssbClient.private.publish); @@ -116,8 +83,7 @@ module.exports = ({ cooler }) => { return publishAsync(tombstone, tombstoneRecps); }, - async listAllPrivate(opts = {}) { - const includeDeleted = !!(opts && opts.includeDeleted); + async listAllPrivate() { const ssbClient = await openSsb(); const raw = await new Promise((resolve, reject) => { pull( @@ -175,7 +141,7 @@ module.exports = ({ cooler }) => { } } } - return posts.filter(m => m && m.key && (includeDeleted || !tombed.has(m.key))); + return posts.filter(m => m && m.key && !tombed.has(m.key)); } }; }; diff --git a/src/models/polls_model.js b/src/models/polls_model.js deleted file mode 100644 index 31db862..0000000 --- a/src/models/polls_model.js +++ /dev/null @@ -1,443 +0,0 @@ -const pull = require('../server/node_modules/pull-stream'); -const { getConfig } = require('../configs/config-manager.js'); -const { buildValidatedTombstoneSet } = require('./tombstone_validator'); - -const logLimit = getConfig().ssbLogStream?.limit || 1000; - -const POLL_TYPE = 'poll'; -const VOTE_TYPE = 'pollVote'; -const OPINION_TYPE = 'pollOpinion'; - -const { MAX_OPTIONS, MIN_OPTIONS, MAX_OPTION_LENGTH } = require('./polls_model_limits'); - -const clean = (v) => String(v == null ? '' : v).trim(); - -const normalizeOptions = (raw) => { - const list = Array.isArray(raw) ? raw : String(raw || '').split('\n'); - const out = []; - const seen = new Set(); - for (const entry of list) { - const value = clean(entry).slice(0, MAX_OPTION_LENGTH); - if (!value) continue; - const key = value.toLowerCase(); - if (seen.has(key)) continue; - seen.add(key); - out.push(value); - if (out.length >= MAX_OPTIONS) break; - } - return out; -}; - -const normalizeTags = (raw) => { - const list = Array.isArray(raw) ? raw : String(raw || '').split(','); - return list.map(t => clean(t)).filter(Boolean).slice(0, 20); -}; - -module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = null }) => { - let ssb; - const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; - - const getAllMessages = async (ssbClient) => - new Promise((resolve, reject) => { - pull( - ssbClient.createLogStream({ limit: logLimit }), - pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))) - ); - }); - - const decryptField = (value, keys) => { - if (!tribeCrypto || typeof value !== 'string' || !value) return null; - for (const k of keys) { - try { - const out = tribeCrypto.decryptWithKey(value, k); - if (typeof out === 'string' && out) return out; - } catch (_) {} - } - return null; - }; - - const scopeKey = async (scope) => { - if (!scope || !chatsModel) return null; - if (!scope.chatId && !scope.tribeId) return null; - try { return await chatsModel.encryptionKeyFor(scope.chatId || null, scope.tribeId || null); } catch (_) { return null; } - }; - - const buildIndex = (messages) => { - const tomb = buildValidatedTombstoneSet(messages); - const nodes = new Map(); - const replaced = new Map(); - const votesByRoot = new Map(); - const opinionsByRoot = new Map(); - const closedRoots = new Set(); - const commentsByRoot = new Map(); - const authorByKey = new Map(); - - for (const m of messages) { - const v = m && m.value; - const c = v && v.content; - if (!c || typeof c !== 'object') continue; - authorByKey.set(m.key, v.author); - - if (c.type === POLL_TYPE) { - nodes.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); - if (typeof c.replaces === 'string') replaced.set(c.replaces, m.key); - continue; - } - if (c.type === VOTE_TYPE && typeof c.target === 'string') { - const entry = votesByRoot.get(c.target) || new Map(); - const prev = entry.get(v.author); - const ts = v.timestamp || m.timestamp || 0; - if (!prev || ts >= prev.ts) { - entry.set(v.author, { - ts, - choices: Array.isArray(c.choices) ? c.choices : [], - encryptedChoices: typeof c.encryptedChoices === 'string' ? c.encryptedChoices : null - }); - } - votesByRoot.set(c.target, entry); - continue; - } - if (c.type === OPINION_TYPE && typeof c.target === 'string') { - const entry = opinionsByRoot.get(c.target) || { counts: {}, voters: [] }; - if (!entry.voters.includes(v.author)) { - entry.voters.push(v.author); - entry.counts[c.category] = (entry.counts[c.category] || 0) + 1; - } - opinionsByRoot.set(c.target, entry); - continue; - } - if (c.type === 'pollClose' && typeof c.target === 'string') { - closedRoots.add(`${v.author}|${c.target}`); - continue; - } - if (c.type === 'post' && typeof c.root === 'string') { - commentsByRoot.set(c.root, (commentsByRoot.get(c.root) || 0) + 1); - } - } - - const rootOf = (key) => { - let cur = key; - const seen = new Set(); - while (nodes.has(cur) && nodes.get(cur).c.replaces && !seen.has(cur)) { - seen.add(cur); - const next = nodes.get(cur).c.replaces; - if (!nodes.has(next)) break; - cur = next; - } - return cur; - }; - - const tipOf = (root) => { - let cur = root; - const seen = new Set(); - while (replaced.has(cur) && !seen.has(cur)) { - seen.add(cur); - const next = replaced.get(cur); - const node = nodes.get(next); - if (!node || node.author !== nodes.get(cur).author) break; - cur = next; - } - return cur; - }; - - return { tomb, nodes, replaced, votesByRoot, opinionsByRoot, closedRoots, commentsByRoot, rootOf, tipOf }; - }; - - const isExpired = (c) => { - if (!c.deadline) return false; - const t = Date.parse(c.deadline); - return Number.isFinite(t) && t <= Date.now(); - }; - - const buildPoll = (idx, rootId, viewerId, keys = []) => { - const tipId = idx.tipOf(rootId); - const node = idx.nodes.get(tipId); - if (!node) return null; - const c = node.c || {}; - let question = clean(c.question); - let rawOptions = c.options; - let undecryptable = false; - if (c.encryptedQuestion) { - const q = decryptField(c.encryptedQuestion, keys); - const o = decryptField(c.encryptedOptions, keys); - if (q === null) undecryptable = true; - question = q || ''; - try { rawOptions = o ? JSON.parse(o) : []; } catch (_) { rawOptions = []; } - } - const options = normalizeOptions(rawOptions); - const voteMap = idx.votesByRoot.get(rootId) || new Map(); - - const counts = {}; - for (const o of options) counts[o] = 0; - let totalVoters = 0; - const voters = []; - let myChoices = []; - for (const [author, entry] of voteMap.entries()) { - let choices = Array.isArray(entry.choices) ? entry.choices : []; - if (entry.encryptedChoices) { - const plain = decryptField(entry.encryptedChoices, keys); - try { choices = plain ? JSON.parse(plain) : []; } catch (_) { choices = []; } - } - const picked = (Array.isArray(choices) ? choices : []).filter(o => options.includes(o)); - if (!picked.length) continue; - totalVoters += 1; - voters.push(author); - for (const o of picked) counts[o] += 1; - if (viewerId && author === viewerId) myChoices = picked; - } - - const closed = idx.closedRoots.has(`${node.author}|${rootId}`) || isExpired(c); - const op = idx.opinionsByRoot.get(rootId) || { counts: {}, voters: [] }; - - return { - id: rootId, - rootId, - tipId, - author: node.author, - question, - options, - encrypted: !!c.encryptedQuestion, - undecryptable, - chatId: c.chatId || null, - anonymous: c.anonymous === true, - multiple: c.multiple === true, - deadline: c.deadline || '', - tags: normalizeTags(c.tags), - tribeId: c.tribeId || null, - houseKey: c.houseKey || null, - createdAt: c.createdAt || new Date(node.ts).toISOString(), - updatedAt: c.updatedAt || null, - status: closed ? 'CLOSED' : 'OPEN', - counts, - totalVoters, - voters: c.anonymous === true ? [] : voters, - votersHidden: c.anonymous === true, - myChoices, - hasVoted: myChoices.length > 0, - commentCount: idx.commentsByRoot.get(rootId) || 0, - opinions: op.counts, - opinions_inhabitants: op.voters - }; - }; - - const collect = async (viewerId) => { - const ssbClient = await openSsb(); - const messages = await getAllMessages(ssbClient); - const idx = buildIndex(messages); - const roots = new Set(); - for (const key of idx.nodes.keys()) roots.add(idx.rootOf(key)); - - const keyCache = new Map(); - const keysFor = async (c) => { - if (!c || (!c.chatId && !c.tribeId)) return []; - const cacheKey = `${c.chatId || ''}|${c.tribeId || ''}`; - if (keyCache.has(cacheKey)) return keyCache.get(cacheKey); - const k = await scopeKey({ chatId: c.chatId || null, tribeId: c.tribeId || null }); - const keys = k ? [k] : []; - keyCache.set(cacheKey, keys); - return keys; - }; - - const list = []; - for (const rootId of roots) { - const tipId = idx.tipOf(rootId); - if (idx.tomb.has(tipId) || idx.tomb.has(rootId)) continue; - const node = idx.nodes.get(tipId); - const keys = await keysFor(node && node.c); - const poll = buildPoll(idx, rootId, viewerId || ssbClient.id, keys); - if (!poll) continue; - if (poll.undecryptable) { list.push(poll); continue; } - if (!poll.question || poll.options.length < MIN_OPTIONS) continue; - list.push(poll); - } - return { list, idx, viewerId: viewerId || ssbClient.id }; - }; - - return { - POLL_TYPE, - VOTE_TYPE, - MAX_OPTIONS, - MIN_OPTIONS, - - async listAll(filter = 'ALL', opts = {}) { - const { list, viewerId } = await collect(); - const f = String(filter || 'ALL').toUpperCase(); - const scopeChat = opts.chatId || null; - const scopeTribe = opts.tribeId || null; - const scopeHouse = opts.houseKey || null; - let out = list.filter(p => - scopeChat ? p.chatId === scopeChat - : scopeTribe ? (p.tribeId === scopeTribe && !p.chatId) - : scopeHouse ? p.houseKey === scopeHouse - : (!p.tribeId && !p.chatId && !p.houseKey)); - - if (f === 'MINE') out = out.filter(p => String(p.author) === String(viewerId)); - else if (f === 'OPEN') out = out.filter(p => p.status === 'OPEN'); - else if (f === 'CLOSED') out = out.filter(p => p.status === 'CLOSED'); - else if (f === 'VOTED') out = out.filter(p => p.hasVoted); - else if (f === 'FAVORITES') { - const fav = new Set((opts.favorites || []).map(String)); - out = out.filter(p => fav.has(String(p.id))); - } - - const q = clean(opts.q).toLowerCase(); - if (q) out = out.filter(p => - p.question.toLowerCase().includes(q) || - p.options.some(o => o.toLowerCase().includes(q)) || - p.tags.some(t => t.toLowerCase().includes(q))); - - if (f === 'TOP') out.sort((a, b) => b.totalVoters - a.totalVoters || new Date(b.createdAt) - new Date(a.createdAt)); - else out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - - return out; - }, - - async getPollById(id, viewerId = null) { - const { list, idx } = await collect(viewerId); - const rootId = idx.rootOf(id); - const poll = list.find(p => p.id === rootId || p.id === id || p.tipId === id); - if (!poll) throw new Error('Poll not found'); - return poll; - }, - - async resolveRootId(id) { - const { idx } = await collect(); - return idx.rootOf(id); - }, - - async createPoll(data = {}) { - if (isPublic) throw new Error('Not available in public mode'); - const question = clean(data.question); - if (!question) throw new Error('Question is required'); - const options = normalizeOptions(data.options); - if (options.length < MIN_OPTIONS) throw new Error(`A poll needs at least ${MIN_OPTIONS} options`); - const deadline = clean(data.deadline); - if (deadline) { - const t = Date.parse(deadline); - if (!Number.isFinite(t)) throw new Error('Invalid deadline'); - if (t <= Date.now()) throw new Error('Deadline must be in the future'); - } - const ssbClient = await openSsb(); - const content = { - type: POLL_TYPE, - anonymous: data.anonymous === true || data.anonymous === 'true' || data.anonymous === '1', - multiple: data.multiple === true || data.multiple === 'true' || data.multiple === '1', - deadline, - tags: normalizeTags(data.tags), - createdAt: new Date().toISOString(), - ...(data.tribeId ? { tribeId: String(data.tribeId) } : {}), - ...(data.chatId ? { chatId: String(data.chatId) } : {}), - ...(data.houseKey ? { houseKey: String(data.houseKey) } : {}) - }; - const key = await scopeKey({ chatId: data.chatId, tribeId: data.tribeId }); - if (key && tribeCrypto) { - content.encryptedQuestion = tribeCrypto.encryptWithKey(question, key); - content.encryptedOptions = tribeCrypto.encryptWithKey(JSON.stringify(options), key); - } else { - content.question = question; - content.options = options; - } - return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); - }, - - async updatePoll(id, data = {}) { - const ssbClient = await openSsb(); - const userId = ssbClient.id; - const poll = await this.getPollById(id, userId); - if (poll.author !== userId) throw new Error('Not the author'); - if (poll.status === 'CLOSED') throw new Error('Cannot edit a closed poll'); - if (poll.totalVoters > 0) throw new Error('Cannot edit a poll that already has votes'); - - const options = data.options !== undefined ? normalizeOptions(data.options) : poll.options; - if (options.length < MIN_OPTIONS) throw new Error(`A poll needs at least ${MIN_OPTIONS} options`); - const deadline = data.deadline !== undefined ? clean(data.deadline) : poll.deadline; - if (deadline) { - const t = Date.parse(deadline); - if (!Number.isFinite(t)) throw new Error('Invalid deadline'); - } - const question = data.question !== undefined ? clean(data.question) : poll.question; - const content = { - type: POLL_TYPE, - replaces: poll.tipId, - anonymous: data.anonymous !== undefined - ? (data.anonymous === true || data.anonymous === 'true' || data.anonymous === '1') - : poll.anonymous, - multiple: data.multiple !== undefined - ? (data.multiple === true || data.multiple === 'true' || data.multiple === '1') - : poll.multiple, - deadline, - tags: data.tags !== undefined ? normalizeTags(data.tags) : poll.tags, - createdAt: poll.createdAt, - updatedAt: new Date().toISOString(), - ...(poll.tribeId ? { tribeId: poll.tribeId } : {}), - ...(poll.chatId ? { chatId: poll.chatId } : {}), - ...(poll.houseKey ? { houseKey: poll.houseKey } : {}) - }; - const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId }); - if (key && tribeCrypto) { - content.encryptedQuestion = tribeCrypto.encryptWithKey(question, key); - content.encryptedOptions = tribeCrypto.encryptWithKey(JSON.stringify(options), key); - } else { - content.question = question; - content.options = options; - } - return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); - }, - - async vote(id, choicesRaw) { - const ssbClient = await openSsb(); - const userId = ssbClient.id; - const poll = await this.getPollById(id, userId); - if (poll.status === 'CLOSED') throw new Error('This poll is closed'); - - const wanted = (Array.isArray(choicesRaw) ? choicesRaw : [choicesRaw]) - .map(c => clean(c)) - .filter(c => poll.options.includes(c)); - const unique = [...new Set(wanted)]; - if (!unique.length) throw new Error('Pick at least one option'); - if (!poll.multiple && unique.length > 1) throw new Error('This poll only accepts one option'); - - const content = { - type: VOTE_TYPE, - target: poll.rootId, - createdAt: new Date().toISOString(), - ...(poll.chatId ? { chatId: poll.chatId } : {}) - }; - const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId }); - if (key && tribeCrypto) content.encryptedChoices = tribeCrypto.encryptWithKey(JSON.stringify(unique), key); - else content.choices = unique; - return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); - }, - - async closePoll(id) { - const ssbClient = await openSsb(); - const userId = ssbClient.id; - const poll = await this.getPollById(id, userId); - if (poll.author !== userId) throw new Error('Not the author'); - if (poll.status === 'CLOSED') return { status: 'already_closed' }; - const content = { type: 'pollClose', target: poll.rootId, createdAt: new Date().toISOString() }; - await new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); - return { status: 'ok' }; - }, - - async deletePoll(id) { - const ssbClient = await openSsb(); - const userId = ssbClient.id; - const poll = await this.getPollById(id, userId); - if (poll.author !== userId) throw new Error('Not the author'); - const content = { type: 'tombstone', target: poll.tipId, deletedAt: new Date().toISOString(), author: userId }; - return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); - }, - - async createOpinion(id, category) { - const categories = require('../backend/opinion_categories'); - if (!categories.includes(category)) throw new Error('Invalid opinion category'); - const ssbClient = await openSsb(); - const userId = ssbClient.id; - const poll = await this.getPollById(id, userId); - if (poll.opinions_inhabitants.includes(userId)) throw new Error('Already opined'); - const content = { type: OPINION_TYPE, target: poll.rootId, category, createdAt: new Date().toISOString() }; - return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result))); - } - }; -}; diff --git a/src/models/polls_model_limits.js b/src/models/polls_model_limits.js deleted file mode 100644 index ba1e774..0000000 --- a/src/models/polls_model_limits.js +++ /dev/null @@ -1,5 +0,0 @@ -const MAX_OPTIONS = 10; -const MIN_OPTIONS = 2; -const MAX_OPTION_LENGTH = 120; - -module.exports = { MAX_OPTIONS, MIN_OPTIONS, MAX_OPTION_LENGTH }; diff --git a/src/models/projects_model.js b/src/models/projects_model.js index f5b8446..05ecf90 100644 --- a/src/models/projects_model.js +++ b/src/models/projects_model.js @@ -239,7 +239,6 @@ module.exports = ({ cooler }) => { if (goal < 0) goal = 0 const deadlineISO = data.deadline ? new Date(data.deadline).toISOString() : null - if (deadlineISO && new Date(deadlineISO).getTime() < Date.now()) throw new Error("The deadline cannot be in the past") const content = { type: TYPE, @@ -324,11 +323,7 @@ module.exports = ({ cooler }) => { } let deadline = patch.deadline === undefined ? current.deadline : patch.deadline - if (deadline != null && deadline !== "") { - deadline = new Date(deadline).toISOString() - const changed = !current.deadline || new Date(current.deadline).getTime() !== new Date(deadline).getTime() - if (changed && new Date(deadline).getTime() < Date.now()) throw new Error("The deadline cannot be in the past") - } + if (deadline != null && deadline !== "") deadline = new Date(deadline).toISOString() else if (deadline === "") deadline = null const updated = { @@ -551,7 +546,7 @@ module.exports = ({ cooler }) => { return publishReplace(ssbClient, project.id, patch) }, - async listProjects(filter, opts = {}) { + async listProjects(filter) { const ssbClient = await openSsb() const currentUserId = ssbClient.id const idx = buildProjectIndex(await getAllMsgs(ssbClient)) @@ -585,15 +580,6 @@ module.exports = ({ cooler }) => { else if (F === "RECENT") list = list.filter((p) => p && moment(p.createdAt).isAfter(moment().subtract(24, "hours"))) else if (F === "FOLLOWING") list = list.filter((p) => Array.isArray(p.followers) && p.followers.includes(currentUserId)) - const q = norm(opts.q) - if (q) { - list = list.filter((p) => - norm(p.title).includes(q) || - norm(p.description).includes(q) || - (Array.isArray(p.tags) && p.tags.some(t => norm(t).includes(q))) - ) - } - if (F === "TOP") { list.sort((a, b) => (parseFloat(b.pledged || 0) / (parseFloat(b.goal || 1))) - (parseFloat(a.pledged || 0) / (parseFloat(a.goal || 1)))) } else { diff --git a/src/models/recurrence.js b/src/models/recurrence.js deleted file mode 100644 index 0c696aa..0000000 --- a/src/models/recurrence.js +++ /dev/null @@ -1,40 +0,0 @@ -const truthy = (v) => v === true || v === 1 || v === '1' || v === 'on' || v === 'true'; - -const hasAnyInterval = (weekly, monthly, yearly) => truthy(weekly) || truthy(monthly) || truthy(yearly); - -const expandRecurrence = (firstDate, until, weekly, monthly, yearly) => { - const start = new Date(firstDate); - if (isNaN(start.getTime())) return []; - const out = [start]; - if (!until || !hasAnyInterval(weekly, monthly, yearly)) return out; - const end = new Date(until).getTime(); - if (!Number.isFinite(end)) return out; - const seen = new Set([start.getTime()]); - const walk = (mutate) => { - const n = new Date(start); - mutate(n); - let guard = 0; - while (n.getTime() <= end && guard++ < 5000) { - const t = n.getTime(); - if (!seen.has(t)) { seen.add(t); out.push(new Date(n)); } - mutate(n); - } - }; - if (truthy(weekly)) walk((d) => d.setUTCDate(d.getUTCDate() + 7)); - if (truthy(monthly)) walk((d) => d.setUTCMonth(d.getUTCMonth() + 1)); - if (truthy(yearly)) walk((d) => d.setUTCFullYear(d.getUTCFullYear() + 1)); - return out.sort((a, b) => a.getTime() - b.getTime()); -}; - -const nextOccurrence = (firstDate, until, weekly, monthly, yearly, from = Date.now()) => { - const list = expandRecurrence(firstDate, until, weekly, monthly, yearly); - for (const d of list) if (d.getTime() >= from) return d; - return list.length ? list[list.length - 1] : null; -}; - -const upcomingOccurrences = (firstDate, until, weekly, monthly, yearly, limit = 12, from = Date.now()) => - expandRecurrence(firstDate, until, weekly, monthly, yearly) - .filter(d => d.getTime() >= from) - .slice(0, limit); - -module.exports = { truthy, hasAnyInterval, expandRecurrence, nextOccurrence, upcomingOccurrences }; diff --git a/src/models/reports_model.js b/src/models/reports_model.js index 09d6ea9..8bb1b29 100644 --- a/src/models/reports_model.js +++ b/src/models/reports_model.js @@ -1,5 +1,4 @@ const pull = require('../server/node_modules/pull-stream'); -const { normalizeImages, normalizeVideo } = require('./media_gallery'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeBy, norm } = require('../backend/dedupe'); @@ -40,7 +39,7 @@ const normalizeTemplate = (category, tpl) => { } if (cat === 'CONTENT') { - return pick(['contentLocation', 'whyInappropriate', 'evidenceLinks']); + return pick(['contentLocation', 'whyInappropriate', 'requestedAction', 'evidenceLinks']); } return {}; @@ -165,8 +164,6 @@ module.exports = ({ cooler }) => { ...c, category: cat, status: normalizeStatus(c.status || 'OPEN'), - images: normalizeImages(c.images && c.images.length ? c.images : c.image), - video: normalizeVideo(c.video), severity: normalizeSeverity(c.severity) || 'low', confirmations, tags: ensureArray(c.tags), @@ -177,13 +174,14 @@ module.exports = ({ cooler }) => { }; return { - async createReport(title, description, category, image, tagsRaw = [], severity = 'low', template = {}, media = {}) { + async createReport(title, description, category, image, tagsRaw = [], severity = 'low', template = {}) { const ssb = await openSsb(); const userId = ssb.id; - const images = normalizeImages((media && media.images && media.images.length) ? media.images : image); - const blobId = images[0] || null; - const clip = normalizeVideo(media && media.video); + let blobId = null; + if (image) { + blobId = String(image).trim() || null; + } const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) @@ -198,8 +196,6 @@ module.exports = ({ cooler }) => { createdAt: new Date().toISOString(), author: userId, image: blobId, - images, - video: clip, tags, confirmations: [], severity: normalizeSeverity(severity) || 'low', @@ -231,15 +227,10 @@ module.exports = ({ cooler }) => { ? String(updatedContent.tags || '').split(',').map(t => t.trim()).filter(Boolean) : ensureArray(report.content.tags); - const nextImages = normalizeImages( - Object.prototype.hasOwnProperty.call(updatedContent, 'images') - ? updatedContent.images - : (report.content.images && report.content.images.length ? report.content.images : (updatedContent.image || report.content.image)) - ); - const blobId = nextImages[0] || null; - const nextVideo = Object.prototype.hasOwnProperty.call(updatedContent, 'video') - ? normalizeVideo(updatedContent.video) - : normalizeVideo(report.content.video); + let blobId = report.content.image || null; + if (updatedContent.image) { + blobId = String(updatedContent.image).trim() || null; + } const nextStatus = Object.prototype.hasOwnProperty.call(updatedContent, 'status') ? normalizeStatus(updatedContent.status) @@ -267,8 +258,6 @@ module.exports = ({ cooler }) => { type: 'report', replaces: tipId, image: blobId, - images: nextImages, - video: nextVideo, tags, confirmations, opinions: agg.opinions, diff --git a/src/models/search_model.js b/src/models/search_model.js index 1f55297..650b736 100644 --- a/src/models/search_model.js +++ b/src/models/search_model.js @@ -40,7 +40,7 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { 'post', 'about', 'curriculum', 'tribe', 'transfer', 'feed', 'votes', 'report', 'task', 'event', 'bookmark', 'document', 'image', 'audio', 'video', 'torrent', 'market', 'bankWallet', 'bankClaim', - 'project', 'job', 'housing', 'industry', 'industryBlueprint', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad', 'poll' + 'project', 'job', 'forum', 'vote', 'contact', 'pub', 'map', 'shop', 'shopProduct', 'chat', 'pad' ]; const getRelevantFields = (type, content) => { @@ -60,7 +60,7 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { case 'audio': return [content?.url, content?.mimeType, content?.title, content?.description, ...(content?.tags || [])]; case 'image': - return [content?.url, content?.title, content?.description, ...(content?.tags || [])]; + return [content?.url, content?.title, content?.description, ...(content?.tags || []), content?.meme]; case 'video': return [content?.url, content?.mimeType, content?.title, content?.description, ...(content?.tags || [])]; case 'document': @@ -85,18 +85,8 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { return [content?.amount, content?.epochId, content?.allocationId, content?.txid]; case 'project': return [content?.title, content?.status, content?.progress, content?.goal, content?.pledged, content?.deadline, (content?.followers || []).length, (content?.backers || []).length, (content?.milestones || []).length, content?.bounty, content?.bountyAmount, content?.bounty_currency, content?.activity?.kind, content?.activityActor]; - case 'poll': - return content?.encryptedQuestion - ? [] - : [content?.question, ...(content?.options || []), ...(content?.tags || [])]; - case 'housing': - return [content?.title, content?.housing_type, content?.property_type, content?.place, content?.price, content?.rooms, content?.size, content?.capacity, content?.status]; case 'job': return [content?.title, content?.job_type, ...(content?.tasks || []), content?.location, content?.vacants, content?.salary, content?.status, (content?.subscribers || []).length]; - case 'industry': - return [content?.name, content?.sector, content?.description, content?.membershipPolicy, content?.status, content?.license, ...(content?.tags || [])]; - case 'industryBlueprint': - return [content?.name, content?.description, content?.sector, content?.outItem, content?.outKind, content?.license, ...(content?.skills || []), ...((content?.materials || []).map(m => m && m.item))]; case 'forum': return [content?.root, content?.category, content?.title, content?.text, content?.key]; case 'vote': @@ -238,17 +228,6 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { ].join('|'); } - if (t === 'housing') { - return [ - 'housing', - author, - norm(c.title), - norm(c.place), - norm(c.price), - norm(c.housing_type) - ].join('|'); - } - if (t === 'job') { return [ 'job', @@ -260,14 +239,6 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { ].join('|'); } - if (t === 'industry') { - return ['industry', author, norm(c.name), norm(c.sector)].join('|'); - } - - if (t === 'industryBlueprint') { - return ['industryBlueprint', author, norm(c.facility), norm(c.name)].join('|'); - } - if (t === 'forum') { return `forum:${c.key || c.root || `${author}|${norm(c.title)}` || msg.key}`; } diff --git a/src/models/stats_model.js b/src/models/stats_model.js index ddbb322..bc14f72 100644 --- a/src/models/stats_model.js +++ b/src/models/stats_model.js @@ -22,7 +22,7 @@ function readAddrMap() { const listPubsFromEbt = () => { try { - const ebtDir = path.join(require('../server/ssb_config').path, 'ebt'); + const ebtDir = path.join(os.homedir(), '.ssb', 'ebt'); const files = fs.readdirSync(ebtDir); return files.filter(f => f.endsWith('.ed25519')); } catch { @@ -58,7 +58,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { }; const types = [ - 'bookmark','event','task','votes','report','feed','project','industry','industryBlueprint', + 'bookmark','event','task','votes','report','feed','project', 'image','torrent','audio','video','document','transfer','post','tribe', 'market','forum','job','aiExchange','map','shop','shopProduct','chat','chatMessage', 'pad','padEntry','gameScore','calendar','calendarDate','calendarNote','log', @@ -281,14 +281,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { else if (t === 'chat') score += 1; else if (t === 'gamescore') score += 2; else if (t === 'pixelia') score += 2; - else if (rawType === 'industry') score += 8; - else if (rawType === 'industryblueprint') score += 6; - else if (rawType === 'industrybuild') score += 8; - else if (rawType === 'industrycontribution') score += 4; - else if (rawType === 'industryallocation') score += 10; - else if (rawType === 'industryvote') score += 2; - else if (rawType === 'industrymember') score += 2; - else if (rawType === 'industryopinion') score += 1; } return Math.max(0, Math.round(score)); }; @@ -428,12 +420,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const inhabitants = new Set(allMsgs.map(m => m.value.author)).size; - const secretStat = fs.statSync((require('../server/ssb_config').path + '/secret')); + const secretStat = fs.statSync(`${os.homedir()}/.ssb/secret`); const createdAt = secretStat.birthtime.toLocaleString(); - const folderSize = getFolderSize((require('../server/ssb_config').path + '')); - const flumeSize = getFolderSize((require('../server/ssb_config').path + '/flume')); - const blobsSize = getFolderSize((require('../server/ssb_config').path + '/blobs')); + const folderSize = getFolderSize(`${os.homedir()}/.ssb`); + const flumeSize = getFolderSize(`${os.homedir()}/.ssb/flume`); + const blobsSize = getFolderSize(`${os.homedir()}/.ssb/blobs`); const allTs = scopedMsgs.map(m => m.value.timestamp || 0).filter(Boolean); const lastTs = allTs.length ? Math.max(...allTs) : 0; diff --git a/src/models/tags_model.js b/src/models/tags_model.js index 3b1b2ac..f074453 100644 --- a/src/models/tags_model.js +++ b/src/models/tags_model.js @@ -13,10 +13,7 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { const norm = (v) => String(v == null ? '' : v).trim().toLowerCase(); const normalizeTag = (tag) => String(tag == null ? '' : tag).trim().replace(/^#/, ''); - const tagKey = (tag) => { - const k = normalizeTag(tag).toLowerCase(); - return k.includes(':') ? '' : k; - }; + const tagKey = (tag) => normalizeTag(tag).toLowerCase(); const getDedupeKey = (msg) => { const c = msg?.value?.content || {}; @@ -81,10 +78,6 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { return ['job', author, norm(c.title), norm(c.location), norm(c.salary), norm(c.job_type)].join('|'); } - if (t === 'industry') { - return ['industry', author, norm(c.name), norm(c.sector)].join('|'); - } - if (t === 'forum') { return `forum:${c.key || c.root || `${author}|${norm(c.title)}` || msg.key}`; } @@ -105,7 +98,7 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { }; return { - async listTags(filter = 'all', search = '') { + async listTags(filter = 'all') { const ssbClient = await openSsb(); const messages = await new Promise((resolve, reject) => { @@ -161,35 +154,21 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { if (c.type === 'task' && String(c.isPublic).toUpperCase() === 'PRIVATE') return false; if ((c.type === 'chat' || c.type === 'pad' || c.type === 'map' || c.type === 'calendar') && c.status === 'INVITE-ONLY' && c.author !== viewerId && !(Array.isArray(c.members) && c.members.includes(viewerId))) return false; if (c.type === 'shop' && c.visibility === 'CLOSED') return false; - if (c.type === 'housing' && String(c.visibility || '').toUpperCase() === 'HIDDEN' && c.author !== viewerId) return false; - if (c.type === 'poll' && c.chatId) return false; return true; }); filtered = dedupeKeepLatest(filtered); const counts = new Map(); - const DAY = 24 * 60 * 60 * 1000; - - const bump = (k, display, { mine, ts }) => { - const prev = counts.get(k); - if (!prev) counts.set(k, { name: display, count: 1, mine: mine ? 1 : 0, lastTs: ts || 0 }); - else counts.set(k, { - name: prev.name || display, - count: prev.count + 1, - mine: prev.mine + (mine ? 1 : 0), - lastTs: Math.max(prev.lastTs || 0, ts || 0) - }); - }; for (const record of filtered) { const tagsArr = record?.value?.content?.tags || []; const uniqueTags = new Set(tagsArr.map(tagKey).filter(Boolean)); - const mine = String(record?.value?.author || '') === String(viewerId); - const ts = Number(record?.value?.timestamp || record?.timestamp || 0); for (const k of uniqueTags) { const display = normalizeTag(tagsArr.find(t => tagKey(t) === k) || k) || k; - bump(k, display, { mine, ts }); + const prev = counts.get(k); + if (!prev) counts.set(k, { name: display, count: 1 }); + else counts.set(k, { name: prev.name || display, count: prev.count + 1 }); } } @@ -203,22 +182,16 @@ module.exports = ({ cooler, padsModel, tribesModel }) => { const uniquePadTags = new Set(pad.tags.map(tagKey).filter(Boolean)); for (const k of uniquePadTags) { const display = normalizeTag(pad.tags.find(t => tagKey(t) === k) || k) || k; - bump(k, display, { mine: String(pad.author || '') === String(viewerId), ts: Number(pad.createdAt ? new Date(pad.createdAt).getTime() : 0) }); + const prev = counts.get(k); + if (!prev) counts.set(k, { name: display, count: 1 }); + else counts.set(k, { name: prev.name || display, count: prev.count + 1 }); } } } let tags = Array.from(counts.values()); - const q = String(search || '').trim().toLowerCase().replace(/^#/, ''); - if (q) tags = tags.filter(t => String(t.name || '').toLowerCase().includes(q)); - - if (filter === 'mine') { - tags = tags.filter(t => (t.mine || 0) > 0).sort((a, b) => b.mine - a.mine || a.name.localeCompare(b.name)); - } else if (filter === 'recent') { - const since = Date.now() - 7 * DAY; - tags = tags.filter(t => (t.lastTs || 0) >= since).sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0)); - } else if (filter === 'top') { + if (filter === 'top') { tags.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); } else if (filter === 'cloud') { const max = Math.max(...tags.map(t => t.count), 1); diff --git a/src/models/tasks_model.js b/src/models/tasks_model.js index 1fd0e29..a750a3b 100644 --- a/src/models/tasks_model.js +++ b/src/models/tasks_model.js @@ -1,6 +1,5 @@ const pull = require('../server/node_modules/pull-stream'); const moment = require('../server/node_modules/moment'); -const { normalizeImages, normalizeVideo } = require('./media_gallery'); const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeBy, norm } = require('../backend/dedupe'); @@ -132,8 +131,6 @@ module.exports = ({ cooler, pmModel }) => { id: node.key, rootId, ...c, - images: normalizeImages(c.images), - video: normalizeVideo(c.video), assignees, opinions, opinions_inhabitants: voters, @@ -142,7 +139,7 @@ module.exports = ({ cooler, pmModel }) => { }; return { - async createTask(title, description, startTime, endTime, priority, location = '', tagsRaw = [], isPublic, media = {}) { + async createTask(title, description, startTime, endTime, priority, location = '', tagsRaw = [], isPublic) { const ssb = await openSsb(); const userId = ssb.id; @@ -169,8 +166,6 @@ module.exports = ({ cooler, pmModel }) => { location, tags, isPublic: visibility, - images: normalizeImages(media && media.images), - video: normalizeVideo(media && media.video), assignees: [userId], createdAt: new Date().toISOString(), status: 'OPEN', @@ -258,8 +253,6 @@ module.exports = ({ cooler, pmModel }) => { tags: newTags, isPublic: newVisibility, status: newStatus, - images: normalizeImages(updatedData.images !== undefined ? updatedData.images : c.images), - video: updatedData.video !== undefined ? normalizeVideo(updatedData.video) : normalizeVideo(c.video), assignees: nextAssignees, updatedAt: new Date().toISOString(), replaces: taskId diff --git a/src/models/trending_model.js b/src/models/trending_model.js index 2c940ce..0528d61 100644 --- a/src/models/trending_model.js +++ b/src/models/trending_model.js @@ -19,9 +19,8 @@ module.exports = ({ cooler }) => { }; const types = [ - 'bookmark', 'votes', 'poll', 'feed', - 'image', 'audio', 'video', 'document', 'torrent', 'transfer', - 'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market' + 'bookmark', 'votes', 'feed', + 'image', 'audio', 'video', 'document', 'torrent', 'transfer' ]; const categories = opinionCategories; @@ -40,8 +39,6 @@ module.exports = ({ cooler }) => { const tombstoned = buildValidatedTombstoneSet(messages); const replaces = new Map(); const itemsById = new Map(); - const authorOf = new Map(); - const opinionMsgs = []; for (const m of messages) { const k = m.key; @@ -54,13 +51,6 @@ module.exports = ({ cooler }) => { continue; } - if (k) authorOf.set(k, m.value?.author); - - if (typeof c.type === 'string' && c.type.endsWith('Opinion') && c.target) { - opinionMsgs.push({ target: c.target, author: m.value?.author, category: c.category }); - continue; - } - if (c.opinions && !tombstoned.has(k) && !['task', 'event', 'report'].includes(c.type)) { if (c.replaces) replaces.set(c.replaces, k); itemsById.set(k, m); @@ -72,47 +62,17 @@ module.exports = ({ cooler }) => { } } - for (const [replacedId, newId] of Array.from(replaces.entries())) { - const oldAuthor = authorOf.get(replacedId); + for (const [replacedId, newId] of replaces.entries()) { + const oldMsg = itemsById.get(replacedId); const newMsg = itemsById.get(newId); - if (oldAuthor === undefined) { replaces.delete(replacedId); continue; } - if (!newMsg || String(newMsg.value?.author) !== String(oldAuthor)) { - replaces.delete(replacedId); + if (!oldMsg) continue; + if (!newMsg || String(newMsg.value?.author) !== String(oldMsg.value?.author)) { itemsById.delete(newId); continue; } itemsById.delete(replacedId); } - const tipToChain = new Map(); - for (const [oldId, newId] of replaces.entries()) { - let tip = newId; let g = 0; - while (replaces.has(tip) && g++ < 100000) tip = replaces.get(tip); - if (!tipToChain.has(tip)) tipToChain.set(tip, new Set([tip])); - tipToChain.get(tip).add(oldId); tipToChain.get(tip).add(newId); - } - const idToTip = new Map(); - for (const [tip, chain] of tipToChain.entries()) for (const id of chain) idToTip.set(id, tip); - const opinionByTip = new Map(); - for (const op of opinionMsgs) { - const tip = idToTip.get(op.target) || op.target; - if (!opinionByTip.has(tip)) opinionByTip.set(tip, { opinions: {}, voters: new Set() }); - const agg = opinionByTip.get(tip); - if (agg.voters.has(op.author)) continue; - agg.voters.add(op.author); - if (op.category) agg.opinions[op.category] = (agg.opinions[op.category] || 0) + 1; - } - for (const [k, m] of Array.from(itemsById.entries())) { - const agg = opinionByTip.get(k); - if (!agg) continue; - const c = m.value?.content || {}; - const mergedVoters = new Set([...(Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : [])]); - const mergedOpinions = { ...(c.opinions || {}) }; - for (const [cat, n] of Object.entries(agg.opinions)) mergedOpinions[cat] = (mergedOpinions[cat] || 0) + n; - for (const v of agg.voters) mergedVoters.add(v); - itemsById.set(k, { ...m, value: { ...m.value, content: { ...c, opinions: mergedOpinions, opinions_inhabitants: [...mergedVoters] } } }); - } - let rawItems = Array.from(itemsById.values()).filter(m => types.includes(m.value?.content?.type)); const blobTypes = ['document', 'image', 'audio', 'video']; @@ -141,8 +101,7 @@ module.exports = ({ cooler }) => { case 'feed': return `feed::${(c.text || '').replace(/\s+/g, ' ').trim()}`; case 'votes': - case 'poll': - return `${c.type}::${(c.question || '').replace(/\s+/g, ' ').trim()}`; + return `votes::${(c.question || '').replace(/\s+/g, ' ').trim()}`; case 'transfer': return `transfer::${(c.concept || '')}|${c.amount || ''}|${c.from || ''}|${c.to || ''}|${c.deadline || ''}`; default: @@ -189,7 +148,9 @@ module.exports = ({ cooler }) => { items = items.filter(m => m.value.content.type === filter); } - items = items.filter(m => (m.value.content.opinions_inhabitants || []).length > 0); + if (filter !== 'ALL' && !types.includes(filter)) { + items = items.filter(m => (m.value.content.opinions_inhabitants || []).length > 0); + } if (filter === 'TOP') { items.sort((a, b) => { diff --git a/src/models/tribes_model.js b/src/models/tribes_model.js index f94e6e1..7d2a799 100644 --- a/src/models/tribes_model.js +++ b/src/models/tribes_model.js @@ -671,7 +671,7 @@ module.exports = ({ cooler, tribeCrypto }) => { if (typeof c.ch !== 'string' || typeof c.s !== 'string' || typeof c.ek !== 'string') continue; if (inviteTombstoned.has(m.key)) continue; if (tribeCrypto.hashInviteCode(code, c.s) !== c.ch) continue; - const chain = tribeCrypto.decryptChainFromInvite(c.ek, code, c.s, 3); + const chain = tribeCrypto.decryptChainFromInvite(c.ek, code, c.s); if (Array.isArray(chain) && chain.length) { matched = { msgKey: m.key, codeHash: c.ch, chain, multi: c.multi === 1 || c.multi === true }; break; @@ -929,7 +929,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const all = tribeCrypto.getAllRootIds(); let removed = 0; for (const rid of all) { - if (idx.effectivelyTombstoned.has(rid)) { + if (!idx.tribes.has(rid) || idx.effectivelyTombstoned.has(rid)) { try { tribeCrypto.dropKey(rid); removed++; } catch (_) {} } } diff --git a/src/models/votes_model.js b/src/models/votes_model.js index 75f2ca7..7802606 100644 --- a/src/models/votes_model.js +++ b/src/models/votes_model.js @@ -170,18 +170,12 @@ module.exports = ({ cooler }) => { } return { - MIN_VOTE_DAYS, - async createVote(question, deadline, options = ['YES', 'NO', 'ABSTENTION', 'CONFUSED', 'FOLLOW_MAJORITY', 'NOT_INTERESTED'], tagsRaw = []) { const ssbClient = await openSsb(); const userId = ssbClient.id; const parsedDeadline = moment(deadline, moment.ISO_8601, true); if (!parsedDeadline.isValid()) throw new Error('Invalid deadline'); - if (parsedDeadline.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) { - const err = new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); - err.code = 'VOTE_DEADLINE_MIN'; - throw err; - } + if (parsedDeadline.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) throw new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) @@ -252,11 +246,7 @@ module.exports = ({ cooler }) => { if (deadline != null && deadline !== '') { const parsed = moment(deadline, moment.ISO_8601, true); if (!parsed.isValid()) throw new Error('Invalid deadline'); - if (parsed.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) { - const err = new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); - err.code = 'VOTE_DEADLINE_MIN'; - throw err; - } + if (parsed.isBefore(moment().add(MIN_VOTE_DAYS, 'days').subtract(2, 'minutes'))) throw new Error(`Deadline must be at least ${MIN_VOTE_DAYS} days from now`); newDeadline = parsed.toISOString(); } diff --git a/src/models/workflows_model.js b/src/models/workflows_model.js deleted file mode 100644 index 19c258c..0000000 --- a/src/models/workflows_model.js +++ /dev/null @@ -1,104 +0,0 @@ -const ALL_MODULES = [ - 'agenda', 'ai', 'aiNav', 'audios', 'banking', 'blogs', 'bookmarks', 'calendars', 'chats', 'cipher', - 'courts', 'dev', 'docs', 'events', 'favorites', 'fediverse', 'feed', 'forum', 'games', 'graphos', - 'housing', 'images', 'industry', 'invites', 'jobs', 'larp', 'legacy', 'logs', 'maps', 'market', - 'melody', 'opinions', 'pads', 'parliament', 'pixelia', 'polls', 'projects', 'reports', 'shops', - 'tags', 'tasks', 'torrents', 'transfers', 'trending', 'tribes', 'videos', 'votes', 'wallet' -]; - -const NETWORK = ['feed', 'blogs', 'tags', 'trending', 'opinions', 'pads', 'forum', 'maps', 'chats']; -const MEDIA = ['audios', 'bookmarks', 'docs', 'images', 'torrents', 'videos']; -const OFFICE = ['agenda', 'calendars', 'events', 'tasks', 'reports', 'favorites']; -const GOVERNANCE = ['tribes', 'larp', 'votes', 'polls', 'parliament', 'courts', 'logs']; -const ECONOMY = ['banking', 'wallet', 'transfers', 'market', 'housing', 'jobs', 'shops', 'industry', 'projects']; - -const MOBILE_MODULES = [ - 'agenda', 'favorites', 'wallet', 'tribes', 'larp', 'votes', 'polls', 'events', 'calendars', 'tasks', - 'reports', 'banking', 'market', 'housing', 'jobs', 'shops', 'transfers', 'cipher', 'invites', - 'games', 'audios', 'bookmarks', 'docs', 'images', ...NETWORK -]; - -const WORKFLOWS = [ - { - key: 'default', - theme: 'Dark-SNH', - homePage: 'activity', - modules: ALL_MODULES.filter(m => m !== 'aiNav') - }, - { - key: 'jobs', - theme: 'Clear-SNH', - homePage: 'activity', - modules: [...OFFICE, 'jobs', 'projects', 'industry', 'market', 'shops', 'banking', 'wallet', 'transfers', - 'forum', 'chats', 'pads', 'feed', 'tags', 'trending', 'docs', 'invites'] - }, - { - key: 'ruling', - theme: 'Purple-SNH', - homePage: 'activity', - modules: [...GOVERNANCE, ...OFFICE, 'forum', 'pads', 'chats', 'feed', 'blogs', 'opinions', 'tags', 'trending', 'docs', 'invites'] - }, - { - key: 'politics', - theme: 'Dark-SNH', - homePage: 'trending', - modules: ['parliament', 'courts', 'votes', 'polls', 'reports', 'logs', 'forum', 'opinions', 'blogs', - 'feed', 'tags', 'trending', 'pads', 'chats', 'fediverse', 'tribes', 'events', 'agenda'] - }, - { - key: 'social', - theme: 'Clear-SNH', - homePage: 'feed', - modules: [...NETWORK, ...MEDIA, 'events', 'calendars', 'agenda', 'favorites', 'games', 'pixelia', - 'melody', 'tribes', 'fediverse', 'invites', 'polls'] - }, - { - key: 'business', - theme: 'Dark-SNH', - homePage: 'activity', - modules: [...ECONOMY, ...OFFICE, 'forum', 'chats', 'pads', 'docs', 'feed', 'tags', 'trending', 'invites', 'cipher'] - }, - { - key: 'night', - theme: 'Matrix-SNH', - homePage: 'feed', - modules: ['feed', 'blogs', 'opinions', 'chats', 'forum', 'tags', 'trending', 'audios', 'videos', - 'images', 'bookmarks', 'melody', 'games', 'pixelia', 'favorites'] - }, - { - key: 'mobile', - theme: 'OasisMobile', - homePage: 'activity', - modules: MOBILE_MODULES - } -]; - -const WORKFLOW_KEYS = WORKFLOWS.map(w => w.key); - -const getWorkflow = (key) => WORKFLOWS.find(w => w.key === String(key || '')) || null; - -const modulesOf = (workflow) => { - const set = new Set(workflow.modules.filter(m => ALL_MODULES.includes(m))); - return ALL_MODULES.filter(m => set.has(m)); -}; - -const currentWorkflow = (config) => { - const modules = (config && config.modules) || {}; - const theme = (config && config.themes && config.themes.current) || ''; - return WORKFLOW_KEYS.find(key => { - const w = getWorkflow(key); - if (theme !== w.theme) return false; - const on = new Set(modulesOf(w)); - return ALL_MODULES.every(m => (modules[`${m}Mod`] === 'on') === on.has(m)); - }) || null; -}; - -module.exports = { - ALL_MODULES, - MOBILE_MODULES, - WORKFLOWS, - WORKFLOW_KEYS, - getWorkflow, - modulesOf, - currentWorkflow -}; diff --git a/src/server/package-lock.json b/src/server/package-lock.json index 099f6b7..f8ea18b 100644 --- a/src/server/package-lock.json +++ b/src/server/package-lock.json @@ -1,6 +1,6 @@ { "name": "@krakenslab/oasis", - "version": "0.8.9", + "version": "0.8.7", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/src/server/package.json b/src/server/package.json index 7e26715..00bb761 100644 --- a/src/server/package.json +++ b/src/server/package.json @@ -1,6 +1,6 @@ { "name": "@krakenslab/oasis", - "version": "0.9.5", + "version": "0.8.7", "description": "Oasis - Social Networking Utopia", "repository": { "type": "git", diff --git a/src/server/ssb_config.js b/src/server/ssb_config.js index 551da1a..0299f68 100644 --- a/src/server/ssb_config.js +++ b/src/server/ssb_config.js @@ -11,31 +11,14 @@ const i = argv.indexOf('--'); const conf = argv.slice(i + 1); const cliArgs = ~i ? argv.slice(0, i) : argv; -// Nombre de la cuenta: define donde vive todo (~/.) y de ahi cuelgan el -// secret, la base de datos, los blobs y el socket. Sin la variable se usa 'ssb', -// que es lo de siempre, asi que las instalaciones existentes no notan nada. -// -// Se valida a proposito: este nombre acaba en rutas de fichero y hay operaciones -// destructivas (el modo panico borra el directorio de la cuenta) que no deben poder -// apuntar fuera de sitio. -// -// Manda la variable de entorno; si no esta, la cuenta que dejo anotada el selector -// de identidades de los ajustes; y si tampoco, 'ssb'. -const ACCOUNT_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/; -let annotated = null; -try { - const f = path.resolve(__dirname, '../configs', 'active-account'); - annotated = fs.readFileSync(f, 'utf8').trim() || null; -} catch (_) {} -const rawAccount = process.env.OASIS_ACCOUNT || annotated || 'ssb'; -const account = ACCOUNT_RE.test(rawAccount) ? rawAccount : 'ssb'; -if (account !== rawAccount) { - console.error(`[oasis] cuenta no valida (${rawAccount}); se usa 'ssb'`); -} +const override = minimist(conf); +if (global.__OASIS_KEYS__) override.keys = global.__OASIS_KEYS__; -let config = Config(account, minimist(conf)); +let config = Config('ssb', override); config = { ...config, ...configData }; +if (global.__OASIS_KEYS__) config.keys = global.__OASIS_KEYS__; + const debug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; if (debug) { config.logging = { ...(config.logging || {}), level: 'debug' }; @@ -45,13 +28,4 @@ const megabyte = Math.pow(2, 20); config.blobs = config.blobs || {}; config.blobs.max = 50 * megabyte; -config.account = account; - -config.statePath = (name) => { - // El directorio externo se separa por cuenta: sin esto dos identidades - // compartirian estado, que es justo lo que hay que evitar. - if (process.env.OASIS_STATE_DIR) return path.join(process.env.OASIS_STATE_DIR, account, name); - return config.path ? path.join(config.path, name) : null; -}; - module.exports = config; diff --git a/src/views/AI_view.js b/src/views/AI_view.js index 1529237..e7a0d14 100644 --- a/src/views/AI_view.js +++ b/src/views/AI_view.js @@ -9,34 +9,74 @@ exports.aiView = (history = [], userPrompt = '') => { div({ class: "tags-header" }, h2(i18n.aiTitle), p(i18n.aiDescription), - userPrompt ? div({ class: 'user-prompt' }, + userPrompt ? div({ class: 'user-prompt', style: 'margin-bottom: 2em; font-size: 0.95em; color: #888;' }, `${i18n.aiPromptUsed || 'System Prompt'}: `, - span({ class: 'user-prompt-text' }, `"${userPrompt}"`) + span({ style: 'font-style: italic;' }, `"${userPrompt}"`) ) : null, - form({ method: 'POST', action: '/ai', class: 'ai-prompt-form' }, + form({ method: 'POST', action: '/ai', style: "margin-bottom: 0;" }, textarea({ name: 'input', rows: 4, placeholder: i18n.aiInputPlaceholder, required: true }), br(), - div({ class: 'ai-submit-row' }, + div({ style: "display: flex; gap: 1.5em; justify-content: flex-end; align-items: center; margin-top: 0.7em;" }, button({ type: 'submit' }, i18n.aiSubmitButton) ) ), - div({ class: 'ai-clear-row' }, - form({ method: 'POST', action: '/ai/clear', class: 'ai-clear-form' }, - button({ type: 'submit', class: 'ai-clear-btn' }, i18n.aiClearHistory) + div({ style: "display: flex; justify-content: flex-end; margin-bottom: 2em;" }, + form({ method: 'POST', action: '/ai/clear', style: "display: inline;" }, + button({ + type: 'submit', + style: ` + background: #b80c09; + color: #fff; + border: none; + padding: 0.4em 1.2em; + border-radius: 6px; + cursor: pointer; + font-size: 1em; + margin-left: 1em; + ` + }, i18n.aiClearHistory) ) ), br(), ...history.map(entry => - div({ class: 'chat-entry' }, - entry.timestamp - ? span({ class: 'chat-entry-timestamp' }, new Date(entry.timestamp).toLocaleString()) - : null, + div({ + class: 'chat-entry', + style: ` + margin-bottom: 2em; + position: relative; + background: #191919; + border-radius: 10px; + box-shadow: 0 0 8px #0004; + padding-top: 1.8em; + ` + }, + entry.timestamp ? span({ + style: ` + position: absolute; + top: 0.5em; + right: 1.3em; + font-size: 0.92em; + color: #888; + ` + }, new Date(entry.timestamp).toLocaleString()) : null, br(), br(), - div({ class: 'user-question' }, + div({ class: 'user-question', style: 'margin-bottom: 0.75em;' }, h2(`${i18n.aiUserQuestion}:`), p(...renderUrl(entry.question)) ), - div({ class: 'ai-response' }, + div({ + class: 'ai-response', + style: ` + max-width: 800px; + margin: auto; + background: #111; + padding: 1.25em; + border-radius: 6px; + font-family: sans-serif; + line-height: 1.6; + color: #ffcc00; + ` + }, h2(`${i18n.aiResponseTitle}:`), ...String(entry.answer || '') .split('\n\n') @@ -44,19 +84,31 @@ exports.aiView = (history = [], userPrompt = '') => { paragraph .split('\n') .map(line => - p(...renderUrl(line.trim())) + p({ style: "margin-bottom: 1.2em;" }, ...renderUrl(line.trim())) ) ) ), - div({ class: 'ai-train-bar' }, + div({ + class: 'ai-train-bar', + style: ` + display:flex; + align-items:center; + gap:12px; + margin: 12px auto 8px auto; + max-width: 800px; + padding: 8px 0; + border-top: 1px solid #2a2a2a; + flex-wrap: wrap; + ` + }, Array.isArray(entry.snippets) && entry.snippets.length - ? span({ class: 'ai-snippets-used' }, `${i18n.aiSnippetsUsed}: ${entry.snippets.length}`) + ? span({ style: 'color:#9aa; font-size:0.95em;' }, `${i18n.aiSnippetsUsed}: ${entry.snippets.length}`) : null, h2(`${i18n.statsAITraining}:`), entry.trainStatus === 'approved' - ? span({ class: 'ai-train-approved' }, i18n.aiTrainApproved) + ? span({ style: 'color:#5ad25a; font-weight:600;' }, i18n.aiTrainApproved) : entry.trainStatus === 'rejected' - ? span({ class: 'ai-train-rejected' }, i18n.aiTrainRejected) + ? span({ style: 'color:#ff6b6b; font-weight:600;' }, i18n.aiTrainRejected) : null, entry.trainStatus === 'approved' || entry.trainStatus === 'rejected' ? null diff --git a/src/views/activity_view.js b/src/views/activity_view.js index f922d5c..f594a71 100644 --- a/src/views/activity_view.js +++ b/src/views/activity_view.js @@ -1,8 +1,8 @@ -const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions } = require('./main_views'); +const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary, ul, li, label } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions, emptyState, renderHiveNav } = require('./main_views'); const opinionCategories = require('../backend/opinion_categories'); -const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent']); +const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent','transfer']); const OPINION_ROUTES = { feed: (id) => `/feed/opinions/${encodeURIComponent(id)}`, bookmark: (id) => `/bookmarks/opinions/${encodeURIComponent(id)}`, @@ -11,11 +11,11 @@ const OPINION_ROUTES = { torrent: (id) => `/torrents/opinions/${encodeURIComponent(id)}`, video: (id) => `/videos/opinions/${encodeURIComponent(id)}`, document: (id) => `/documents/opinions/${encodeURIComponent(id)}`, - votes: (id) => `/votes/opinions/${encodeURIComponent(id)}` + votes: (id) => `/votes/opinions/${encodeURIComponent(id)}`, + transfer: (id) => `/transfers/opinions/${encodeURIComponent(id)}` }; const moment = require("../server/node_modules/moment"); const { renderUrl } = require('../backend/renderUrl'); -const { letterOf } = require('./polls_view'); const { getConfig } = require("../configs/config-manager.js"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); @@ -228,14 +228,7 @@ function buildActivityItemsWithPostThreads(deduped, allActions) { } exports.renderActionCards = renderActionCards; -const SPREADABLE_TYPES = new Set([ - 'post', 'audio', 'video', 'image', 'document', 'torrent', 'bookmark', - 'event', 'calendar', 'task', 'votes', 'vote', 'market', 'shop', 'shopProduct', - 'project', 'transfer', 'housing', 'job', 'report', 'industry', 'industryBuild', 'industryBlueprint', - 'chat', 'chatMessage', 'pad', 'padEntry', 'forum', 'map', 'poll', 'blog' -]); - -function renderActionCards(actions, userId, allActions, spreadMap = new Map(), extras = {}) { +function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const all = Array.isArray(allActions) ? allActions : actions; const byIdAll = new Map(); for (const a0 of all) { @@ -292,7 +285,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e : validActions; if (!deduped.length) { - return div({ class: "no-actions" }, p(i18n.noActions)); + return emptyState({ icon: "📭", title: i18n.noActions, text: i18n.emptyConnectHint, ctaHref: "/invites", ctaLabel: i18n.emptyConnectCta }); } const seenDocumentTitles = new Set(); @@ -350,7 +343,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e } else if (type === 'shopProduct') { headerText = `[SHOP · PRODUCT]`; } else if (type === 'chat') { - headerText = `[CHAT \u00b7 ${String(i18n.chatThreadsLabel).toUpperCase()}]`; + headerText = `[CHAT \u00b7 NEW]`; } else if (type === 'pad') { headerText = `[PAD · ${String(i18n.padNew || 'NEW').toUpperCase()}]`; } else if (type === 'ubiClaim') { @@ -558,19 +551,29 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e } if (type === 'tribe') { - const { title, image, description, isAnonymous } = content; + const { title, image, description, location, tags, inviteMode, isAnonymous } = content; if (isAnonymous === true) { skip = true; } + const validTags = Array.isArray(tags) ? tags : []; cardBody.push( div({ class: 'card-section tribe' }, h2({ class: 'tribe-title' }, a({ href: `/tribe/${encodeURIComponent(action.id)}`, class: "user-link" }, title) ), + div({ style: 'display:flex; gap:.6em; flex-wrap:wrap;' }, + location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeLocationLabel.toUpperCase()) + ':'), span({ class: 'card-value' }, ...renderUrl(location))) : "", + typeof isAnonymous === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeIsAnonymousLabel+ ':'), span({ class: 'card-value' }, isAnonymous ? i18n.tribePrivate : i18n.tribePublic)) : "", + inviteMode ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.tribeModeLabel) + ':'), span({ class: 'card-value' }, inviteMode.toUpperCase())) : "" + ), image ? (/^(\/|https?:)/.test(String(image)) ? img({ src: image, class: 'feed-image tribe-image' }) : renderMediaBlob(image, null)) : null, - p({ class: 'tribe-description' }, ...renderUrl(description || '')) + p({ class: 'tribe-description' }, ...renderUrl(description || '')), + validTags.length + ? div({ class: 'card-tags' }, validTags.map(tag => + a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) + : "" ) ); } @@ -742,7 +745,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e } if (type === 'event') { - const { title, description, date, location, price, attendees, isPublic } = content; + const { title, description, date, location, price, attendees, organizer, isPublic } = content; cardBody.push( div({ class: 'card-section event' }, div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)), @@ -751,6 +754,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "", price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "", br(), + organizer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.organizer || 'Organizer') + ': '), userLink(organizer)) : "", Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : "" ) ); @@ -834,9 +838,9 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e (url) => `${url}` ); - bodyNode = div({ class: 'post-text post-text-clamped', innerHTML: sanitizeHtml(linkified) }); + bodyNode = div({ class: 'post-text', style: 'max-height:180px;overflow:hidden;', innerHTML: sanitizeHtml(linkified) }); } else { - bodyNode = p({ class: 'post-text post-text-pre post-text-clamped' }, ...renderUrlPreserveNewlines(displayText)); + bodyNode = p({ class: 'post-text post-text-pre', style: 'max-height:180px;overflow:hidden;' }, ...renderUrlPreserveNewlines(displayText)); } const threadId = getThreadIdFromPost(action); const replyToId = getReplyToIdFromPost(action, byIdAll); @@ -851,18 +855,18 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e div({ class: 'card-section post' }, isReply ? div( - { class: 'reply-context' }, - span({ class: 'reply-context-label' }, + { class: 'reply-context', style: 'border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:0.85;' }, + span({ style: 'font-size:0.85em;' }, a({ href: ctxHref, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : '' ), - parentText ? p({ class: 'post-text reply-context-text post-text-pre' }, ...renderUrlPreserveNewlines(parentText)) : '' + parentText ? p({ class: 'post-text reply-context-text post-text-pre', style: 'font-size:0.85em;max-height:80px;overflow:hidden;margin-top:4px;' }, ...renderUrlPreserveNewlines(parentText)) : '' ) : '', contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '', bodyNode, isTruncated && threadId - ? div({ class: 'card-section-action' }, + ? div({ style: 'margin-top:6px;' }, a({ href: `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(action.id || threadId)}`, class: 'filter-btn' }, i18n.keepReading || 'Keep reading...') ) : '' @@ -889,16 +893,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e span({ class: 'pm-exposition-text' }, `${String(i18n.typePost || 'POST').toUpperCase()} · THREAD`) ) ), - renderContentActions(threadId, href, { - author: action.author, - spread: spreadMap.get(threadId) || null, - ...favOptsFor('post', threadId, extras) - }) + renderContentActions(threadId, href) ), div({ class: 'card-body' }, root && root.text ? div({ class: 'card-section' }, - p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(root.text)) + p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(root.text)) ) : '', div({ class: 'card-section' }, @@ -906,7 +906,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e const rDate = r.ts ? new Date(r.ts).toLocaleString() : ''; return div({ class: 'thread-reply-item' }, div({ class: 'thread-reply' }, - r.text ? p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(r.text)) : '' + r.text ? p({ class: 'post-text', style: 'white-space:pre-wrap;' }, ...renderUrlPreserveNewlines(r.text)) : '' ), div({ class: 'card-footer thread-reply-footer' }, span({ class: 'date-link' }, rDate), @@ -923,68 +923,6 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e ); } - if (type === 'chatThread') { - const c = action.content || {}; - const chatRoot = c.chatRoot; - const href = `/chats/${encodeURIComponent(chatRoot)}`; - const chatTitle = c.title || chatRoot; - const chatDesc = c.description || ''; - const replies = Array.isArray(c.replies) ? c.replies : []; - const repliesAsc = replies.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0)); - const limit = 6; - const show = repliesAsc.slice(Math.max(0, repliesAsc.length - limit)); - return div({ class: 'trending-card post-thread chat-thread' + (String(action.author) === String(userId) ? ' own-content' : '') }, - div({ class: 'card-header activity-card-header' }, - div({ class: 'card-chips-row' }, - span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} \u00b7 ${String(i18n.chatThreadsLabel).toUpperCase()}`) - ) - ), - renderContentActions(chatRoot, href, { - author: action.author, - reportTitle: chatTitle, - spread: spreadMap.get(chatRoot) || null, - ...favOptsFor('chat', chatRoot, extras) - }) - ), - div({ class: 'card-body' }, - div({ class: 'card-section chat' }, - div({ class: 'card-field' }, a({ href, class: 'card-value user-link' }, chatTitle)), - chatDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, chatDesc)) : '', - div({ class: 'card-field chat-thread-meta' }, - span({ class: 'card-label' }, `${i18n.chatParticipants}: `), - span({ class: 'card-value' }, String(c.members || 0)), - span({ class: 'card-label' }, ` · ${i18n.chatMessagesLabel}: `), - span({ class: 'card-value' }, String(c.messageCount || show.length)) - ) - ), - show.length - ? details({ class: 'chat-thread-details' }, - summary({ class: 'comments-summary chat-thread-summary' }, i18n.keepReading || 'Keep reading...'), - div({ class: 'card-section' }, - show.map(r => { - const rDate = r.ts ? new Date(r.ts).toLocaleString() : ''; - return div({ class: 'thread-reply-item' }, - div({ class: 'thread-reply' }, - r.text ? p({ class: 'post-text thread-reply-text' }, ...renderUrlPreserveNewlines(r.text)) : '' - ), - div({ class: 'card-footer thread-reply-footer' }, - span({ class: 'date-link' }, rDate), - userLink(r.author, action.authorNames && action.authorNames[r.author]) - ) - ); - }) - ) - ) - : null - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `), - userLink(action.author, action.authorNames && action.authorNames[action.author]) - ) - ); - } - if (type === 'forum') { const { root, category, title, text, key, rootTitle, rootKey } = content; if (!root) { @@ -992,10 +930,9 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e const linkText = (title && String(title).trim()) ? title : ''; cardBody.push( div({ class: 'card-section forum' }, - div({ class: 'card-field' }, - linkKey - ? a({ href: `/forum/${encodeURIComponent(linkKey)}`, class: 'card-value user-link' }, linkText || linkKey) - : span({ class: 'card-value' }, linkText) + div({ class: 'card-field', style: "font-size:1.12em; margin-bottom:5px;" }, + span({ class: 'card-label', style: "font-weight:800;color:#ff9800;" }, i18n.title + ': '), + a({ href: `/forum/${encodeURIComponent(linkKey)}`, style: "font-weight:800;color:#4fc3f7;" }, linkText) ) ) ); @@ -1010,15 +947,15 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e cardBody.push( div({ class: 'card-section forum' }, div( - { class: 'reply-context' }, - span({ class: 'reply-context-meta' }, + { class: 'reply-context', style: 'border-left:3px solid #666;padding-left:10px;margin-bottom:8px;opacity:0.85;' }, + span({ style: 'font-size:0.85em;' }, a({ href: `/forum/${encodeURIComponent(hrefKey)}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : '' ), - parentTitle ? p({ class: 'post-text reply-context-text' }, parentTitle) : '' + parentTitle ? p({ class: 'post-text reply-context-text', style: 'font-size:0.85em;max-height:60px;overflow:hidden;margin-top:4px;font-weight:bold;color:#4fc3f7;' }, parentTitle) : '' ), - div({ class: 'card-field forum-reply-body' }, - p({ class: 'forum-reply-text' }, ...renderUrl(text)) + div({ class: 'card-field', style: 'margin-bottom:12px;' }, + p({ style: "margin:0 0 8px 0; word-break:break-all;" }, ...renderUrl(text)) ) ) ); @@ -1061,8 +998,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e spreadTitle ? h2({ class: 'post-title activity-spread-title' }, spreadTitle) : '', spreadContentWarning ? h2({ class: 'content-warning' }, spreadContentWarning) : '', spreadExcerpt - ? div({ class: 'post-text activity-spread-text post-text-pre activity-spread-clamped' }, ...renderUrlPreserveNewlines(spreadExcerpt)) - : div({ class: 'post-text activity-spread-text activity-spread-missing' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'), + ? div({ class: 'post-text activity-spread-text post-text-pre', style: 'max-height:200px;overflow:hidden;' }, ...renderUrlPreserveNewlines(spreadExcerpt)) + : div({ class: 'post-text activity-spread-text', style: 'opacity:0.6;font-style:italic;' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'), spreadOriginalAuthor ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.spreadBy || 'By') + ': '), @@ -1076,7 +1013,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e ) : '', link - ? div({ class: 'card-section-action' }, + ? div({ style: 'margin-top:6px;' }, a({ href: `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}`, class: 'filter-btn' }, i18n.viewDetails || 'View details') ) : '' @@ -1206,100 +1143,6 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e ); } - if (type === 'industry') { - const { name, sector, description, image, tags, membershipPolicy, status } = content; - const facKey = action.id || action.key || ''; - const validTags = Array.isArray(tags) ? tags : []; - const statusValue = (() => { - const u = String(status || 'ACTIVE').toUpperCase(); - if (u === 'PAUSED') return i18n.industryStatusPaused || 'PAUSED'; - if (u === 'DISSOLVED') return i18n.industryStatusDissolved || 'DISSOLVED'; - return i18n.industryStatusActive || 'WORKING'; - })(); - const policyValue = String(i18n[`industryPolicy_${membershipPolicy}`] || membershipPolicy || '').toUpperCase(); - cardBody.push( - div({ class: 'card-section tribe industry' }, - h2({ class: 'tribe-title' }, - a({ href: `/industry/${encodeURIComponent(facKey)}`, class: "user-link" }, name || (i18n.industryTitle || 'Industry')) - ), - div({ class: 'card-field-row' }, - sector ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industrySector || 'Sector').toUpperCase() + ':'), span({ class: 'card-value' }, String(sector).toUpperCase())) : "", - status ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, statusValue)) : "", - membershipPolicy ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryMembershipPolicy || 'Membership policy').toUpperCase() + ':'), span({ class: 'card-value' }, policyValue)) : "" - ), - image - ? (/^(\/|https?:)/.test(String(image)) - ? img({ src: image, class: 'feed-image tribe-image' }) - : renderMediaBlob(image, null)) - : null, - p({ class: 'tribe-description' }, ...renderUrl(description || '')), - validTags.length - ? div({ class: 'card-tags' }, validTags.map(tag => - a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) - : "" - ) - ); - } - - if (type === 'industryBuild') { - const { title, notes, image, startDate, endDate, buildStatus } = content; - const buildKey = action.id || action.key || ''; - const daysLeft = endDate ? Math.max(0, Math.ceil((new Date(endDate) - Date.now()) / 86400000)) : null; - cardBody.push( - div({ class: 'card-section tribe industry' }, - h2({ class: 'tribe-title' }, - a({ href: `/industry/build/${encodeURIComponent(buildKey)}`, class: "user-link" }, title || (i18n.industryBuild || 'Build')) - ), - (buildStatus || startDate || endDate) ? div({ class: 'card-field-row' }, - buildStatus ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryBuildStatus_' + buildStatus] || buildStatus).toUpperCase())) : "", - startDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildStart || 'Start date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(startDate).format('YYYY-MM-DD'))) : "", - endDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildEnd || 'End date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(endDate).format('YYYY-MM-DD'))) : "", - daysLeft != null ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryTimeLeft || 'Time left').toUpperCase() + ':'), span({ class: 'card-value' }, `${daysLeft}d`)) : "" - ) : null, - image - ? (/^(\/|https?:)/.test(String(image)) - ? img({ src: image, class: 'feed-image tribe-image' }) - : renderMediaBlob(image, null)) - : null, - notes ? p({ class: 'tribe-description' }, ...renderUrl(notes)) : "" - ) - ); - } - - if (type === 'industryBlueprint') { - const { name, facility, description, outKind, laborHours, image, estTotal } = content; - cardBody.push( - div({ class: 'card-section tribe industry' }, - h2({ class: 'tribe-title' }, - facility - ? a({ href: `/industry/${encodeURIComponent(facility)}`, class: "user-link" }, name || (i18n.industryBlueprint || 'Blueprint')) - : (name || (i18n.industryBlueprint || 'Blueprint')) - ), - div({ class: 'card-field-row' }, - outKind ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryOutputKind || 'Product type').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryKind_' + outKind] || outKind).toUpperCase())) : "", - laborHours ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryLaborHours || 'Labor hours').toUpperCase() + ':'), span({ class: 'card-value' }, String(laborHours))) : "", - estTotal != null ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryEstTotal || 'Estimated price').toUpperCase() + ':'), span({ class: 'card-value' }, `${Number(estTotal).toFixed(2)} ECO`)) : "" - ), - image - ? (/^(\/|https?:)/.test(String(image)) - ? img({ src: image, class: 'feed-image tribe-image' }) - : renderMediaBlob(image, null)) - : null, - description ? p({ class: 'tribe-description' }, ...renderUrl(description)) : "" - ) - ); - } - - if (type === 'industryAllocation') { - const { target, pot } = content; - cardBody.push( - div({ class: 'card-section industry' }, - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.industryDistribution || 'Distribution') + ':'), span({ class: 'card-value' }, target ? a({ href: `/industry/build/${encodeURIComponent(target)}`, class: 'user-link' }, i18n.industryBuild || 'Build') : '')), - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.industryPot || 'Pot') + ':'), span({ class: 'card-value' }, `${Number(pot || 0).toFixed(6)} ECO`)) - ) - ); - } - if (type === 'chat') { const { title, description, image, category, status } = content; const chatKey = action.id || action.key || ''; @@ -1307,12 +1150,10 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e const chatImageNode = renderMediaBlob(image); cardBody.push( div({ class: 'card-section chat' }, - div({ class: 'card-field' }, - chatKey - ? a({ href: `/chats/${encodeURIComponent(chatKey)}`, class: 'card-value user-link' }, title || chatKey) - : span({ class: 'card-value' }, title || '')), - displayDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, displayDesc)) : '', - category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : '' + div({ class: 'card-field' }, span({ class: 'card-label' }, 'Chat:'), span({ class: 'card-value' }, chatKey ? a({ href: `/chats/${encodeURIComponent(chatKey)}`, class: 'user-link' }, title || chatKey) : (title || ''))), + displayDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatDescription || 'Description') + ':'), span({ class: 'card-value' }, displayDesc)) : '', + category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : '', + status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatStatus || 'Status') + ':'), span({ class: 'card-value' }, status)) : '' ) ); } @@ -1322,7 +1163,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e const padTitle = content.title || action.title || ''; cardBody.push( div({ class: 'card-section' }, - div({ class: 'card-field' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'card-value user-link' }, padTitle || padKey) : span({ class: 'card-value' }, padTitle || '')), + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padTitle || 'Pad') + ':'), span({ class: 'card-value' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'user-link' }, padTitle || padKey) : '')), content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padDeadlineLabel || 'Deadline') + ':'), span({ class: 'card-value' }, content.deadline)) : '' ) ); @@ -1333,41 +1174,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e const calTitle = content.title || action.title || ''; cardBody.push( div({ class: 'card-section' }, - div({ class: 'card-field' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'card-value user-link' }, calTitle || calKey) : span({ class: 'card-value' }, calTitle || '')) - ) - ); - } - - if (type === 'poll') { - const options = Array.isArray(content.options) ? content.options : []; - const counts = (action.pollCounts && typeof action.pollCounts === 'object') ? action.pollCounts : {}; - const voters = Number(action.pollVoters) || 0; - const pctOf = (opt) => (voters > 0 ? Math.round(((counts[opt] || 0) / voters) * 100) : 0); - const step = (opt) => Math.round(pctOf(opt) / 5) * 5; - cardBody.push( - div({ class: 'card-section poll' }, - div({ class: 'card-field' }, - span({ class: 'card-value' }, content.question || '') - ), - options.length - ? table({ class: 'poll-results' }, - ...options.map((opt, i) => - tr( - td({ class: 'poll-result-option' }, `${letterOf(i)})`), - td({ class: 'poll-result-bar' }, - div({ class: 'poll-bar' }, - div({ class: `poll-bar-fill poll-bar-fill-${step(opt)} poll-hue-${i % 8}` }) - ) - ), - td({ class: 'poll-result-count' }, `${counts[opt] || 0} (${pctOf(opt)}%)`) - ) - ) - ) - : null, - div({ class: 'card-field' }, - span({ class: 'card-label' }, `${i18n.pollVoters}: `), - span({ class: 'card-value' }, String(voters)) - ) + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.calendarTitle || 'Calendar') + ':'), span({ class: 'card-value' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'user-link' }, calTitle || calKey) : '')), + content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.calendarStatusLabel || 'Status') + ':'), span({ class: 'card-value' }, content.status)) : '' ) ); } @@ -1535,51 +1343,6 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e ); } - if (type === 'housing') { - const { title, housing_type, property_type, price, place, status, rooms, size, capacity } = content; - const isFree = String(housing_type || '').toLowerCase() === 'couchsurfing'; - cardBody.push( - div({ class: 'card-section report' }, - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.title + ':'), - span({ class: 'card-value' }, title) - ), - housing_type ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.housingType + ':'), - span({ class: 'card-value' }, String(housing_type).toUpperCase()) - ) : null, - property_type ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.housingProperty + ':'), - span({ class: 'card-value' }, String(property_type).toUpperCase()) - ) : null, - div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.housingPrice + ':'), - span({ class: 'card-value' }, isFree ? (i18n.housingFree || 'FREE') : `${Number(price || 0).toFixed(2)} ECO`) - ), - place ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.housingPlace + ':'), - span({ class: 'card-value' }, place) - ) : null, - Number(rooms) > 0 ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.housingRooms + ':'), - span({ class: 'card-value' }, String(rooms)) - ) : null, - Number(size) > 0 ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.housingSize + ':'), - span({ class: 'card-value' }, `${size} m²`) - ) : null, - Number(capacity) > 0 ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.housingCapacity + ':'), - span({ class: 'card-value' }, String(capacity)) - ) : null, - status ? div({ class: 'card-field' }, - span({ class: 'card-label' }, i18n.housingStatus + ':'), - span({ class: 'card-value' }, String(status).toUpperCase()) - ) : null - ) - ); - } - if (type === 'job') { const { title, job_type, tasks, location, vacants, salary, status, subscribers } = content; cardBody.push( @@ -1588,26 +1351,26 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title) ), - salary ? div({ class: 'card-field' }, + salary && div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSalary + ':'), span({ class: 'card-value' }, salary + ' ECO') - ) : null, - status ? div({ class: 'card-field' }, + ), + status && div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobStatus + ':'), - span({ class: 'card-value' }, String(status).toUpperCase()) - ) : null, - job_type ? div({ class: 'card-field' }, + span({ class: 'card-value' }, status.toUpperCase()) + ), + job_type && div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobType + ':'), - span({ class: 'card-value' }, String(job_type).toUpperCase()) - ) : null, - location ? div({ class: 'card-field' }, + span({ class: 'card-value' }, job_type.toUpperCase()) + ), + location && div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobLocation + ':'), - span({ class: 'card-value' }, String(location).toUpperCase()) - ) : null, - Number(vacants) > 0 ? div({ class: 'card-field' }, + span({ class: 'card-value' }, location.toUpperCase()) + ), + vacants && div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobVacants + ':'), - span({ class: 'card-value' }, String(vacants)) - ) : null, + span({ class: 'card-value' }, vacants) + ), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.jobSubscribers + ':'), span({ class: 'card-value' }, @@ -1639,7 +1402,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e } if (type === 'parliamentTerm') { - const { method, powerType, powerId, powerTitle, startAt, endAt } = content; + const { method, powerType, powerId, powerTitle, winnerVotes, totalVotes, startAt, endAt } = content; const powerTypeNorm = String(powerType || '').toLowerCase(); const winnerLink = powerTypeNorm === 'tribe' @@ -1648,11 +1411,17 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e ? a({ href: `/parliament?filter=government`, class: 'user-link' }, (i18n.parliamentAnarchy || 'ANARCHY')) : userLink(powerId, powerTitle); + const methodUpper = String( + i18n['parliamentMethod' + String(method || '').toUpperCase()] || method + ).toUpperCase(); + cardBody.push( div({ class: 'card-section parliament' }, startAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsStart.toUpperCase() || 'Elections start') + ':'), span({ class: 'card-value' }, new Date(startAt).toLocaleString())) : '', endAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsEnd.toUpperCase() || 'Elections end') + ':'), span({ class: 'card-value' }, new Date(endAt).toLocaleString())) : '', - div({ class: 'card-field' }, span({ class: 'card-value' }, winnerLink)) + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentCurrentLeader.toUpperCase() || 'Winning candidature') + ':'), span({ class: 'card-value' }, winnerLink)), + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod.toUpperCase() || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), + div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesReceived.toUpperCase() || 'Votes received') + ':'), span({ class: 'card-value' }, `${Number(winnerVotes || 0)} (${Number(totalVotes || 0)})`)) ) ); } @@ -1667,7 +1436,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e cardBody.push( div({ class: 'card-section parliament' }, title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '', - description ? p({ class: 'card-section-text' }, description) : '', + description ? p({ style: 'margin:.4rem 0' }, description) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '', voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '', @@ -1685,7 +1454,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e cardBody.push( div({ class: 'card-section parliament' }, title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '', - reasons ? p({ class: 'card-section-text' }, reasons) : '', + reasons ? p({ style: 'margin:.4rem 0' }, reasons) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '', voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '', @@ -1706,7 +1475,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e cardBody.push( div({ class: 'card-section parliament' }, question ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawQuestion || 'Question') + ':'), span({ class: 'card-value' }, question)) : '', - description ? p({ class: 'card-section-text' }, description) : '', + description ? p({ style: 'margin:.4rem 0' }, description) : '', div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)), proposer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawProposer || 'Proposer') + ':'), span({ class: 'card-value' }, userLink(proposer))) : '', enactedAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted || 'Enacted at') + ':'), span({ class: 'card-value' }, new Date(enactedAt).toLocaleString())) : '', @@ -1768,7 +1537,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e return null; } - const detailHref = (type !== 'aiExchange' && type !== 'bankWallet') + const detailHref = (type !== 'feed' && type !== 'aiExchange' && type !== 'bankWallet') ? (isParliamentTarget ? `/parliament?filter=${encodeURIComponent(parliamentFilter)}` : isCourtsTarget @@ -1784,12 +1553,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e span({ class: 'pm-exposition-text' }, String(type || '').toUpperCase()) ) ), - renderContentActions(msgId, detailHref, { - author: action.author, - reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '', - spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined, - ...favOptsFor(type, msgId, extras) - }) + renderContentActions(msgId, detailHref) ), ...cardBody, (() => { @@ -1799,18 +1563,27 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e const ops = (action.value?.content?.opinions) || (action.content?.opinions) || {}; const opsTotal = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0); return details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, - span({ class: 'opinions-summary-icon' }, 'ꔍ'), - span({ class: 'opinions-summary-count' }, `(${opsTotal})`)), + summary({ class: 'opinions-summary' }, `${i18n.opinionsTitle || 'Opinions'} (${opsTotal})`), div({ class: 'voting-buttons' }, opinionCategories.map(cat => form({ method: 'POST', action: `${routeFn(action.id)}/${cat}` }, - button({ class: 'vote-btn' }, `${String(i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat).toUpperCase()} [${ops[cat] || 0}]`) + button({ class: 'vote-btn' }, `${i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat} [${ops[cat] || 0}]`) ) ) ) ); })(), + (() => { + const SPREADABLE = new Set([ + 'post','audio','video','image','document','torrent','bookmark', + 'event','calendar','task','votes','vote','market','shop','shopProduct', + 'project','transfer','job','report', + 'chat','chatMessage','pad','padEntry','forum','map' + ]); + if (!SPREADABLE.has(type)) return null; + const btn = renderSpreadButton(action.id, spreadMap.get(action.id)); + return btn ? div({ class: 'card-spread-left' }, btn) : null; + })(), (() => { const footerAuthorId = action.author || (content && content.proposer) || ''; return p({ class: 'card-footer' }, @@ -1823,31 +1596,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e const filteredCards = cards.filter(Boolean); if (!filteredCards.length) { - return div({ class: "no-actions" }, p(i18n.noActions)); + return emptyState({ icon: "📭", title: i18n.noActions, text: i18n.emptyConnectHint, ctaHref: "/invites", ctaLabel: i18n.emptyConnectCta }); } return filteredCards; } -const FAV_KIND_BY_TYPE = { - image: 'images', audio: 'audios', video: 'videos', document: 'documents', - bookmark: 'bookmarks', torrent: 'torrents', event: 'events', task: 'tasks', - report: 'reports', votes: 'votes', poll: 'polls', market: 'market', - housing: 'housing', job: 'jobs', project: 'projects', shop: 'shops', - chat: 'chats', chatThread: 'chats', pad: 'pads', calendar: 'calendars', - map: 'maps', forum: 'forum', transfer: 'transfers', post: 'blogs' -}; - -const favOptsFor = (type, id, extras = {}) => { - const favKind = FAV_KIND_BY_TYPE[type]; - if (!favKind) return {}; - const index = extras.favIndex instanceof Map ? extras.favIndex : null; - return { - favKind, - isFavorite: index ? index.get(String(id)) === favKind : false, - returnTo: extras.returnTo || '/activity' - }; -}; - function getViewDetailsAction(type, action) { const id = encodeURIComponent(safeMsgId(action.tipId || action.id || action.key || action)); switch (type) { @@ -1869,18 +1622,11 @@ function getViewDetailsAction(type, action) { const link = normalizeSpreadLink(action.content?.spreadTargetId || action.content?.vote?.link || ''); return link ? `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}` : `/activity`; } - case 'post': { - const root = action.content && action.content.root; - return root - ? `/blogs/${encodeURIComponent(root)}#${id}` - : `/blogs/${id}`; - } - case 'feed': return `/feed/${id}`; + case 'post': return `/thread/${id}#${id}`; case 'vote': return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`; case 'votes': return `/votes/${id}`; case 'transfer': return `/transfers/${id}`; case 'pixelia': return `/pixelia`; - case 'poll': return `/polls/${id}`; case 'larpHousePost': return action.content?.house ? `/larp/${encodeURIComponent(action.content.house)}` : `/activity`; case 'tribe': return `/tribe/${id}`; case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`; @@ -1905,13 +1651,8 @@ function getViewDetailsAction(type, action) { case 'chat': return `/chats/${id}`; case 'pad': return `/pads/${id}`; case 'calendar': return `/calendars/${id}`; - case 'housing': return `/housing/${id}`; case 'job': return `/jobs/${id}`; case 'project': return `/projects/${id}`; - case 'industry': return `/industry/${id}`; - case 'industryBuild': return `/industry/build/${id}`; - case 'industryBlueprint': return `/industry/blueprint/${id}`; - case 'industryAllocation': return action.content?.target ? `/industry/build/${encodeURIComponent(action.content.target)}` : '/industry?filter=BUILDS'; case 'report': return `/reports/${id}`; case 'bankWallet': return `/wallet`; case 'bankClaim': return `/banking${action.content?.epochId ? `/epoch/${encodeURIComponent(action.content.epochId)}` : ''}`; @@ -1920,18 +1661,15 @@ function getViewDetailsAction(type, action) { } } -exports.getViewDetailsAction = getViewDetailsAction; - exports.activityView = (actions, filter, userId, q = '', extras = {}) => { const spreadMap = (extras && extras.spreadMap) || new Map(); const title = filter === 'mine' ? i18n.yourActivity : i18n.globalActivity; const desc = i18n.activityDesc; const activityTypes = [ + { type: 'recent', label: i18n.typeRecent }, { type: 'all', label: i18n.allButton }, { type: 'mine', label: i18n.mineButton }, - { type: 'recent', label: i18n.typeRecent }, - { type: 'top', label: i18n.typeTop }, { type: 'inhabitants', label: i18n.typeInhabitants }, { type: 'tribe', label: i18n.typeTribe }, { type: 'larp', label: i18n.typeLarp }, @@ -1951,11 +1689,10 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { { type: 'banking', label: i18n.typeBanking }, { type: 'market', label: i18n.typeMarket }, { type: 'project', label: i18n.typeProject }, - { type: 'industry', label: i18n.typeIndustry }, - { type: 'housing', label: i18n.typeHousing }, { type: 'job', label: i18n.typeJob }, { type: 'shop', label: i18n.typeShop }, { type: 'transfer', label: i18n.typeTransfer }, + { type: 'curriculum',label: i18n.typeCurriculum }, { type: 'audio', label: i18n.typeAudio }, { type: 'bookmark', label: i18n.typeBookmark }, { type: 'document', label: i18n.typeDocument }, @@ -1964,18 +1701,15 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { { type: 'video', label: i18n.typeVideo } ]; - const FILTER_META = new Set(['all', 'mine', 'recent']); + const FILTER_META = new Set(['recent', 'all', 'mine']); const GROUP_SUBTYPES = { parliament: ['parliamentCandidature', 'parliamentTerm', 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw'], courts: ['courtsCase', 'courtsNomination', 'courtsNominationVote'], banking: ['bankWallet', 'bankClaim', 'ubiClaim'], task: ['task', 'taskAssignment'], - votes: ['votes', 'poll'], shop: ['shop', 'shopProduct'], larp: ['larpHousePost'], - inhabitants:['about'], - chat: ['chat', 'chatThread'], - industry: ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'] + inhabitants:['about'] }; const ALLOWED_TYPES = new Set(); for (const { type } of activityTypes) { @@ -2010,17 +1744,8 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'task' || action.type === 'taskAssignment')); } else if (filter === 'torrent') { filteredActions = actions.filter(action => action.type === 'torrent'); - } else if (filter === 'chat') { - filteredActions = actions.filter(action => (action.type === 'chat' || action.type === 'chatThread') && action.type !== 'tombstone'); - } else if (filter === 'industry') { - filteredActions = actions.filter(action => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(action.type) && action.type !== 'tombstone'); } else { - filteredActions = actions.filter(action => - (action.type === filter - || filter === 'all' - || (filter === 'shop' && action.type === 'shopProduct') - || (filter === 'votes' && action.type === 'poll')) - && action.type !== 'tombstone'); + filteredActions = actions.filter(action => (action.type === filter || filter === 'all' || (filter === 'shop' && action.type === 'shopProduct')) && action.type !== 'tombstone'); } const larpEarliest = new Map(); @@ -2100,7 +1825,6 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { shop: { url: '/shops', filters: ['all', 'mine', 'recent'] }, job: { url: '/jobs', filters: ['ALL', 'MINE', 'REMOTE', 'PRESENCIAL', 'OPEN', 'CLOSED'] }, project: { url: '/projects', filters: ['all', 'mine', 'active', 'completed'] }, - industry:{ url: '/industry', filters: ['ALL', 'MINE', 'ACTIVE', 'PAUSED', 'DISSOLVED', 'BLUEPRINTS', 'BUILDS', 'MEMBER', 'RULES'] }, chat: { url: '/chats', filters: ['all', 'mine'] }, pad: { url: '/pads', filters: ['all', 'mine'] }, calendar:{ url: '/calendars', filters: ['all', 'mine'] }, @@ -2117,49 +1841,35 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { h2(i18n.activityList), p(desc) ), - div({ class: 'activity-filter-grid' }, - ...(() => { - const COLUMNS = [ - ['all', 'mine', 'recent', 'top'], - ['inhabitants', 'tribe', 'larp', 'parliament', 'courts'], - ['votes', 'event', 'calendar', 'task', 'report'], - ['banking', 'market', 'housing', 'project', 'industry', 'job', 'shop', 'transfer'], - ['post', 'feed', 'chat', 'pad', 'forum', 'map'], - ['audio', 'bookmark', 'document', 'image', 'torrent', 'video'] - ]; - const byType = new Map(activityTypes.map(t => [t.type, t])); - const placed = new Set(COLUMNS.flat()); - const leftovers = activityTypes.filter(t => !placed.has(t.type)); - return COLUMNS - .map(col => col.map(type => byType.get(type)).filter(Boolean)) - .concat(leftovers.length ? [leftovers] : []) - .filter(col => col.length); - })().map(col => - div({ class: 'activity-filter-col' }, - col.map(({ type, label }) => - form({ method: 'GET', action: '/activity' }, - input({ type: 'hidden', name: 'filter', value: type }), - button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label) - ) - ) - ) - ) - ), + renderHiveNav(), + (() => { + const labelOf = {}; + activityTypes.forEach(t => { labelOf[t.type] = t.label; }); + const mkBtn = (type) => form({ method: 'GET', action: '/activity' }, + input({ type: 'hidden', name: 'filter', value: type }), + button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, labelOf[type] || type) + ); + const QUICK = ['recent', 'all', 'mine']; + const isTypeFilter = filter && !QUICK.includes(filter) && labelOf[filter]; + if (isTypeFilter) { + return div({ class: 'activity-filter-collapsed' }, + a({ class: 'activity-back filter-btn', href: '/activity' }, + span({ class: 'activity-back-arrow' }, '‹'), + span(i18n.activityChangeFilter || 'Change filter') + ), + span({ class: 'activity-active-filter' }, labelOf[filter] || filter) + ); + } + return div({ class: 'activity-filter-acc' }, + div({ class: 'activity-quick-modes' }, QUICK.map(mkBtn)) + ); + })(), sub ? div({ class: 'activity-sub-filter' }, sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'filter-btn' }, String(f).toUpperCase())) ) : null, - div({ class: 'filters' }, - form({ method: 'GET', action: '/activity', class: 'filter-box' }, - input({ type: 'hidden', name: 'filter', value: filter }), - input({ type: 'text', name: 'q', value: qs, placeholder: i18n.activitySearchPlaceholder, class: 'filter-box__input' }), - div({ class: 'filter-box__controls' }, - button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) - ) - ) - ), - section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, actions, spreadMap, extras)) + section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, actions, spreadMap)) ) ); diff --git a/src/views/agenda_view.js b/src/views/agenda_view.js index bdfdc56..c13b8e2 100644 --- a/src/views/agenda_view.js +++ b/src/views/agenda_view.js @@ -1,4 +1,4 @@ -const { div, h2, p, section, button, form, img, input, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, img, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe"); const { template, i18n, userLink, renderContentActions } = require('./main_views'); const moment = require('../server/node_modules/moment'); const { config } = require('../server/SSB_server.js'); @@ -24,8 +24,6 @@ function getViewDetailsAction(item) { case 'report': return `/reports/${encodeURIComponent(item.id)}`; case 'job': return `/jobs/${encodeURIComponent(item.id)}`; case 'project': return `/projects/${encodeURIComponent(item.id)}`; - case 'industry': return `/industry/build/${encodeURIComponent(item.id)}`; - case 'housing': return `/housing/${encodeURIComponent(item.id)}`; case 'calendar': return `/calendars/${encodeURIComponent(item.id)}`; default: return `/messages/${encodeURIComponent(item.id)}`; } @@ -151,21 +149,6 @@ const renderAgendaItem = (item, userId, filter) => { ]; } - if (item.type === 'housing') { - const isOwner = String(item.author) === String(userId); - const requestCount = Number(item.requestCount) || 0; - details = [ - renderCardField(i18n.housingType + ":", (i18n["housingType" + String(item.housing_type || '').toUpperCase()] || item.housing_type || '').toUpperCase()), - renderCardField(i18n.housingStatus + ":", String(item.status || '').toUpperCase() === 'CLOSED' ? i18n.housingStatusCLOSED : i18n.housingStatusOPEN), - item.place ? renderCardField(i18n.housingPlace + ":", item.place) : null, - renderCardField(i18n.housingPrice + ":", String(item.housing_type) === 'couchsurfing' ? (i18n.housingFree || 'FREE') : `${item.price} ECO`), - item.availableFrom ? renderCardField(i18n.housingAvailableFrom + ":", moment(item.availableFrom).format('YYYY/MM/DD')) : null, - isOwner - ? renderCardField(i18n.housingRequests + ":", String(requestCount)) - : renderCardField(i18n.housingRequests + ":", i18n.housingRequestedBadge || 'REQUESTED') - ].filter(Boolean); - } - if (item.type === 'job') { const subs = Array.isArray(item.subscribers) ? item.subscribers @@ -202,14 +185,6 @@ const renderAgendaItem = (item, userId, filter) => { } } - if (item.type === 'industry') { - const st = String(item.status || 'PROPOSED').toUpperCase(); - details = [ - renderCardField((i18n.industryFacility || 'Facility') + ":", item.facilityName || ''), - renderCardField((i18n.industryStatusLabel || 'Status') + ":", i18n['industryBuildStatus_' + st] || st) - ]; - } - const isOwn = author && String(author) === String(userId); return div({ class: 'trending-card agenda-card' + (isOwn ? ' own-content' : '') }, div({ class: 'card-header activity-card-header' }, @@ -228,9 +203,9 @@ const renderAgendaItem = (item, userId, filter) => { ); }; -exports.agendaView = async (data, filter, q = '') => { +exports.agendaView = async (data, filter) => { const { items = [], counts: _c = {} } = data || {}; - const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, housing: 0, discarded: 0, ..._c }; + const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, discarded: 0, ..._c }; return template( i18n.agendaTitle, section( @@ -266,10 +241,6 @@ exports.agendaView = async (data, filter, q = '') => { `${i18n.agendaFilterMarket} (${counts.market})`), button({ type: 'submit', name: 'filter', value: 'projects', class: filter === 'projects' ? 'filter-btn active' : 'filter-btn' }, `${i18n.agendaFilterProjects} (${counts.projects})`), - button({ type: 'submit', name: 'filter', value: 'industry', class: filter === 'industry' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterIndustry || 'INDUSTRY'} (${counts.industry || 0})`), - button({ type: 'submit', name: 'filter', value: 'housing', class: filter === 'housing' ? 'filter-btn active' : 'filter-btn' }, - `${i18n.agendaFilterHousing || 'HOUSING'} (${counts.housing || 0})`), button({ type: 'submit', name: 'filter', value: 'calendars', class: filter === 'calendars' ? 'filter-btn active' : 'filter-btn' }, `${i18n.agendaFilterCalendars || 'CALENDARS'} (${counts.calendars})`), button({ type: 'submit', name: 'filter', value: 'transfers', class: filter === 'transfers' ? 'filter-btn active' : 'filter-btn' }, @@ -278,15 +249,6 @@ exports.agendaView = async (data, filter, q = '') => { `DISCARDED (${counts.discarded})`) ) ), - div({ class: 'filters' }, - form({ method: 'GET', action: '/agenda', class: 'filter-box' }, - input({ type: 'hidden', name: 'filter', value: filter }), - input({ type: 'text', name: 'q', value: q, placeholder: i18n.agendaSearchPlaceholder, class: 'filter-box__input' }), - div({ class: 'filter-box__controls' }, - button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) - ) - ) - ), div({ class: 'agenda-list' }, items.length ? items.map(item => renderAgendaItem(item, userId, filter)) diff --git a/src/views/audio_view.js b/src/views/audio_view.js index 5502f7a..7b4f57c 100644 --- a/src/views/audio_view.js +++ b/src/views/audio_view.js @@ -15,9 +15,8 @@ const { label, option } = require("../server/node_modules/hyperaxe"); -const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderUrl } = require("../backend/renderUrl") @@ -48,6 +47,21 @@ const renderTags = (tags) => { : null; }; +const renderAudioFavoriteToggle = (audioObj, returnTo = "") => + form( + { + method: "POST", + action: audioObj.isFavorite + ? `/audios/favorites/remove/${encodeURIComponent(audioObj.key)}` + : `/audios/favorites/add/${encodeURIComponent(audioObj.key)}` + }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + button( + { type: "submit", class: "filter-btn" }, + audioObj.isFavorite ? i18n.audioRemoveFavoriteButton : i18n.audioAddFavoriteButton + ) + ); + const renderTranscodeButton = (audioObj) => audioObj.isBcs ? form( @@ -94,11 +108,66 @@ const renderAudioOwnerActions = (filter, audioObj, params = {}) => { }; const renderAudioCommentsSection = (audioId, comments = [], returnTo = null) => { - return renderSharedCommentsSection({ - action: `/audios/${encodeURIComponent(audioId)}/comments`, - comments: comments, - returnTo: returnTo + const list = safeArr(comments).filter(c => { + const t = c && c.value && c.value.content && c.value.content.text; + return t && String(t).trim(); }); + const commentsCount = list.length; + + return div( + { class: "vote-comments-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), + span({ class: "card-value" }, String(commentsCount)) + ), + div( + { class: "comment-form-wrapper" }, + h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), + form( + { method: "POST", action: `/audios/${encodeURIComponent(audioId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + textarea({ + id: "comment-text", + name: "text", + rows: 4, + class: "comment-textarea", + placeholder: i18n.voteNewCommentPlaceholder + }), + div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), + br(), + button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) + ) + ), + list.length + ? div( + { class: "comments-list" }, + list.map((c) => { + const author = c?.value?.author || ""; + const ts = c?.value?.timestamp || c?.timestamp; + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; + const relDate = ts ? moment(ts).fromNow() : ""; + const content = c?.value?.content || {}; + const rootId = content.fork || content.root || null; + const text = content.text || ""; + + return div( + { class: "votations-comment-card" }, + span( + { class: "created-at" }, + span(i18n.createdBy), + author ? userLink(author) : span("(unknown)"), + absDate ? span(" | ") : "", + absDate ? span({ class: "votations-comment-date" }, absDate) : "", + relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", + relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" + ), + p({ class: "votations-comment-text" }, ...renderUrl(text)) + ); + }) + ) + : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) + ); }; const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) => { @@ -108,6 +177,7 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) ? audios.map((audioObj) => { const commentCount = typeof audioObj.commentCount === "number" ? audioObj.commentCount : 0; const title = safeText(audioObj.title); + const ownerActions = renderAudioOwnerActions(filter, audioObj, params); const isOwn = audioObj.author && String(audioObj.author) === String(userId); return div( @@ -115,17 +185,55 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) div( { class: "card-header activity-card-header" }, span(), - renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(audioObj.key)) || params.spreads || null, author: audioObj.author, favKind: 'audios', isFavorite: audioObj.isFavorite, reportTitle: audioObj.title }) + renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`) ), div( { class: "card-section audio-card-body" }, + div( + { class: "bookmark-topbar" }, + div( + { class: "bookmark-topbar-left" }, + renderAudioFavoriteToggle(audioObj, returnTo), + audioObj.author && String(audioObj.author) !== String(userId) + ? form( + { method: "GET", action: "/pm" }, + input({ type: "hidden", name: "recipients", value: audioObj.author }), + button({ type: "submit", class: "filter-btn" }, i18n.audioMessageAuthorButton) + ) + : null + ), + ownerActions.length + ? div({ class: "bookmark-topbar-right" }, div({ class: "bookmark-actions" }, ...ownerActions)) + : null + ), title ? h2(title) : null, audioObj.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(audioObj.lifetime, i18n)) : null, renderAudioPlayer(audioObj), - renderEngagement(audioObj.key, - renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), - renderCommentsLink({ href: `/audios/${encodeURIComponent(audioObj.key)}`, count: commentCount }) + div( + { class: "card-comments-summary" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), + span({ class: "card-value" }, String(commentCount)), + br(), + br(), + form( + { method: "GET", action: `/audios/${encodeURIComponent(audioObj.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "filter", value: filter || "all" }), + params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, + params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, + button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) + ) ), + div( + { class: "card-comments-summary feed-opinions-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), + span({ class: "card-value" }, String(Object.values(audioObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) + ), + renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants) + ), + div({ class: "card-spread-left" }, renderSpreadButton(audioObj.key, (params.spreadMap && params.spreadMap.get(audioObj.key)) || params.spreads)), renderMapLocationVisitLabel(audioObj.mapUrl), br(), (() => { @@ -135,12 +243,12 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), userLink(audioObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}` + ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` ) : null ); @@ -155,7 +263,6 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => { const returnTo = safeText(params.returnTo) || buildReturnTo("all", params); return div( { class: "div-center audio-form" }, - params.spreadWarning || null, form( { action: filter === "edit" ? `/audios/update/${encodeURIComponent(audioId)}` : "/audios/create", @@ -170,7 +277,7 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => { br(), span(i18n.audioTitleLabel), br(), - input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.audioTitlePlaceholder, value: audioToEdit?.title || "" }), + input({ type: "text", name: "title", placeholder: i18n.audioTitlePlaceholder, value: audioToEdit?.title || "" }), br(), span(i18n.audioDescriptionLabel), br(), @@ -196,8 +303,20 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => { }; exports.audioView = async (audios, filter = "all", audioId = null, params = {}) => { - if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(audioId) }; - const title = i18n.audioTitle; + const title = + filter === "mine" + ? i18n.audioMineSectionTitle + : filter === "create" + ? i18n.audioCreateSectionTitle + : filter === "edit" + ? i18n.audioUpdateSectionTitle + : filter === "recent" + ? i18n.audioRecentSectionTitle + : filter === "top" + ? i18n.audioTopSectionTitle + : filter === "favorites" + ? i18n.audioFavoritesSectionTitle + : i18n.audioAllSectionTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); @@ -224,15 +343,15 @@ exports.audioView = async (audios, filter = "all", audioId = null, params = {}) { method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterMine).toUpperCase()), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase()), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterAll), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterMine), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterRecent), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterTop), + button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"), button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - String(i18n.audioFilterFavorites).toUpperCase() + i18n.audioFilterFavorites ), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterTop).toUpperCase()), - button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.audioCreateButton) ) ) @@ -257,9 +376,9 @@ exports.audioView = async (audios, filter = "all", audioId = null, params = {}) { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.audioSortRecent), - option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.audioSortOldest), - option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.audioSortTop) + option({ value: "recent", selected: sort === "recent" }, i18n.audioSortRecent), + option({ value: "oldest", selected: sort === "oldest" }, i18n.audioSortOldest), + option({ value: "top", selected: sort === "top" }, i18n.audioSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.audioSearchButton) ) @@ -288,6 +407,7 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params const ownerActions = renderAudioOwnerActions(filter, audioObj, { q, sort }); const sideActions = []; + sideActions.push(renderAudioFavoriteToggle(audioObj, returnTo)); if (audioObj.author && String(audioObj.author) !== String(userId)) { sideActions.push(form( { method: "GET", action: "/pm" }, @@ -305,17 +425,6 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params const tagsNode = renderTags(audioObj.tags); - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(audioObj.key, null, { - author: audioObj.author, - favKind: 'audios', - isFavorite: audioObj.isFavorite, - spread: params.spreads || null, - returnTo, - reportTitle: audioObj.title - }) - ); - const audioSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, @@ -326,12 +435,12 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params ? p({ class: "tribe-side-description" }, ...renderUrl(audioObj.description)) : null, tagsNode, + div({ class: "card-spread-centered" }, renderSpreadButton(audioObj.key, params.spreads)), renderMapLocationVisitLabel(audioObj.mapUrl), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const audioMain = div({ class: "tribe-main" }, - detailActions, renderAudioPlayer(audioObj), (() => { const createdTs = audioObj.createdAt ? new Date(audioObj.createdAt).getTime() : NaN; @@ -340,20 +449,18 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), userLink(audioObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}` + ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` ) : null ); })(), - renderEngagement(audioObj.key, - renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), - renderAudioCommentsSection(audioObj.key, comments, returnTo) - ) + renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), + renderAudioCommentsSection(audioObj.key, comments, returnTo) ); return template( @@ -369,15 +476,15 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params { method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterMine).toUpperCase()), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase()), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterAll), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterMine), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterRecent), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterTop), + button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"), button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - String(i18n.audioFilterFavorites).toUpperCase() + i18n.audioFilterFavorites ), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterTop).toUpperCase()), - button({ type: "submit", name: "filter", value: "bcs", class: filter === "bcs" ? "filter-btn active" : "filter-btn" }, i18n.audioFilterBcs || "BCS"), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.audioCreateButton) ) ), @@ -392,7 +499,7 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload const title = i18n.audioTranscodeDetailTitle || "Transcode"; const composition = Array.isArray(audio.bcsComposition) ? audio.bcsComposition : []; const hasStego = decoded && stegoPayload && (stegoPayload.id || stegoPayload.ts || stegoPayload.msg); - const stegoDate = hasStego && Number.isFinite(stegoPayload.ts) ? moment(stegoPayload.ts).format("YYYY/MM/DD HH:mm") : null; + const stegoDate = hasStego && Number.isFinite(stegoPayload.ts) ? moment(stegoPayload.ts).format("YYYY/MM/DD HH:mm:ss") : null; return template( title, @@ -413,7 +520,7 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload p({ class: "transcode-meta card-footer" }, userLink(audio.author), span({ class: "melody-meta-sep" }, " · "), - span({ class: "card-value" }, moment(audio.createdAt).format("YYYY/MM/DD HH:mm")), + span({ class: "card-value" }, moment(audio.createdAt).format("YYYY/MM/DD HH:mm:ss")), itemSize ? span({ class: "melody-meta-sep" }, " · ") : null, itemSize ? renderEcoTax(itemSize, audio.key) : null ), diff --git a/src/views/banking_views.js b/src/views/banking_views.js index 69f9ffc..5928d3e 100644 --- a/src/views/banking_views.js +++ b/src/views/banking_views.js @@ -297,8 +297,7 @@ const renderTaxes = (data, lookup) => { ); }; -const renderOverviewSummaryTable = (s, rules, userEcoinTax, isPub = false) => { - const ubiWired = !!s.pubId || isPub; +const renderOverviewSummaryTable = (s, rules, userEcoinTax) => { const score = Number(s.userEngagementScore || 0); const pool = Number(s.pool || 0); const W = Math.max(1, Number(s.weightsSum || 1)); @@ -315,17 +314,14 @@ const renderOverviewSummaryTable = (s, rules, userEcoinTax, isPub = false) => { table({ class: "bank-info-table" }, tbody( kvRow(i18n.bankUserBalance, `${Number(s.userBalance || 0).toFixed(6)} ECO`), - kvRow(i18n.bankIndustryBalance || 'Industry Production (estimated)', a({ href: '/industry' }, `${Number(s.industryNetworkTotal || 0).toFixed(6)} ECO`)), - ubiWired ? kvRow(i18n.bankUbiAvailability, span({ class: availClass }, availLabel)) : null, + kvRow(i18n.bankUbiAvailability, span({ class: availClass }, availLabel)), s.pubId ? kvRow(i18n.pubIdLabel, userLink(s.pubId)) : null, kvRow(i18n.bankEpoch, String(s.epochId || "-")), kvRow(i18n.bankPool, `${pool.toFixed(6)} ECO`), kvRow(i18n.bankWeightsSum, String(W.toFixed(6))), kvRow(i18n.bankingUserEngagementScore, String(score)), - ubiWired ? kvRow(i18n.bankYourUbiMonth || 'Your UBI (this month)', `${future.toFixed(6)} ECO`) : null, - kvRow(i18n.bankYourIndustryBalance || 'Your Industry Share', a({ href: '/industry?filter=MEMBER' }, `${Number(s.industryBalance || 0).toFixed(6)} ECO`)), kvRow(i18n.bankOverviewYourTaxes || 'Your taxes', a({ href: '/banking?filter=taxes' }, `${tax.toFixed(6)} ECO`)), - kvRow(i18n.bankYourFundsMonth || 'Your Funds (this month)', `${((ubiWired ? future : 0) + Number(s.industryBalance || 0) - tax).toFixed(6)} ECO`) + kvRow(i18n.bankUbiThisMonth, `${future.toFixed(6)} ECO`) ) ) ); @@ -622,7 +618,7 @@ const renderBankingView = (data, filter, userId, isPub) => generateFilterButtons(["overview","exchange","taxes","mine","pending","closed","claimed","expired","epochs","rules","addresses"], filter, "/banking"), filter === "overview" ? div( - renderOverviewSummaryTable(data.summary || {}, data.rules, data.userTotalTax || data.userEcoinTax, isPub), + renderOverviewSummaryTable(data.summary || {}, data.rules, data.userTotalTax || data.userEcoinTax), renderClaimUBIBlock(data.pendingUBI || null, isPub, data.alreadyClaimed, (data.summary || {}).pubId, (data.summary || {}).hasValidWallet, (data.summary || {}).ubiAvailability), allocationsTable((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), userId) ) diff --git a/src/views/blockchain_view.js b/src/views/blockchain_view.js index f39792c..a70d752 100644 --- a/src/views/blockchain_view.js +++ b/src/views/blockchain_view.js @@ -9,8 +9,8 @@ const FILTER_LABELS = { feed: i18n.typeFeed, event: i18n.typeEvent, task: i18n.typeTask, report: i18n.typeReport, image: i18n.typeImage, audio: i18n.typeAudio, video: i18n.typeVideo, post: i18n.typePost, forum: i18n.typeForum, about: i18n.typeAbout, contact: i18n.typeContact, pub: i18n.typePub, - transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, housing: i18n.typeHousing, tribe: i18n.typeTribe, - project: i18n.typeProject, industry: i18n.typeIndustry, industryBlueprint: i18n.industryBlueprints, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim, + transfer: i18n.typeTransfer, market: i18n.typeMarket, job: i18n.typeJob, tribe: i18n.typeTribe, + project: i18n.typeProject, banking: i18n.typeBanking, bankWallet: i18n.typeBankWallet, bankClaim: i18n.typeBankClaim, aiExchange: i18n.typeAiExchange, parliament: i18n.typeParliament, courts: i18n.typeCourts, map: i18n.typeMap, shop: i18n.typeShop, shopProduct: i18n.typeShopProduct || 'Shop Product', pad: i18n.typePad || 'PAD', chat: i18n.typeChat || 'CHAT', gameScore: i18n.typeGameScore || 'GAME SCORE', @@ -20,7 +20,7 @@ const FILTER_LABELS = { const BASE_FILTERS = ['recent', 'all', 'mine', 'tombstone', 'logs']; const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'calendar', 'parliament', 'courts']; const CAT_BLOCK2 = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange']; -const CAT_BLOCK3 = ['banking', 'job', 'housing', 'market', 'project', 'industry', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore']; +const CAT_BLOCK3 = ['banking', 'job', 'market', 'project', 'transfer', 'feed', 'post', 'pixelia', 'shop', 'gameScore']; const CAT_BLOCK4 = ['forum', 'pad', 'chat', 'bookmark', 'image', 'video', 'audio', 'document', 'map', 'torrent']; const SEARCH_FIELDS = ['author','id','from','to']; @@ -100,7 +100,7 @@ const computeStats = (blocks) => { const renderStatsPanel = (stats, currentFilter, search) => { if (!stats || stats.total === 0) return null; - const topTypes = stats.typeBreakdown.filter(t => t.type !== 'industryBlueprint').slice(0, 10); + const topTypes = stats.typeBreakdown.slice(0, 10); return div({ class: 'blockchain-stats' }, div({ class: 'tags-header' }, h3(`${i18n.blockchainStatsTitle || 'Stats'} (${stats.total})`) @@ -179,10 +179,7 @@ const getViewDetailsAction = (type, block) => { case 'pub': return `/invites`; case 'market': return `/market/${encodeURIComponent(block.id)}`; case 'job': return `/jobs/${encodeURIComponent(block.id)}`; - case 'housing': return `/housing/${encodeURIComponent(block.id)}`; case 'project': return `/projects/${encodeURIComponent(block.id)}`; - case 'industry': return `/industry/${encodeURIComponent(block.id)}`; - case 'industryBlueprint': return `/industry/blueprint/${encodeURIComponent(block.id)}`; case 'report': return `/reports/${encodeURIComponent(block.id)}`; case 'calendar': return `/calendars/${encodeURIComponent(block.id)}`; case 'bankWallet': return `/wallet`; @@ -210,7 +207,6 @@ const getViewDetailsAction = (type, block) => { case 'chat': return `/chats/${encodeURIComponent(block.id)}`; case 'gameScore': return `/games?filter=scoring`; case 'log': return `/logs/view/${encodeURIComponent(block.id)}`; - case 'poll': return `/polls/${encodeURIComponent(block.id)}`; case 'calendarDate': case 'calendarNote': return block.content?.calendarId ? `/calendars/${encodeURIComponent(block.content.calendarId)}` : `/calendars`; case 'padEntry': return block.content?.padId ? `/pads/${encodeURIComponent(block.content.padId)}` : `/pads`; @@ -223,7 +219,7 @@ const TYPE_COLORS = { post:'#3498db', vote:'#9b59b6', votes:'#9b59b6', about:'#1abc9c', contact:'#16a085', pub:'#2ecc71', tribe:'#e67e22', event:'#e74c3c', task:'#f39c12', report:'#c0392b', image:'#2980b9', audio:'#8e44ad', video:'#d35400', document:'#27ae60', bookmark:'#f1c40f', - forum:'#1abc9c', feed:'#95a5a6', transfer:'#e74c3c', market:'#e67e22', job:'#3498db', housing:'#16a085', + forum:'#1abc9c', feed:'#95a5a6', transfer:'#e74c3c', market:'#e67e22', job:'#3498db', project:'#2ecc71', banking:'#f39c12', bankWallet:'#f39c12', bankClaim:'#f39c12', pixelia:'#9b59b6', curriculum:'#1abc9c', aiExchange:'#3498db', tombstone:'#7f8c8d', parliamentTerm:'#8e44ad', parliamentProposal:'#8e44ad', parliamentLaw:'#8e44ad', @@ -418,7 +414,9 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi button({ type:'submit', class:'filter-btn' }, `← ${i18n.blockchainBack}`) ), !block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ? - a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, '↗') + form({ method:'GET', action:getViewDetailsAction(block.type, block) }, + button({ type:'submit', class:'filter-btn' }, i18n.visitContent) + ) : (block.isTombstoned || block.isReplaced) ? div({ class: 'deleted-label' }, i18n.blockchainContentDeleted || "This content has been deleted." @@ -474,7 +472,7 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) input({ type: 'datetime-local', name: 'to', value: toVal, class: 'blockexplorer-search-input' }) ), div({ class: 'blockexplorer-search-actions' }, - div({ class: 'filter-box__controls' }, button({ type: 'submit', class: 'filter-box__button' }, i18n.searchSubmit)) + button({ type: 'submit', class: 'filter-box__button' }, i18n.searchSubmit) ) ) ) @@ -526,9 +524,11 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) div({ class:'block' }, div({ class:'block-buttons' }, block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, '⦿'), - block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram }, '⊞'), + block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram || 'Datagram' }, '⊞'), !block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ? - a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, '↗') + form({ method:'GET', action:getViewDetailsAction(block.type, block) }, + button({ type:'submit', class:'filter-btn' }, i18n.visitContent) + ) : (block.isTombstoned || block.isReplaced) ? div({ class: 'deleted-label' }, i18n.blockchainContentDeleted || "This content has been deleted." diff --git a/src/views/blog_view.js b/src/views/blog_view.js deleted file mode 100644 index fb6401c..0000000 --- a/src/views/blog_view.js +++ /dev/null @@ -1,179 +0,0 @@ -const { div, h2, p, section, button, form, a, input, label, span, textarea, br, table, tr, td } = require("../server/node_modules/hyperaxe"); -const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { template, i18n, userLink, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderContentActions } = require("./main_views"); -const moment = require("../server/node_modules/moment"); -const { config } = require("../server/SSB_server.js"); -const { renderUrl } = require("../backend/renderUrl"); -const { renderTextWithStyles } = require("../backend/renderTextWithStyles"); -const { sanitizeHtml } = require("../backend/sanitizeHtml"); - -const userId = config.keys.id; - -const FILTERS = [ - { key: "ALL", i18n: "blogFilterAll" }, - { key: "MINE", i18n: "blogFilterMine" }, - { key: "RECENT", i18n: "blogFilterRecent" }, - { key: "FAVORITES", i18n: "blogFilterFavorites" }, - { key: "TOP", i18n: "blogFilterTop" } -]; - -const safeText = (v) => String(v == null ? "" : v).trim(); - -const excerpt = (text, max = 420) => { - const s = safeText(text); - return s.length > max ? s.slice(0, max) + "…" : s; -}; - -const renderFilterBar = (filter, q, showSearch = true) => - section( - div({ class: "filters" }, - form({ method: "GET", action: "/blogs", class: "ui-toolbar ui-toolbar--filters" }, - ...FILTERS.map(f => - button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) - ), - button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.blogCreateButton) - ) - ), - showSearch - ? div({ class: "filters" }, - form({ method: "GET", action: "/blogs", class: "filter-box" }, - input({ type: "hidden", name: "filter", value: filter }), - input({ type: "text", name: "q", value: q || "", placeholder: i18n.blogSearchPlaceholder, class: "filter-box__input" }), - div({ class: "filter-box__controls" }, - button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) - ) - ) - ) - : null - ); - -const renderBlogCard = (blog, filter, spreadInfo) => { - const href = `/blogs/${encodeURIComponent(blog.id)}`; - const isOwn = String(blog.author) === String(userId); - return div({ class: "trending-card blog-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, - renderContentActions(blog.id, href, { spread: spreadInfo || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text }) - ), - div({ class: "card-section blog-card-body" }, - blog.subject - ? div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, a({ href }, blog.subject)) - ) - : null, - div({ class: "blog-card-text", innerHTML: sanitizeHtml(renderTextWithStyles(excerpt(blog.text))) }), - p({ class: "card-footer" }, - span({ class: "date-link" }, moment(blog.createdAt).format("YYYY/MM/DD HH:mm")), - span(" · "), - userLink(blog.author) - ) - ) - ); -}; - -const renderCreateForm = () => - section( - div({ class: "publish-form" }, - form({ action: "/blogs/create", method: "POST", enctype: "multipart/form-data" }, - label({ for: "subject" }, i18n.blogSubject), - br(), - input({ type: "text", name: "subject", id: "subject", maxlength: "150", placeholder: i18n.contentWarningPlaceholder }), - br(), - br(), - label({ for: "text" }, i18n.blogMessage), - br(), - textarea({ required: true, name: "text", id: "text", rows: "8", class: "publish-textarea", maxlength: "7000", placeholder: i18n.publishWarningPlaceholder }), - br(), - br(), - label({ for: "blob" }, i18n.blogMedia), - br(), - input({ type: "file", id: "blob", name: "blob", multiple: true, accept: "image/*,video/*,audio/*,application/pdf" }), - br(), - br(), - div({ class: "blog-allow-comments" }, - input({ type: "hidden", name: "allowComments", value: "0" }), - label( - input({ type: "checkbox", name: "allowComments", value: "1", checked: true }), - " ", - i18n.blogAllowComments - ) - ), - br(), - button({ type: "submit" }, i18n.blogPublish) - ) - ) - ); - -const renderCommentsSection = (blog, comments = []) => { - const href = `/blogs/${encodeURIComponent(blog.id)}`; - return renderSharedCommentsSection({ - action: `${href}/comments`, - comments: comments, - returnTo: href, - closedNote: blog.allowComments ? null : i18n.blogCommentsClosed - }); -}; - -exports.blogView = async (blogs = [], filter = "ALL", params = {}) => { - const showForm = String(filter).toUpperCase() === "CREATE"; - const spreadMap = params.spreadMap instanceof Map ? params.spreadMap : new Map(); - return template( - i18n.blogTitle, - section( - div({ class: "tags-header" }, - h2(i18n.blogTitle), - p(i18n.blogDescription) - ) - ), - renderFilterBar(showForm ? "ALL" : filter, params.q, !showForm), - showForm - ? renderCreateForm() - : section( - blogs.length - ? div({ class: "jobs-grid" }, ...blogs.map(b => renderBlogCard(b, filter, spreadMap.get(b.id)))) - : p({ class: "no-content" }, i18n.blogNoItems) - ) - ); -}; - -exports.singleBlogView = async (blog, comments = [], params = {}) => { - const href = `/blogs/${encodeURIComponent(blog.id)}`; - const isAuthor = String(blog.author) === String(userId); - - const blogSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(blog.id, null, { spread: params.spreads || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text }) - ), - div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, blog.subject || i18n.blogTitle) - ), - table({ class: "tribe-info-table jobs-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), - td({ class: "tribe-info-value", colspan: "3" }, moment(blog.createdAt).format("YYYY/MM/DD HH:mm")) - ), - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(blog.author)) - ) - ), - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${i18n.blogComments}: ${blog.commentCount || 0}`) - ), - ); - - const blogMain = div({ class: "tribe-main" }, - div({ class: "job-section" }, - div({ class: "blog-detail-text", innerHTML: sanitizeHtml(renderTextWithStyles(blog.text || "")) }) - ), - renderEngagement(blog.id, - renderOpinionsVoting('/blogs/opinions', blog.id, blog.opinions, href, blog.opinions_inhabitants), - renderCommentsSection(blog, comments) - ) - ); - - return template( - blog.subject || i18n.blogTitle, - section(div({ class: "tags-header" }, h2(i18n.blogTitle), p(i18n.blogDescription))), - renderFilterBar("ALL", params.q, false), - section(div({ class: "tribe-details" }, blogSide, blogMain)) - ); -}; diff --git a/src/views/bookmark_view.js b/src/views/bookmark_view.js index 671b4cf..24381ba 100644 --- a/src/views/bookmark_view.js +++ b/src/views/bookmark_view.js @@ -1,8 +1,7 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option } = require("../server/node_modules/hyperaxe"); -const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderUrl } = require("../backend/renderUrl"); @@ -59,11 +58,70 @@ const renderBookmarkActions = (filter, bookmark, params = {}) => { }; const renderBookmarkCommentsSection = (bookmarkId, rootId, comments = [], returnTo = null) => { - return renderSharedCommentsSection({ - action: `/bookmarks/${encodeURIComponent(bookmarkId)}/comments`, - comments: comments, - returnTo: returnTo + const list = safeArr(comments).filter(c => { + const t = c && c.value && c.value.content && c.value.content.text; + return t && String(t).trim(); }); + const commentsCount = list.length; + + return div( + { class: "vote-comments-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), + span({ class: "card-value" }, String(commentsCount)) + ), + div( + { class: "comment-form-wrapper" }, + h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), + form( + { method: "POST", action: `/bookmarks/${encodeURIComponent(bookmarkId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + rootId ? input({ type: "hidden", name: "rootId", value: rootId }) : null, + textarea({ + id: "comment-text", + name: "text", + rows: 4, + class: "comment-textarea", + placeholder: i18n.voteNewCommentPlaceholder + }), + div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), + br(), + button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) + ) + ), + list.length + ? div( + { class: "comments-list" }, + list.map((c) => { + const author = c?.value?.author || ""; + const ts = c?.value?.timestamp || c?.timestamp; + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; + const relDate = ts ? moment(ts).fromNow() : ""; + + const content = c?.value?.content || {}; + const text = content.text || ""; + const threadRoot = content.fork || content.root || null; + + return div( + { class: "votations-comment-card" }, + span( + { class: "created-at" }, + span(i18n.createdBy), + author ? userLink(author) : span("(unknown)"), + absDate ? span(" | ") : "", + absDate ? span({ class: "votations-comment-date" }, absDate) : "", + relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", + relDate && threadRoot + ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) + : "" + ), + p({ class: "votations-comment-text" }, ...renderUrl(text)) + ); + }) + ) + : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) + ); }; const renderCardField = (labelText, value) => @@ -73,6 +131,22 @@ const renderCardField = (labelText, value) => span({ class: "card-value" }, value) ); +const renderFavoriteToggle = (bookmark, returnTo) => + form( + { + method: "POST", + action: bookmark.isFavorite + ? `/bookmarks/favorites/remove/${encodeURIComponent(bookmark.id)}` + : `/bookmarks/favorites/add/${encodeURIComponent(bookmark.id)}`, + class: "bookmark-favorite-form" + }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + button( + { type: "submit", class: "filter-btn" }, + bookmark.isFavorite ? i18n.bookmarkRemoveFavoriteButton : i18n.bookmarkAddFavoriteButton + ) + ); + const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -93,7 +167,7 @@ const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { const lastVisit = bookmark.lastVisit ? moment(bookmark.lastVisit) : null; const lastVisitTxt = lastVisit && lastVisit.isValid() - ? `${lastVisit.format("YYYY/MM/DD HH:mm")} (${lastVisit.fromNow()})` + ? `${lastVisit.format("YYYY/MM/DD HH:mm:ss")} (${lastVisit.fromNow()})` : i18n.noLastVisit; const urlLink = bookmark.url @@ -106,19 +180,52 @@ const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { div( { class: "card-header activity-card-header" }, span(), - renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`, { spread: (params.spreadMap && params.spreadMap.get(bookmark.id)) || params.spreads || null, author: bookmark.author, favKind: 'bookmarks', isFavorite: bookmark.isFavorite, reportTitle: bookmark.title }) + renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`) ), div( { class: "card-section bookmark-card-body" }, - h2({ class: "bookmark-title" }, bookmark.url ? urlLink : (bookmark.title || "")), + div( + { class: "bookmark-topbar" }, + div( + { class: "bookmark-topbar-left" }, + renderPMButton(bookmark.author), + renderFavoriteToggle(bookmark, returnTo) + ), + div( + { class: "bookmark-topbar-right" }, + renderBookmarkActions(filter, bookmark, params) + ) + ), + h2({ class: "bookmark-title" }, bookmark.category || bookmark.url || ""), bookmark.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(bookmark.lifetime, i18n)) : null, - bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, bookmark.title) : null, + renderCardField(i18n.bookmarkUrlLabel + ":", urlLink), renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), br, - renderEngagement(bookmark.id, - renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), - renderCommentsLink({ href: `/bookmarks/${encodeURIComponent(bookmark.id)}`, count: commentCount }) + div( + { class: "card-comments-summary" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), + span({ class: "card-value" }, String(commentCount)), + br(), + br(), + form( + { method: "GET", action: `/bookmarks/${encodeURIComponent(bookmark.id)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "filter", value: filter || "all" }), + params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, + params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, + button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) + ) ), + div( + { class: "card-comments-summary feed-opinions-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), + span({ class: "card-value" }, String(Object.values(bookmark.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) + ), + renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants) + ), + div({ class: "card-spread-left" }, renderSpreadButton(bookmark.id, (params.spreadMap && params.spreadMap.get(bookmark.id)) || params.spreads)), (() => { const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; const updatedTs = bookmark.updatedAt ? new Date(bookmark.updatedAt).getTime() : NaN; @@ -126,12 +233,12 @@ const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), userLink(bookmark.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}` + ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` ) : null ); @@ -155,7 +262,6 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { return div( { class: "div-center bookmark-form" }, - params.spreadWarning || null, form( { action: filter === "edit" ? `/bookmarks/update/${encodeURIComponent(bookmarkId)}` : "/bookmarks/create", method: "POST" }, input({ type: "hidden", name: "returnTo", value: returnTo }), @@ -188,6 +294,16 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { }), br(), br(), + label(i18n.bookmarkCategoryLabel), + br(), + input({ + type: "text", + name: "category", + id: "category", + placeholder: i18n.bookmarkCategoryPlaceholder, + value: filter === "edit" ? bookmarkToEdit.category || "" : "" + }), + br(), label(i18n.bookmarkTagsLabel), br(), input({ @@ -205,8 +321,20 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { }; exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, params = {}) => { - const bookmarkEditWarning = filter === "edit" ? await renderSpreadEditWarning(bookmarkId) : null; - const title = i18n.bookmarkTitle; + const title = + filter === "mine" + ? i18n.bookmarkMineSectionTitle + : filter === "create" + ? i18n.bookmarkCreateSectionTitle + : filter === "edit" + ? i18n.bookmarkUpdateSectionTitle + : filter === "recent" + ? i18n.bookmarkRecentSectionTitle + : filter === "top" + ? i18n.bookmarkTopSectionTitle + : filter === "favorites" + ? i18n.bookmarkFavoritesSectionTitle + : i18n.bookmarkAllSectionTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); @@ -231,18 +359,18 @@ exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, para { method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterMine).toUpperCase()), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase()), - button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterFavorites).toUpperCase()), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterTop).toUpperCase()), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterAll), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterMine), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterTop), + button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterFavorites), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterRecent), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.bookmarkCreateButton) ) ) ), section( filter === "edit" || filter === "create" - ? renderBookmarkForm(filter, bookmarkId, bookmarkToEdit || {}, tags, { ...params, filter, spreadWarning: bookmarkEditWarning }) + ? renderBookmarkForm(filter, bookmarkId, bookmarkToEdit || {}, tags, { ...params, filter }) : section( div( { class: "bookmarks-search" }, @@ -254,9 +382,9 @@ exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, para { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.bookmarkSortRecent), - option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.bookmarkSortOldest), - option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.bookmarkSortTop) + option({ value: "recent", selected: sort === "recent" }, i18n.bookmarkSortRecent), + option({ value: "oldest", selected: sort === "oldest" }, i18n.bookmarkSortOldest), + option({ value: "top", selected: sort === "top" }, i18n.bookmarkSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.bookmarkSearchButton) ) @@ -281,7 +409,7 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par const lastVisit = bookmark.lastVisit ? moment(bookmark.lastVisit) : null; const lastVisitTxt = lastVisit && lastVisit.isValid() - ? `${lastVisit.format("YYYY/MM/DD HH:mm")} (${lastVisit.fromNow()})` + ? `${lastVisit.format("YYYY/MM/DD HH:mm:ss")} (${lastVisit.fromNow()})` : i18n.noLastVisit; const urlLink = bookmark.url @@ -294,7 +422,10 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par ].filter(Boolean); const sideActions = []; - sideActions.push(renderPMButton(bookmark.author)); + sideActions.push(renderFavoriteToggle(bookmark, returnTo)); + if (bookmark.author && String(bookmark.author) !== String(userId)) { + sideActions.push(renderPMButton(bookmark.author)); + } if (isAuthor && !hasOpinions) { sideActions.push(form( { method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` }, @@ -312,33 +443,25 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par const tagsNode = renderTags(bookmark.tags); - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(bookmark.id, null, { - author: bookmark.author, - favKind: 'bookmarks', - isFavorite: bookmark.isFavorite, - spread: params.spreads || null, - returnTo, - reportTitle: bookmark.title - }) - ); - const bookmarkSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, bookmark.url ? urlLink : (bookmark.title || "")), + h2({ class: "tribe-card-title" }, bookmark.category || bookmark.url || ""), renderReachChip(isClearnet, i18n) ), - bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, bookmark.title) : null, chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(bookmark.description) ? p({ class: "tribe-side-description" }, ...renderUrl(bookmark.description)) : null, + safeText(bookmark.category) + ? renderCardField(i18n.bookmarkCategoryLabel + ":", safeText(bookmark.category)) + : null, tagsNode, + div({ class: "card-spread-centered" }, renderSpreadButton(bookmark.id, params.spreads)), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const bookmarkMain = div({ class: "tribe-main" }, - detailActions, + renderCardField(i18n.bookmarkUrlLabel + ":", urlLink), renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), (() => { const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; @@ -346,20 +469,18 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), userLink(bookmark.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}` + ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` ) : null ); })(), - renderEngagement(bookmark.id, - renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), - renderBookmarkCommentsSection(bookmark.id, bookmark.rootId, comments, returnTo) - ) + renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), + renderBookmarkCommentsSection(bookmark.id, bookmark.rootId, comments, returnTo) ); return template( @@ -375,11 +496,11 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par { method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterMine).toUpperCase()), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase()), - button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterFavorites).toUpperCase()), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterTop).toUpperCase()), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterAll), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterMine), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterTop), + button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterFavorites), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterRecent), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.bookmarkCreateButton) ) ), diff --git a/src/views/calendars_view.js b/src/views/calendars_view.js index 30a5cc4..2ae1127 100644 --- a/src/views/calendars_view.js +++ b/src/views/calendars_view.js @@ -1,6 +1,5 @@ const { div, h2, h3, h4, p, section, button, form, a, span, br, textarea, input, label, select, option, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton , renderSpreadEditWarning, renderContentActions, renderDocumentActions } = require("./main_views") -const { renderMapLocationVisitLabel } = require("./maps_view") +const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views") const { renderEncryptedChip } = require("./clearnet_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") @@ -21,6 +20,13 @@ const renderNoteText = (text) => { return result } +const renderCalendarFavoriteToggle = (cal, returnTo) => + form( + { method: "POST", action: cal.isFavorite ? `/calendars/favorites/remove/${encodeURIComponent(cal.rootId)}` : `/calendars/favorites/add/${encodeURIComponent(cal.rootId)}` }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + button({ type: "submit", class: "tribe-action-btn" }, cal.isFavorite ? (i18n.calendarRemoveFavorite || "Remove Favorite") : (i18n.calendarAddFavorite || "Add Favorite")) + ) + const renderModeButtons = (currentFilter) => div({ class: "tribe-mode-buttons" }, ["all", "mine", "recent", "favorites", "open", "closed"].map(f => @@ -57,62 +63,62 @@ const renderCalendarCard = (cal, spreadInfo) => { renderLifespanChip(cal.lifetime, i18n) ].filter(Boolean) return div({ class: "tribe-card" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(cal.rootId, href, { spread: spreadInfo || null, author: cal.author, favKind: 'calendars', isFavorite: cal.isFavorite, reportTitle: cal.title }) - ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, cal.title || "\u2014")) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, cal.deadline - ? p({ class: "job-meta-line" }, `${i18n.calendarDeadlineLabel || "Deadline"}: ${moment(cal.deadline).format("YYYY/MM/DD HH:mm")}`) + ? p({ class: "job-meta-line" }, `${i18n.calendarDeadlineLabel || "Deadline"}: ${moment(cal.deadline).format("YYYY-MM-DD HH:mm")}`) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${cal.participants.length}`) + ), + div({ class: "card-spread-centered" }, renderSpreadButton(cal.rootId, spreadInfo)), + div({ class: "card-visit-btn-centered" }, + a({ href, class: "filter-btn" }, i18n.calendarVisitCalendar || "Visit Calendar") ) ) ) } -const renderIntervalBlock = (bounds = {}, current = "", until = "") => { - const sel = (v) => (String(current || "") === v ? { selected: true } : {}) - return div({ class: "calendar-interval-block" }, +const renderIntervalBlock = () => + div({ class: "calendar-interval-block" }, span({ class: "calendar-interval-label" }, i18n.calendarIntervalLabel || "Interval"), - select({ name: "interval", class: "calendar-interval-select" }, - option({ value: "", ...sel("") }, i18n.calendarIntervalNone), - option({ value: "weekly", ...sel("weekly") }, i18n.calendarIntervalWeekly || "Weekly"), - option({ value: "monthly", ...sel("monthly") }, i18n.calendarIntervalMonthly || "Monthly"), - option({ value: "yearly", ...sel("yearly") }, i18n.calendarIntervalYearly || "Yearly") + div({ class: "calendar-interval-row" }, + input({ type: "hidden", name: "intervalWeekly", value: "0" }), + label({ class: "calendar-interval-option" }, + input({ type: "checkbox", name: "intervalWeekly", value: "1" }), + " ", i18n.calendarIntervalWeekly || "Weekly" + ), + input({ type: "hidden", name: "intervalMonthly", value: "0" }), + label({ class: "calendar-interval-option" }, + input({ type: "checkbox", name: "intervalMonthly", value: "1" }), + " ", i18n.calendarIntervalMonthly || "Monthly" + ), + input({ type: "hidden", name: "intervalYearly", value: "0" }), + label({ class: "calendar-interval-option" }, + input({ type: "checkbox", name: "intervalYearly", value: "1" }), + " ", i18n.calendarIntervalYearly || "Yearly" + ) ), span({ class: "calendar-interval-label calendar-interval-until" }, i18n.calendarIntervalUntil || "Until"), - input({ - type: "datetime-local", - name: "intervalDeadline", - ...(until ? { value: until } : {}), - ...(bounds.min ? { min: bounds.min } : {}), - ...(bounds.max ? { max: bounds.max } : {}) - }), + input({ type: "datetime-local", name: "intervalDeadline" }), br() ) -} const renderCreateForm = (calendarToEdit, params) => { const isEdit = !!calendarToEdit const tribeId = (params && params.tribeId) || "" const now = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm") - const deadlineMax = calendarToEdit && calendarToEdit.deadline - ? moment(calendarToEdit.deadline).format("YYYY-MM-DDTHH:mm") - : "" const action = isEdit ? `/calendars/update/${encodeURIComponent(calendarToEdit.rootId)}` : "/calendars/create" const sectionTitle = isEdit ? (i18n.calendarUpdateSectionTitle || "Update Calendar") : (i18n.calendarCreateSectionTitle || "Create New Calendar") return div({ class: "div-center audio-form" }, h2(sectionTitle), - (params && params.spreadWarning) || null, form({ method: "POST", action }, tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, span(i18n.calendarTitleLabel || "Title"), br(), - input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.calendarTitlePlaceholder || "Calendar title...", value: calendarToEdit ? calendarToEdit.title : "" }), + input({ type: "text", name: "title", required: true, placeholder: i18n.calendarTitlePlaceholder || "Calendar title...", value: calendarToEdit ? calendarToEdit.title : "" }), br(), br(), span(i18n.calendarStatusLabel || "Status"), br(), select({ name: "status", required: true }, @@ -126,21 +132,17 @@ const renderCreateForm = (calendarToEdit, params) => { span(i18n.calendarTagsLabel || "Tags"), br(), input({ type: "text", name: "tags", placeholder: i18n.calendarTagsPlaceholder || "tag1, tag2...", value: calendarToEdit && Array.isArray(calendarToEdit.tags) ? calendarToEdit.tags.join(", ") : "" }), br(), br(), - span(i18n.mapLocationTitle || "Map Location"), br(), - input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: (calendarToEdit && calendarToEdit.mapUrl) || "" }), - br(), br(), !isEdit ? [ span(i18n.calendarFirstDateLabel || "Date"), br(), - input({ type: "datetime-local", name: "firstDate", required: true, min: now, max: deadlineMax || undefined }), + input({ type: "datetime-local", name: "firstDate", required: true, min: now }), br(), br(), span(i18n.calendarFormDescription || "Description"), br(), input({ type: "text", name: "firstDateLabel", placeholder: i18n.calendarDatePlaceholder || "Describe this date..." }), br(), br(), + renderIntervalBlock(), span(i18n.calendarFirstNoteLabel || "Notes"), br(), textarea({ name: "firstNote", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }), - br(), br(), - renderIntervalBlock({ min: now, max: deadlineMax || undefined }), br(), br() ] : null, @@ -190,10 +192,17 @@ exports.renderCalendarInvitePage = (code) => { } exports.calendarsView = async (calendars, filter, calendarToEdit, params) => { - if (calendarToEdit) params = { ...(params || {}), spreadWarning: await renderSpreadEditWarning(calendarToEdit.id || calendarToEdit.key || calendarToEdit.rootId) }; const q = (params && params.q) || "" const showForm = filter === "create" || filter === "edit" || !!calendarToEdit - const headerText = i18n.calendarsTitle || "Calendars" + const headerMap = { + all: i18n.calendarAllSectionTitle || "Calendars", + mine: i18n.calendarMineSectionTitle || "Your Calendars", + recent: i18n.calendarRecentSectionTitle || "Recent Calendars", + favorites: i18n.calendarFavoritesSectionTitle || "Favorites", + open: i18n.calendarOpenSectionTitle || "Open Calendars", + closed: i18n.calendarClosedSectionTitle || "Closed Calendars" + } + const headerText = headerMap[filter] || headerMap.all return template( i18n.calendarsTitle || "Calendars", @@ -203,17 +212,14 @@ exports.calendarsView = async (calendars, filter, calendarToEdit, params) => { p(i18n.calendarsDescription || "Discover and manage calendars.") ), renderModeButtons(filter), - showForm - ? null - : div({ class: "filters" }, - form({ method: "GET", action: "/calendars", class: "filter-box" }, - input({ type: "hidden", name: "filter", value: filter }), - input({ type: "text", name: "q", value: q, placeholder: i18n.calendarSearchPlaceholder || "Search calendars...", class: "filter-box__input" }), - div({ class: "filter-box__controls" }, - button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) - ) + q + ? div({ class: "filters" }, + form({ method: "GET", action: "/calendars" }, + input({ type: "text", name: "q", value: q, placeholder: i18n.calendarSearchPlaceholder || "Search calendars..." }), + button({ type: "submit", class: "filter-btn" }, i18n.searchButton || "Search") ) ) + : null ), section( showForm @@ -260,17 +266,26 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { h2({ class: "tribe-card-title" }, calendar.title || "\u2014") ), detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null, + div({ class: "card-spread-centered" }, renderSpreadButton(calendar.rootId, params && params.spreads)), + div({ class: "shop-share" }, + span({ class: "tribe-info-label" }, i18n.calendarsShareUrl || "Share URL"), + input({ type: "text", readonly: true, value: shareUrl, class: "shop-share-input" }) + ), table({ class: "tribe-info-table" }, - tr(td({ class: "tribe-info-label" }, i18n.calendarCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.createdAt).format("YYYY/MM/DD HH:mm"))), - calendar.deadline ? tr(td({ class: "tribe-info-label" }, i18n.calendarDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.deadline).format("YYYY/MM/DD HH:mm"))) : null, + tr(td({ class: "tribe-info-label" }, i18n.calendarCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.createdAt).format("YYYY-MM-DD"))), tr(td({ class: "tribe-info-label" }, i18n.calendarStatusLabel || "Status"), td({ class: "tribe-info-value", colspan: "3" }, renderStatus(calendar))), - tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(calendar.author))) + tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(calendar.author))), + calendar.deadline ? tr(td({ class: "tribe-info-label" }, i18n.calendarDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.deadline).format("YYYY-MM-DD HH:mm"))) : null ), - tags, - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${calendar.participants.length}`) - ), - div({ class: "tribe-side-actions calendar-invite-actions" }, + div({ class: "tribe-side-actions" }, + renderCalendarFavoriteToggle(calendar, shareUrl), + isAuthor + ? form({ method: "GET", action: "/calendars" }, + input({ type: "hidden", name: "filter", value: "edit" }), + input({ type: "hidden", name: "id", value: calendar.rootId }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.calendarUpdate || "Update") + ) + : null, isAuthor && calendar.status !== "OPEN" ? form({ method: "POST", action: `/calendars/generate-invite/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeGenerateInvite) @@ -280,18 +295,27 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { if (!(isAuthor && !calendar.tribeId)) return null const invs = Array.isArray(calendar.invites) ? calendar.invites : [] const openInvite = invs.find(inv => typeof inv === "object" && inv && inv.public === true && inv.code) - if (openInvite) return div({ class: "calendar-open-invite-block" }, + if (openInvite) return [ div({ class: "tribe-open-invite" }, + span({ class: "card-label" }, i18n.tribeInviteCodeText), span({ class: "tribe-open-invite-code" }, openInvite.code) ), form({ method: "POST", action: `/calendars/open-invite/remove/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeRemoveInvitation) ) - ) + ] return form({ method: "POST", action: `/calendars/open-invite/create/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) ) })(), + isAuthor + ? form({ method: "POST", action: `/calendars/delete/${encodeURIComponent(calendar.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.calendarDelete || "Delete") + ) + : null, + !isAuthor + ? a({ href: `/pm?to=${encodeURIComponent(calendar.author)}`, class: "tribe-action-btn" }, "PM") + : null, !isAuthor && !isParticipant && calendar.status === "OPEN" ? form({ method: "POST", action: `/calendars/join/${encodeURIComponent(calendar.rootId)}` }, button({ type: "submit", class: "create-button" }, i18n.calendarJoin || "Join Calendar") @@ -306,43 +330,34 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { ) : null ), - isAuthor - ? div({ class: "tribe-side-actions calendar-owner-actions" }, - form({ method: "GET", action: "/calendars" }, - input({ type: "hidden", name: "filter", value: "edit" }), - input({ type: "hidden", name: "id", value: calendar.rootId }), - button({ type: "submit", class: "tribe-action-btn" }, i18n.calendarUpdate || "Update") - ), - form({ method: "POST", action: `/calendars/delete/${encodeURIComponent(calendar.rootId)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.calendarDelete || "Delete") - ) - ) - : null, - renderDocumentActions('calendars', calendar.rootId) + tags, + div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${calendar.participants.length}`) + ) ) const minDate = now.add(1, "minute").format("YYYY-MM-DDTHH:mm") - const calMax = calendar.deadline ? moment(calendar.deadline).format("YYYY-MM-DDTHH:mm") : "" const canAddDate = !calClosed && (calendar.status === "OPEN" || isAuthor) const unifiedForm = canAddDate ? div({ class: "div-center audio-form" }, + h4(i18n.calendarAddEntry || "Add Entry"), form({ method: "POST", action: `/calendars/add-date/${encodeURIComponent(calendar.rootId)}` }, span(i18n.calendarDateLabel || "Date"), br(), - input({ type: "datetime-local", name: "date", required: true, min: minDate, max: calMax || undefined }), + input({ type: "datetime-local", name: "date", required: true, min: minDate }), br(), br(), span(i18n.calendarFormDescription || "Description"), br(), input({ type: "text", name: "label", placeholder: i18n.calendarDatePlaceholder || "Describe this date..." }), br(), br(), + renderIntervalBlock(), + br(), isParticipant ? [ - span(i18n.calendarNoteLabel), br(), + span(i18n.calendarNoteLabel || "Note (optional)"), br(), textarea({ name: "text", rows: "3", placeholder: i18n.calendarNotePlaceholder || "Add a note..." }), br(), br() ] : null, - renderIntervalBlock({ min: minDate, max: calMax || undefined }), - br(), button({ type: "submit", class: "create-button" }, i18n.calendarAddEntry || "Add Entry") ) ) @@ -376,7 +391,7 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { ) : null, div({ class: "calendar-date-item-header" }, - `${moment(d.date).format("YYYY/MM/DD HH:mm")}${d.label ? " \u2014 " + d.label : ""}` + `${moment(d.date).format("YYYY-MM-DD HH:mm")}${d.label ? " \u2014 " + d.label : ""}` ), (() => { const visibleNotes = notes.filter(n => n.text && String(n.text).trim()) @@ -409,17 +424,6 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { : null const calMain = div({ class: "tribe-main" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(calendar.rootId, null, { - author: calendar.author, - favKind: 'calendars', - isFavorite: calendar.isFavorite, - spread: (params && params.spreads) || null, - returnTo: shareUrl, - reportTitle: calendar.title - }) - ), - calNav, grid, dayNotesSection, @@ -438,5 +442,3 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { section(div({ class: "tribe-details" }, calSide, calMain)) ) } - -exports.renderIntervalBlock = renderIntervalBlock diff --git a/src/views/chats_view.js b/src/views/chats_view.js index c122134..a3c2c00 100644 --- a/src/views/chats_view.js +++ b/src/views/chats_view.js @@ -1,11 +1,9 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, td, ul, li, details, summary } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, userLinkLabel, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions } = require("./main_views") +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe") +const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton } = require("./main_views") const { renderEncryptedChip } = require("./clearnet_view") -const { renderResults, renderBallot, outcomeOf } = require("./polls_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const { renderUrl } = require("../backend/renderUrl") -const { renderZoomableImage } = require("./gallery_view") const userId = config.keys.id const safeArr = (v) => (Array.isArray(v) ? v : []) @@ -19,13 +17,6 @@ const ALL_CATS = [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3] const catKey = (c) => "forumCat" + String(c || "").replace(/\./g, "").replace(/[\s-]/g, "").toUpperCase() const catLabel = (c) => i18n[catKey(c)] || c -const blobSrcOf = (value) => { - const s = String(value || "").trim() - if (s.startsWith("&")) return `/blob/${encodeURIComponent(s)}` - const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - return mImg ? `/blob/${encodeURIComponent(mImg[1])}` : null -} - const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null const s = String(value).trim() @@ -74,19 +65,8 @@ const renderChatCard = (chat, filter, params = {}) => { renderEncryptedChip(i18n), renderLifespanChip(chat.lifetime, i18n) ].filter(Boolean) - const href = `/chats/${encodeURIComponent(chat.key)}` return div({ class: "tribe-card" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(chat.key, href, { - spread: params.spread || null, - author: chat.author, - favKind: 'chats', - isFavorite: chat.isFavorite, - returnTo: buildReturnTo(filter, params), - reportTitle: chat.title - }) - ), div({ class: "tribe-card-image-wrapper" }, a({ href: `/chats/${encodeURIComponent(chat.key)}` }, renderMediaBlob(chat.image, "/assets/images/default-avatar.png", { class: "tribe-card-hero-image" }) @@ -102,6 +82,13 @@ const renderChatCard = (chat, filter, params = {}) => { chat.description ? p({ class: "tribe-card-description" }, chat.description) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.chatParticipants}: ${safeArr(chat.members).length}`) + ), + (() => { + const btn = renderSpreadButton(chat.key, (params && params.spreadMap && params.spreadMap.get(chat.key)) || (params && params.spreads)); + return btn ? div({ class: "card-spread-left" }, btn) : null; + })(), + div({ class: "visit-btn-centered" }, + a({ href: `/chats/${encodeURIComponent(chat.key)}`, class: "filter-btn" }, i18n.chatVisitChat) ) ) ) @@ -117,15 +104,16 @@ const renderChatForm = (filter, chat = {}, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, span(i18n.title || "Title"), br(), - input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.chatTitlePlaceholder, value: chat.title || "" }), br(), br(), + input({ type: "text", name: "title", required: true, placeholder: i18n.chatTitlePlaceholder, value: chat.title || "" }), br(), br(), span(i18n.chatDescription), br(), textarea({ name: "description", rows: 4, placeholder: i18n.chatDescriptionPlaceholder }, chat.description || ""), br(), br(), - span(i18n.uploadMedia), br(), + span(i18n.chatImageLabel || "Select an image file (.jpeg, .jpg, .png, .gif)"), br(), input({ type: "file", name: "image", accept: "image/*" }), br(), br(), span(i18n.chatCategory), br(), select({ name: "category" }, + option({ value: "" }, "\u2014"), ALL_CATS.map(cat => - option({ value: cat, ...((chat.category || "GENERAL") === cat ? { selected: true } : {}) }, catLabel(cat)) + option({ value: cat, ...(chat.category === cat ? { selected: true } : {}) }, catLabel(cat)) ) ), br(), br(), span(i18n.chatStatusLabel || "Status"), br(), @@ -152,99 +140,22 @@ const renderMessageText = (text) => { return span({ class: "chat-message-text" }, ...nodes) } -const chatActivityTs = (c) => Math.max( - Number(c.lastMsgAt || 0), - Date.parse(c.updatedAt || "") || 0, - Date.parse(c.createdAt || "") || 0 -) - -const renderChatTopics = (chats, activeKey) => - div({ class: "chat-topics" }, - div({ class: "chat-topics-head" }, - a({ href: "/chats", class: "chat-topics-title-link" }, i18n.chatsTitle), - a({ href: "/chats?filter=create", class: "filter-btn chat-topics-new" }, "+") - ), - ...safeArr(chats).slice().sort((a, b) => chatActivityTs(b) - chatActivityTs(a)).map(c => { - const key = c.rootId || c.key - const active = activeKey && String(key) === String(activeKey) - return a({ href: `/chats/${encodeURIComponent(key)}`, class: active ? "chat-topic chat-topic-active" : "chat-topic" }, - span({ class: "chat-topic-title" }, c.title || i18n.chatUntitled), - span({ class: "chat-topic-meta" }, `${i18n.chatParticipants}: ${safeArr(c.members).length}`) - ) - }) - ) - -const renderSenderLink = (author, isOwner) => - author - ? a({ href: `/author/${encodeURIComponent(author)}`, class: isOwner ? "chat-bubble-sender chat-bubble-sender-owner" : "chat-bubble-sender" }, userLinkLabel(author)) - : null - -const renderChatPoll = (poll, chat) => { - const isSelf = String(poll.author) === String(userId) - const isOwner = String(poll.author) === String(chat.author) - const showResults = poll.hasVoted || poll.status === "CLOSED" - return div({ class: isSelf ? "chat-bubble-row chat-bubble-row-self" : "chat-bubble-row" }, - div({ class: isSelf ? "chat-message chat-message-self chat-poll" : "chat-message chat-poll" }, - isSelf ? null : renderSenderLink(poll.author, isOwner), - div({ class: "chat-poll-head" }, - span({ class: "chat-poll-tag" }, i18n.pollInChat), - poll.anonymous ? span({ class: "chat-poll-tag" }, i18n.pollAnonymous) : null, - poll.multiple ? span({ class: "chat-poll-tag" }, i18n.pollMultiple) : null, - poll.status === "CLOSED" ? span({ class: "chat-poll-tag" }, i18n.pollStatusClosed) : null - ), - poll.undecryptable - ? p({ class: "chat-poll-question" }, i18n.chatAccessDenied) - : [ - p({ class: "chat-poll-question" }, poll.question), - showResults ? renderResults(poll) : null, - div({ class: "chat-poll-actions" }, - renderBallot(poll, `/chats/${encodeURIComponent(chat.key)}`, "/polls"), - String(poll.author) === String(userId) && poll.status === "OPEN" - ? form({ method: "POST", action: `/polls/close/${encodeURIComponent(poll.id)}` }, - input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }), - button({ type: "submit", class: "filter-btn" }, i18n.pollCloseButton) - ) - : null - ), - div({ class: "chat-poll-meta" }, - span({ class: "card-label" }, `${i18n.pollVoters}: `), - span({ class: "card-value" }, String(poll.totalVoters)), - span({ class: "card-label" }, ` · ${i18n.pollOutcome}: `), - span({ class: "card-value" }, outcomeOf(poll)) - ) - ], - span({ class: "chat-bubble-time" }, moment(poll.createdAt).format("HH:mm")) - ) - ) -} - -const renderMessage = (msg, chatAuthor, ctx = {}) => { +const renderMessage = (msg, chatAuthor) => { const isAuthor = String(msg.author) === String(chatAuthor) const isSelf = String(msg.author) === String(userId) - // cita del mensaje respondido: se resuelve con el indice que arma la vista - const quoted = (msg.replyTo && ctx.msgById) ? ctx.msgById.get(String(msg.replyTo)) : null - const replyHref = (ctx.chatKey && msg.key) - ? `/chats/${encodeURIComponent(ctx.chatKey)}?replyTo=${encodeURIComponent(msg.key)}#chat-compose` - : null - const imageSrc = blobSrcOf(msg.image) - const imageNode = imageSrc - ? renderZoomableImage(imageSrc, { imgClass: "chat-message-image" }) - : (msg.image ? renderMediaBlob(msg.image, null, { class: "chat-message-image" }) : null) + const dateStr = moment(msg.createdAt).format("YYYY/MM/DD HH:mm") + const shortId = msg.author ? "@" + msg.author.slice(1, 9) + "\u2026" : "?" + const authorLink = msg.author ? userLink(msg.author) : span("?") - return div({ class: isSelf ? "chat-bubble-row chat-bubble-row-self" : "chat-bubble-row" }, - div({ class: isSelf ? "chat-message chat-message-self" : isAuthor ? "chat-message chat-message-author" : "chat-message" }, - isSelf ? null : renderSenderLink(msg.author, isAuthor), - quoted - ? div({ class: "chat-quote" }, - span({ class: "chat-quote-author" }, String(quoted.author || "").slice(0, 10)), - span({ class: "chat-quote-text" }, String(quoted.text || "").slice(0, 120)) - ) - : null, - renderMessageText(msg.text || ""), - imageNode ? div({ class: "chat-message-image-wrap" }, imageNode) : null, - span({ class: "chat-bubble-time" }, moment(msg.createdAt).format("HH:mm")), - replyHref ? a({ class: "chat-reply-btn", href: replyHref, title: i18n.chatReply || "Reply" }, "↩") : null - ) + const imageNode = msg.image ? renderMediaBlob(msg.image, null, { class: "chat-message-image" }) : null + + return div({ class: isSelf ? "chat-message chat-message-self" : isAuthor ? "chat-message chat-message-author" : "chat-message" }, + div({ class: "chat-message-meta" }, + span({ class: "chat-message-sender" }, authorLink), + span({ class: "chat-message-date" }, ` [ ${dateStr} ]`) + ), + imageNode ? div({ class: "chat-message-image-wrap" }, imageNode) : null, + renderMessageText(msg.text || "") ) } @@ -266,21 +177,15 @@ exports.chatsView = async (chats, filter, chatToEdit = null, params = {}) => { const isForm = filter === "create" || filter === "edit" - const headerText = i18n.chatsTitle - - if (params.workspace && !isForm) { - return template( - i18n.chatsTitle, - section({ class: "chat-workspace-section" }, - div({ class: "chat-workspace" }, - renderChatTopics(list, null), - div({ class: "chat-workspace-main chat-workspace-empty" }, - p({ class: "chat-no-messages" }, list.length ? i18n.chatPickChat : i18n.chatNoneYet) - ) - ) - ) - ) + const chatHeaderMap = { + all: i18n.chatsTitle, + mine: i18n.chatMineSectionTitle || "Your Chats", + recent: i18n.chatRecentTitle || "Recent Chats", + favorites: i18n.chatFavoritesTitle || "Favorites", + open: i18n.chatOpenTitle || "Open Chats", + closed: i18n.chatClosedTitle || "Closed Chats" } + const headerText = chatHeaderMap[filter] || i18n.chatsTitle return template( i18n.chatsTitle, @@ -294,12 +199,12 @@ exports.chatsView = async (chats, filter, chatToEdit = null, params = {}) => { !isForm ? section( div({ class: "filters" }, - form({ method: "GET", action: "/chats", class: "filter-box" }, + form({ method: "GET", action: "/chats" }, input({ type: "hidden", name: "filter", value: filter }), - input({ type: "text", name: "q", placeholder: i18n.chatSearchPlaceholder, value: q, class: "filter-box__input" }), - div({ class: "filter-box__controls" }, - button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) - ) + input({ type: "text", name: "q", placeholder: i18n.chatSearchPlaceholder, value: q }), + br(), + button({ type: "submit" }, i18n.search), + br() ) ) ) @@ -309,7 +214,7 @@ exports.chatsView = async (chats, filter, chatToEdit = null, params = {}) => { ? renderChatForm(filter, filter === "edit" ? (chatToEdit || {}) : {}, params) : div({ class: "tribe-grid" }, list.length - ? list.map(chat => renderChatCard(chat, filter, { q, spread: params.spreadMap && params.spreadMap.get(chat.key) })) + ? list.map(chat => renderChatCard(chat, filter, { q })) : p(i18n.chatNoItems) ) ) @@ -321,6 +226,7 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q }) const isAuthor = String(chat.author) === String(userId) const isMember = safeArr(chat.members).includes(userId) || (!!chat.tribeId && !!chat.isTribeMember) + const fullShareUrl = `/chats/${encodeURIComponent(chat.key)}` const isRestrictedInviteOnly = !isMember && !isAuthor && chat.status === "INVITE-ONLY" const statusLabel = chat.status === "CLOSED" ? i18n.chatStatusClosed : @@ -332,14 +238,19 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { renderLifespanChip(chat.lifetime, i18n) ].filter(Boolean) const chatSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(chat.key, null, { spread: params.spreads || null, author: chat.author, favKind: 'chats', isFavorite: chat.isFavorite, returnTo: params.returnTo, reportTitle: chat.title }) - ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, chat.title || i18n.chatUntitled) ), detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null, renderMediaBlob(chat.image, "/assets/images/default-avatar.png", { class: "tribe-detail-image" }), + div({ class: "shop-share" }, + span({ class: "tribe-info-label" }, `${i18n.chatShareUrl}: `), + input({ type: "text", value: fullShareUrl, readonly: true, class: "shop-share-input" }) + ), + (() => { + const btn = renderSpreadButton(chat.key, params.spreads); + return btn ? div({ class: "card-spread-centered" }, btn) : null; + })(), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.chatParticipants}: ${safeArr(chat.members).length}`) ), @@ -348,18 +259,17 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { td({ class: "tribe-info-label" }, i18n.chatCreatedAt), td({ class: "tribe-info-value", colspan: "3" }, moment(chat.createdAt).format("YYYY/MM/DD HH:mm")) ), - !isRestrictedInviteOnly && chat.category ? tr( - td({ class: "tribe-info-label" }, i18n.chatCategoryLabel), - td({ class: "tribe-info-value", colspan: "3" }, catLabel(chat.category)) - ) : null, isRestrictedInviteOnly ? null : tr( td({ class: "tribe-info-value", colspan: "4" }, userLink(chat.author) ) - ) + ), + !isRestrictedInviteOnly && chat.category ? tr( + td({ class: "tribe-info-label" }, i18n.chatCategoryLabel), + td({ class: "tribe-info-value", colspan: "3" }, catLabel(chat.category)) + ) : null ), - isRestrictedInviteOnly ? null : (() => { - const sideActionNodes = [ + isRestrictedInviteOnly ? null : div({ class: "tribe-side-actions" }, isAuthor && chat.status === "INVITE-ONLY" ? form({ method: "POST", action: `/chats/generate-invite` }, input({ type: "hidden", name: "chatId", value: chat.key }), @@ -385,34 +295,39 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { button({ type: "submit", class: "tribe-action-btn" }, i18n.tribeOpenInvitation) ) })(), + form( + { method: "POST", action: chat.isFavorite ? `/chats/favorites/remove/${encodeURIComponent(chat.key)}` : `/chats/favorites/add/${encodeURIComponent(chat.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn" }, chat.isFavorite ? i18n.chatRemoveFavorite : i18n.chatAddFavorite) + ), + chat.author && String(chat.author) !== String(userId) + ? form({ method: "GET", action: "/pm" }, + input({ type: "hidden", name: "recipients", value: chat.author }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatPM || i18n.privateMessage) + ) + : null, + isAuthor + ? form({ method: "GET", action: `/chats/edit/${encodeURIComponent(chat.key)}` }, + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate) + ) + : null, + isAuthor && chat.status !== "CLOSED" + ? form({ method: "POST", action: `/chats/close/${encodeURIComponent(chat.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ type: "submit", class: "tribe-action-btn" }, i18n.chatClose) + ) + : null, + isAuthor + ? form({ method: "POST", action: `/chats/delete/${encodeURIComponent(chat.key)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.chatDelete) + ) + : null, !isAuthor && isMember ? form({ method: "POST", action: `/chats/leave/${encodeURIComponent(chat.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeLeaveButton) ) : null - ].flat().filter(Boolean) - return sideActionNodes.length ? div({ class: "tribe-side-actions" }, ...sideActionNodes) : null - })(), - isAuthor - ? div({ class: "tribe-side-actions housing-status-row" }, - span({ class: "card-label" }, `${i18n.chatStatusLabel}: `), - renderStateChip(chat.status === "CLOSED" ? "hidden" : "mutuals", chat.status === "CLOSED" ? "🔒" : "👁", String(chat.status || "OPEN").toUpperCase()), - chat.status !== "CLOSED" - ? form({ method: "POST", action: `/chats/close/${encodeURIComponent(chat.key)}`, class: "inline-form" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "tribe-action-btn" }, i18n.chatClose) - ) - : null - ) - : null, - !isAuthor ? null : div({ class: "tribe-side-actions owner-actions" }, - form({ method: "GET", action: `/chats/edit/${encodeURIComponent(chat.key)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.chatUpdate) - ), - form({ method: "POST", action: `/chats/delete/${encodeURIComponent(chat.key)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.chatDelete) - ) ), !isMember && chat.status === "INVITE-ONLY" ? div({ class: "chat-join-section" }, @@ -427,104 +342,31 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { ) const msgList = safeArr(messages) - // indice por clave para resolver las citas; se arma antes de mezclar con encuestas - const msgById = new Map(msgList.filter(m => m && m.key).map(m => [String(m.key), m])) - const replyingTo = params.replyTo ? msgById.get(String(params.replyTo)) : null const canWrite = (isMember || chat.status === "OPEN") && chat.status !== "CLOSED" const chatMain = isRestrictedInviteOnly ? div({ class: "tribe-main chat-full-width" }, p({ class: "access-denied-msg" }, i18n.chatAccessDenied)) : div({ class: "tribe-main chat-full-width" }, - msgList.length - ? div({ class: "chat-jump-row" }, - a({ href: "#chat-latest", class: "filter-btn chat-jump-latest" }, i18n.chatJumpLatest) - ) - : null, - div({ class: "chat-messages-list" }, - (() => { - const visible = msgList - .filter(msg => (msg.text && String(msg.text).trim()) || msg.image) - .map(msg => ({ kind: 'message', ts: Date.parse(msg.createdAt) || 0, msg })) - const pollItems = safeArr(params.polls).map(poll => ({ - kind: 'poll', ts: Date.parse(poll.createdAt) || 0, poll - })) - const stream = [...visible, ...pollItems].sort((a, b) => a.ts - b.ts) - if (!stream.length) return p({ class: "chat-no-messages" }, i18n.chatNoMessages) - const nodes = [] - let prevDay = null - stream.forEach((entry, i) => { - const day = moment(entry.ts).format("YYYY/MM/DD") - if (day !== prevDay) { - nodes.push(div({ class: "chat-day-separator" }, span({ class: "chat-day-chip" }, day))) - prevDay = day - } - const last = i === stream.length - 1 - const node = entry.kind === 'poll' - ? renderChatPoll(entry.poll, chat) - : renderMessage(entry.msg, chat.author, { msgById, chatKey: chat.key }) - nodes.push(last ? div({ id: "chat-latest", class: "chat-latest-anchor" }, node) : node) - }) - return nodes - })() - ), canWrite ? div({ class: "chat-message-form" }, form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/message`, enctype: "multipart/form-data" }, - input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }), - replyingTo - ? div({ class: "chat-replying-to", id: "chat-compose" }, - span({ class: "chat-quote-author" }, String(replyingTo.author || "").slice(0, 10)), - span({ class: "chat-quote-text" }, String(replyingTo.text || "").slice(0, 120)), - a({ class: "chat-reply-cancel", href: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }, "✕") - ) - : null, - replyingTo ? input({ type: "hidden", name: "replyTo", value: String(params.replyTo) }) : null, + input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }), textarea({ name: "text", rows: 3, placeholder: i18n.chatMessagePlaceholder }), br(), - span(i18n.uploadMedia), br(), - input({ type: "file", name: "image", accept: "image/*,video/*" }), br(), br(), + span(i18n.chatImageLabel || "Select an image file (.jpeg, .jpg, .png, .gif)"), br(), + input({ type: "file", name: "image", accept: "image/*" }), br(), br(), button({ type: "submit", class: "filter-btn" }, i18n.chatSendMessage) ) ) : null, - canWrite && params.pollsEnabled - ? details({ class: "chat-poll-form" }, - summary({ class: "chat-poll-summary" }, i18n.pollChatCreate), - form({ method: "POST", action: `/chats/${encodeURIComponent(chat.key)}/polls/create` }, - input({ type: "hidden", name: "returnTo", value: `/chats/${encodeURIComponent(chat.key)}` }), - span(i18n.pollQuestion), br(), - input({ type: "text", name: "question", required: true, maxlength: "300", placeholder: i18n.pollQuestionPlaceholder }), br(), br(), - span(i18n.pollOptions), br(), - textarea({ name: "options", rows: 4, required: true, placeholder: i18n.pollOptionsPlaceholder }), br(), br(), - div({ class: "poll-switch" }, - input({ type: "hidden", name: "anonymous", value: "0" }), - label(input({ type: "checkbox", name: "anonymous", value: "1" }), " ", i18n.pollAnonymousLabel) - ), - div({ class: "poll-switch" }, - input({ type: "hidden", name: "multiple", value: "0" }), - label(input({ type: "checkbox", name: "multiple", value: "1" }), " ", i18n.pollMultipleLabel) - ), - button({ type: "submit", class: "filter-btn" }, i18n.pollPublishButton) - ) - ) - : null - ) - - if (params.workspace) { - return template( - chat.title || i18n.chatUntitled, - section({ class: "chat-workspace-section" }, - div({ class: "chat-workspace" }, - renderChatTopics(safeArr(params.allChats), chat.rootId || chat.key), - div({ class: "chat-workspace-main" }, - div({ class: "tribe-details chat-workspace-details" }, - chatSide, - chatMain - ) - ) - ) - ) + div({ class: "chat-messages-list" }, + (() => { + const visible = msgList.filter(msg => (msg.text && String(msg.text).trim()) || msg.image) + return visible.length + ? visible.map(msg => renderMessage(msg, chat.author)) + : p({ class: "chat-no-messages" }, i18n.chatNoMessages) + })() ) - } + ) return template( chat.title || i18n.chatUntitled, diff --git a/src/views/clearnet_view.js b/src/views/clearnet_view.js index 2ba743a..a863462 100644 --- a/src/views/clearnet_view.js +++ b/src/views/clearnet_view.js @@ -71,7 +71,7 @@ const renderFediverseReach = (prefs, i18nObj = {}) => { a({ href: url, target: '_blank', rel: 'noopener noreferrer', class: 'pm-exposition-chip-link' }, span({ class: 'pm-exposition-chip pm-exposition-fediverse' }, span({ class: 'pm-exposition-icon' }, '🐘'), - span({ class: 'pm-exposition-text' }, i18nObj.fediverse || 'Multiverse') + span({ class: 'pm-exposition-text' }, i18nObj.fediverse || 'Fediverse') ) ), a({ href: url, target: '_blank', rel: 'noopener noreferrer', class: 'fediverse-reach-url' }, handle) diff --git a/src/views/comments_view.js b/src/views/comments_view.js deleted file mode 100644 index 85489fc..0000000 --- a/src/views/comments_view.js +++ /dev/null @@ -1,77 +0,0 @@ -const { div, p, h2, span, a, form, input, textarea, button, label, br, details, summary } = require("../server/node_modules/hyperaxe"); -const moment = require("../server/node_modules/moment"); -const { i18n, userLink } = require("./main_views"); -const { renderUrl } = require("../backend/renderUrl"); - -const COMMENT_ICON = "✑"; - -const visibleComments = (comments) => (Array.isArray(comments) ? comments : []).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); -}); - -const renderCommentCard = (c) => { - const author = (c && c.value && c.value.author) || ""; - const ts = (c && c.value && c.value.timestamp) || (c && c.timestamp); - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - const content = (c && c.value && c.value.content) || {}; - const rootId = content.fork || content.root || null; - const text = content.text || ""; - - return div({ class: "votations-comment-card", id: c && c.key ? c.key : undefined }, - span({ class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(String(text))) - ); -}; - -const renderCommentsSection = ({ action, comments = [], returnTo = null, closedNote = null, extraClass = "" } = {}) => { - const list = visibleComments(comments); - return details({ class: "comments-collapse" + (extraClass ? ` ${extraClass}` : "") }, - summary({ class: list.length > 0 ? "comments-summary engage-on" : "comments-summary" }, - span({ class: "comments-summary-icon" }, COMMENT_ICON), - span({ class: "comments-summary-count" }, `(${list.length})`) - ), - div({ class: "vote-comments-section" }, - closedNote - ? p({ class: "muted" }, closedNote) - : div({ class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form({ method: "POST", action, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - textarea({ name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div({ class: "comments-list" }, ...list.map(renderCommentCard)) - : (closedNote ? null : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)) - ) - ); -}; - -const renderCommentsLink = ({ href, count = 0 } = {}) => { - if (!href) return null; - const total = Number(count) || 0; - return a({ href, class: total > 0 ? "comments-summary engage-on comments-link" : "comments-summary comments-link" }, - span({ class: "comments-summary-icon" }, COMMENT_ICON), - span({ class: "comments-summary-count" }, `(${total})`) - ); -}; - -module.exports = { - COMMENT_ICON, - renderCommentsLink, - visibleComments, - renderCommentCard, - renderCommentsSection -}; diff --git a/src/views/courts_view.js b/src/views/courts_view.js index 9fb4138..ef8d681 100644 --- a/src/views/courts_view.js +++ b/src/views/courts_view.js @@ -67,7 +67,7 @@ const Tabs = (active) => ) ); -const CaseForm = (prefill = {}) => +const CaseForm = () => div( { class: 'div-center' }, h2(i18n.courtsCaseFormTitle), @@ -82,15 +82,14 @@ const CaseForm = (prefill = {}) => type: 'text', name: 'titleSuffix', required: true, - placeholder: 'Subject or short description', - value: prefill.titleSuffix || '' + placeholder: 'Subject or short description' }), br(), label('Case type'), br(), select( { name: 'titlePreset', required: true }, - CASE_TITLE_PRESETS.map((t) => option({ value: t, ...(prefill.titlePreset === t ? { selected: true } : {}) }, t)) + CASE_TITLE_PRESETS.map((t) => option({ value: t }, t)) ), br(), br(), @@ -102,8 +101,7 @@ const CaseForm = (prefill = {}) => placeholder: i18n.courtsCaseRespondentPh, required: true, pattern: '^@[A-Za-z0-9+/]+=*\\.ed25519$', - title: i18n.courtsRespondentInvalid || 'Must be a valid SSB ID (@...ed25519)', - value: prefill.respondentId || '' + title: i18n.courtsRespondentInvalid || 'Must be a valid SSB ID (@...ed25519)' }), br(), label(i18n.courtsCaseMethod), @@ -111,7 +109,7 @@ const CaseForm = (prefill = {}) => select( { name: 'method', required: true }, ['JUDGE', 'DICTATOR', 'POPULAR', 'MEDIATION', 'KARMATOCRACY'].map((m) => - option({ value: m, ...(prefill.method === m ? { selected: true } : {}) }, i18n[`courtsMethod${m}`]) + option({ value: m }, i18n[`courtsMethod${m}`]) ) ), br(), @@ -470,11 +468,11 @@ const CaseCard = (c) => { select( { name: 'preference' }, option( - { value: 'YES', ...(c.myPublicPreference === true ? { selected: true } : {})}, + { value: 'YES', selected: c.myPublicPreference === true }, i18n.courtsPublicPrefYes ), option( - { value: 'NO', ...(c.myPublicPreference === false ? { selected: true } : {})}, + { value: 'NO', selected: c.myPublicPreference === false }, i18n.courtsPublicPrefNo ) ), @@ -685,11 +683,11 @@ const MyCaseCard = (c) => { select( { name: 'preference' }, option( - { value: 'YES', ...(c.myPublicPreference === true ? { selected: true } : {})}, + { value: 'YES', selected: c.myPublicPreference === true }, i18n.courtsPublicPrefYes ), option( - { value: 'NO', ...(c.myPublicPreference === false ? { selected: true } : {})}, + { value: 'NO', selected: c.myPublicPreference === false }, i18n.courtsPublicPrefNo ) ), @@ -1069,18 +1067,17 @@ const CaseSearch = (filter, search = '') => div( { class: 'filters' }, form( - { method: 'GET', action: '/courts', class: 'filter-box' }, + { method: 'GET', action: '/courts' }, input({ type: 'hidden', name: 'filter', value: filter }), input({ type: 'text', name: 'search', placeholder: i18n.searchCasesPlaceholder, - value: search || '', - class: 'filter-box__input' + value: search || '' }), - div({ class: 'filter-box__controls' }, - button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) - ) + br(), + button({ type: 'submit' }, i18n.applyFilters), + br() ) ); @@ -1385,7 +1382,7 @@ const courtsView = async (state) => { filter === 'judges' ? JudgesSection(nominations, userId) : null, filter === 'history' ? HistoryList(history) : null, filter === 'rules' ? RulesContent() : null, - filter === 'open' ? CaseForm(state.prefill || {}) : null + filter === 'open' ? CaseForm() : null ) ); }; diff --git a/src/views/cv_view.js b/src/views/cv_view.js index b076e4b..9725175 100644 --- a/src/views/cv_view.js +++ b/src/views/cv_view.js @@ -1,5 +1,5 @@ -const { form, button, div, h2, p, section, textarea, label, input, br, img, a, select, option, span, table, tr, td } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderVisibilityChip, renderDocumentActions } = require('./main_views'); +const { form, button, div, h2, p, section, textarea, label, input, br, img, a, select, option, span } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, renderVisibilityChip } = require('./main_views'); const { renderUrl } = require('../backend/renderUrl'); const generateCVBox = (label, content, className) => { @@ -9,8 +9,22 @@ const generateCVBox = (label, content, className) => { ); }; +const generateTags = (tags) => { + return tags && tags.length + ? div( + tags.map(tag => + a({ + href: `/search?query=%23${encodeURIComponent(tag)}`, + class: "tag-link", + style: "margin-right:0.8em;margin-bottom:0.5em;" + }, `#${tag}`) + ) + ) + : null; +}; + exports.createCVView = async (cv = {}, editMode = false) => { - const title = i18n.cvTitle; + const title = editMode ? i18n.cvEditSectionTitle : i18n.cvCreateSectionTitle; return template( title, @@ -26,20 +40,6 @@ exports.createCVView = async (cv = {}, editMode = false) => { enctype: "multipart/form-data" }, - div({ class: "cv-box cv-ai-box" }, - div({ class: "poll-switch" }, - input({ type: "hidden", name: "aiManaged", value: "0" }), - label( - input({ type: "checkbox", name: "aiManaged", value: "1", ...(cv.aiManaged === false ? {} : { checked: true }) }), - " ", i18n.cvAiManaged - ) - ), - div({ class: "cv-threshold-row" }, - label({ for: "cv_match_threshold" }, `${i18n.cvMatchThreshold}: `), - input({ type: "number", id: "cv_match_threshold", name: "matchThreshold", min: "0", max: "100", step: "5", value: String(cv.matchThreshold != null ? cv.matchThreshold : 80) }) - ) - ), - generateCVBox(i18n.cvPersonal, [ label(i18n.cvNameLabel), br(), input({ type: "text", name: "name", required: true, value: cv.name || "" }), br(), @@ -81,19 +81,19 @@ exports.createCVView = async (cv = {}, editMode = false) => { input({ type: "text", name: "location", required: true, value: cv.location || "UNKNOWN" }), br(), label(i18n.cvStatusLabel), br(), select({ name: "status", required: true }, - option({ value: "AVAILABLE", ...(cv.status === "AVAILABLE FOR COLLABORATION" ? { selected: true } : {})}, "AVAILABLE FOR COLLABORATION"), - option({ value: "UNAVAILABLE", ...(cv.status === "NOT CURRENTLY AVAILABLE" ? { selected: true } : {})}, "NOT CURRENTLY AVAILABLE"), - option({ value: "LOOKING FOR WORK", ...(!cv.status || cv.status === "LOOKING FOR WORK" ? { selected: true } : {})}, "LOOKING FOR WORK") + option({ value: "AVAILABLE", selected: cv.status === "AVAILABLE FOR COLLABORATION" }, "AVAILABLE FOR COLLABORATION"), + option({ value: "UNAVAILABLE", selected: cv.status === "NOT CURRENTLY AVAILABLE" }, "NOT CURRENTLY AVAILABLE"), + option({ value: "LOOKING FOR WORK", selected: !cv.status || cv.status === "LOOKING FOR WORK" }, "LOOKING FOR WORK") ), br(), br(), label(i18n.cvPreferencesLabel), br(), select({ name: "preferences", required: true }, - option({ value: "IN PERSON", ...(cv.preferences === "IN-PERSON ONLY" ? { selected: true } : {})}, "IN-PERSON ONLY"), - option({ value: "REMOTE WORKING", ...(!cv.preferences || cv.preferences === "REMOTE WORKING" ? { selected: true } : {})}, "REMOTE-WORKING") + option({ value: "IN PERSON", selected: cv.preferences === "IN-PERSON ONLY" }, "IN-PERSON ONLY"), + option({ value: "REMOTE WORKING", selected: !cv.preferences || cv.preferences === "REMOTE WORKING" }, "REMOTE-WORKING") ), br(), br(), label(i18n.visibilityLabel || "Visibility"), br(), select({ name: "visibility" }, - option({ value: "PUBLIC", ...((cv.visibility || "PUBLIC") === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"), - option({ value: "HIDDEN", ...(cv.visibility === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden") + option({ value: "PUBLIC", selected: (cv.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", selected: cv.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden") ), br() ], "availability"), @@ -125,99 +125,12 @@ exports.cvView = async (cv) => { ) } - const skills = (list) => (Array.isArray(list) ? list.filter(Boolean) : []); - const allSkills = [ - ...skills(cv.personalSkills), - ...skills(cv.educationalSkills), - ...skills(cv.professionalSkills), - ...skills(cv.oasisSkills) - ]; - const uniqueSkills = Array.from(new Set(allSkills)); - - const renderSkillTags = (list) => skills(list).length - ? div({ class: "card-tags" }, - skills(list).map(tag => - a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)) - ) - : null; - - const renderBlock = (heading, body, list) => (body || skills(list).length) - ? div({ class: "cv-box" }, - h2({ class: "job-section-title" }, heading), - body ? p({ class: "tribe-side-description" }, ...renderUrl(String(body))) : null, - renderSkillTags(list) - ) - : null; - - const vis = (cv.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'; - const nextVis = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC'; - - const infoRows = []; - const pushRow = (labelText, valueNode) => - infoRows.push(tr( - td({ class: "tribe-info-label" }, labelText), - td({ class: "tribe-info-value" }, valueNode) - )); - if (cv.location) pushRow(i18n.cvLocationLabel, cv.location); - if (cv.status) pushRow(i18n.cvStatusLabel, cv.status); - if (cv.preferences) pushRow(i18n.cvPreferencesLabel, cv.preferences); - if (cv.languages) pushRow(i18n.cvLanguagesLabel, String(cv.languages).toUpperCase()); - pushRow(i18n.cvAiManaged, cv.aiManaged === false ? i18n.switchOff : `${i18n.switchOn} · ${cv.matchThreshold != null ? cv.matchThreshold : 80}%`); - pushRow(i18n.cvCreatedAt, new Date(cv.createdAt).toLocaleString()); - if (cv.updatedAt) pushRow(i18n.cvUpdatedAt, new Date(cv.updatedAt).toLocaleString()); - - const cvSide = div({ class: "tribe-side" }, - div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, cv.name || i18n.unnamed || 'Anonymous') - ), - cv.photo - ? img({ src: `/blob/${encodeURIComponent(cv.photo)}`, class: "cv-photo tribe-detail-image" }) - : null, - (cv.contact || cv.author) - ? div({ class: 'profile-qr' }, - a({ href: `/author/${encodeURIComponent(cv.contact || cv.author)}` }, - img({ class: 'profile-qr-img', src: `/qr/${encodeURIComponent(cv.contact || cv.author)}?size=240`, alt: 'QR' }))) - : null, - cv.contact ? p(userLink(cv.contact)) : null, - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), - uniqueSkills.length - ? div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${i18n.cvSkillsCount}: ${uniqueSkills.length}`) - ) - : null, - div({ class: "tribe-side-actions cv-visibility-row housing-status-row" }, - span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `), - renderVisibilityChip(vis, i18n), - form({ method: "POST", action: `/cv/visibility/${encodeURIComponent(cv.id)}`, class: "inline-form" }, - input({ type: "hidden", name: "visibility", value: nextVis }), - button({ type: "submit", class: "filter-btn" }, - nextVis === 'PUBLIC' ? (i18n.visibilityMakePublic || 'Make public') : (i18n.visibilityMakeHidden || 'Make hidden') - ) - ) - ), - div({ class: "tribe-side-actions" }, - form({ method: "GET", action: `/cv/edit/${encodeURIComponent(cv.id)}` }, - button({ type: "submit", class: "update-btn" }, i18n.cvEditButton) - ), - form({ method: "POST", action: `/cv/delete/${encodeURIComponent(cv.id)}` }, - button({ type: "submit", class: "delete-btn" }, i18n.cvDeleteButton) - ) - ), - renderDocumentActions('cv', null) - ); - - const cvMain = div({ class: "tribe-main" }, - cv.description - ? div({ class: "job-section" }, - h2({ class: "job-section-title" }, i18n.cvDescriptionLabel), - p({ class: "tribe-side-description" }, ...renderUrl(String(cv.description))) - ) - : null, - renderBlock(i18n.cvPersonal, cv.personalExperiences, cv.personalSkills), - renderBlock(i18n.cvEducationalView, cv.educationExperiences, cv.educationalSkills), - renderBlock(i18n.cvProfessionalView, cv.professionalExperiences, cv.professionalSkills), - renderBlock(i18n.cvOasisContributorView, cv.oasisExperiences, cv.oasisSkills) - ); + const hasPersonal = cv.contact || cv.name || cv.description || cv.photo || typeof cv.oasisContributor === "boolean" || (cv.personalSkills && cv.personalSkills.length); + const hasPersonalExp = cv.personalExperiences; + const hasOasis = cv.oasisExperiences || (cv.oasisSkills && cv.oasisSkills.length); + const hasEducational = cv.educationExperiences || cv.languages || (cv.educationalSkills && cv.educationalSkills.length); + const hasProfessional = cv.professionalExperiences || (cv.professionalSkills && cv.professionalSkills.length); + const hasAvailability = cv.location || cv.status || cv.preferences; return template( title, @@ -226,7 +139,119 @@ exports.cvView = async (cv) => { h2(title), p(i18n.cvDescription) ), - div({ class: "tribe-details" }, cvSide, cvMain) + div({ class: "cv-section" }, + div({ class: "cv-item" }, ...[ + (() => { + const vis = (cv.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC'; + const next = vis === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC'; + return div({ class: "cv-visibility-row" }, + span({ class: "card-label" }, `${i18n.visibilityLabel || 'Visibility'}: `), + renderVisibilityChip(vis, i18n), + " ", + form({ method: "POST", action: `/cv/visibility/${encodeURIComponent(cv.id)}`, class: "inline-form" }, + input({ type: "hidden", name: "visibility", value: next }), + button({ type: "submit", class: "filter-btn" }, + next === 'PUBLIC' ? (i18n.visibilityMakePublic || 'Make public') : (i18n.visibilityMakeHidden || 'Make hidden') + ) + ) + ); + })(), + div({ class: "cv-actions" }, + form({ method: "GET", action: `/cv/edit/${encodeURIComponent(cv.id)}` }, + button({ type: "submit" }, i18n.cvEditButton) + ), + form({ method: "POST", action: `/cv/delete/${encodeURIComponent(cv.id)}` }, + button({ type: "submit", class: "danger-btn" }, i18n.cvDeleteButton) + ) + ), + div({ class: "cv-meta" }, + p(`${i18n.cvCreatedAt}: ${new Date(cv.createdAt).toLocaleString()}`), + cv.updatedAt ? p(`${i18n.cvUpdatedAt}: ${new Date(cv.updatedAt).toLocaleString()}`) : null + ), + hasPersonal ? div({ class: "cv-box personal" }, ...[ + cv.photo + ? img({ + src: `/blob/${encodeURIComponent(cv.photo)}`, + class: "cv-photo" + }) + : null, + cv.name ? h2(`${cv.name}`) : null, + (cv.contact || cv.author) + ? a({ href: `/author/${encodeURIComponent(cv.contact || cv.author)}`, class: 'inhabitant-qr-link' }, + img({ class: 'inhabitant-qr-small', src: `/qr/${encodeURIComponent(cv.contact || cv.author)}?size=120`, alt: 'QR' })) + : null, + cv.contact ? p(userLink(cv.contact)) : null, + cv.description ? p(...renderUrl(`${cv.description}`)) : null, + (cv.personalSkills && cv.personalSkills.length) + ? div( + cv.personalSkills.map(tag => + a({ + href: `/search?query=%23${encodeURIComponent(tag)}`, + class: "tag-link", + style: "margin-right:0.8em;margin-bottom:0.5em;" + }, `#${tag}`) + ) + ) + : null + ]) : null, + hasPersonal ? div({ class: "cv-box personal" }, ...[ + h2(i18n.cvLanguagesLabel), + cv.languages ? p(`${cv.languages.toUpperCase()}`) : null + ]) : null, + hasEducational ? div({ class: "cv-box education" }, ...[ + h2(i18n.cvEducationalView), + cv.educationExperiences ? p(...renderUrl(`${cv.educationExperiences}`)) : null, + (cv.educationalSkills && cv.educationalSkills.length) + ? div( + cv.educationalSkills.map(tag => + a({ + href: `/search?query=%23${encodeURIComponent(tag)}`, + class: "tag-link", + style: "margin-right:0.8em;margin-bottom:0.5em;" + }, `#${tag}`) + ) + ) + : null + ]) : null, + hasProfessional ? div({ class: "cv-box professional" }, ...[ + h2(i18n.cvProfessionalView), + cv.professionalExperiences ? p(...renderUrl(`${cv.professionalExperiences}`)) : null, + (cv.professionalSkills && cv.professionalSkills.length) + ? div( + cv.professionalSkills.map(tag => + a({ + href: `/search?query=%23${encodeURIComponent(tag)}`, + class: "tag-link", + style: "margin-right:0.8em;margin-bottom:0.5em;" + }, `#${tag}`) + ) + ) + : null + ]) : null, + hasOasis ? div({ class: "cv-box oasis" }, ...[ + h2(i18n.cvOasisContributorView), + cv.oasisExperiences ? p(...renderUrl(`${cv.oasisExperiences}`)) : null, + (cv.oasisSkills && cv.oasisSkills.length) + ? div( + cv.oasisSkills.map(tag => + a({ + href: `/search?query=%23${encodeURIComponent(tag)}`, + class: "tag-link", + style: "margin-right:0.8em;margin-bottom:0.5em;" + }, `#${tag}`) + ) + ) + : null + ]) : null, + hasAvailability ? div({ class: "cv-box availability" }, ...[ + h2(i18n.cvAvailabilityView), + cv.location ? p(`${i18n.cvLocationLabel}: ${cv.location}`) : null, + cv.status ? p(`${i18n.cvStatusLabel}: ${cv.status}`) : null, + cv.preferences ? p(`${i18n.cvPreferencesLabel}: ${cv.preferences}`) : null + ]) : null + ]) + ) ) ); }; + diff --git a/src/views/data_view.js b/src/views/data_view.js deleted file mode 100644 index 9878c3a..0000000 --- a/src/views/data_view.js +++ /dev/null @@ -1,172 +0,0 @@ -const { div, h2, p, section, button, form, a, input, span, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink } = require("./main_views"); - -const KIND_LABEL = { - inhabitants: () => i18n.dataKindInhabitants, - jobs: () => i18n.dataKindJobs, - projects: () => i18n.dataKindProjects, - events: () => i18n.dataKindEvents, - tribes: () => i18n.dataKindTribes, - market: () => i18n.dataKindMarket, - housing: () => i18n.dataKindHousing, - industry: () => i18n.dataKindIndustry, - tasks: () => i18n.dataKindTasks, - reports: () => i18n.dataKindReports, - votes: () => i18n.dataKindVotes, - audios: () => i18n.audioTitle, - videos: () => i18n.videoTitle, - images: () => i18n.imageTitle, - documents: () => i18n.documentTitle, - bookmarks: () => i18n.bookmarkTitle, - torrents: () => i18n.typeTorrent, - chats: () => i18n.chatsTitle, - pads: () => i18n.padsTitle, - maps: () => i18n.mapTitle, - calendars: () => i18n.calendarsTitle, - forum: () => i18n.forumTitle -}; - -const FILTER_COLUMNS = [ - ["ALL", "RECENT", "TOP"], - ["INHABITANTS", "TRIBES"], - ["VOTES", "EVENTS", "CALENDARS", "TASKS", "REPORTS"], - ["MARKET", "HOUSING", "JOBS", "PROJECTS", "INDUSTRY"], - ["FORUM", "CHATS", "PADS", "MAPS"], - ["AUDIOS", "BOOKMARKS", "DOCUMENTS", "IMAGES", "TORRENTS", "VIDEOS"] -]; - -const kindLabel = (kind) => (KIND_LABEL[kind] ? KIND_LABEL[kind]() : String(kind || '').toUpperCase()); - -const filterLabel = (key) => { - const map = { - ALL: i18n.dataFilterAll, MINE: i18n.dataFilterMine, - RECENT: i18n.dataFilterRecent, TOP: i18n.dataFilterTop - }; - return map[key] || kindLabel(key.toLowerCase()); -}; - -const filterButton = (mode, current, q) => - form({ method: "GET", action: "/data" }, - input({ type: "hidden", name: "q", value: q || "" }), - button({ - type: "submit", name: "filter", value: mode, - class: current === mode ? "filter-btn active" : "filter-btn" - }, String(filterLabel(mode)).toUpperCase()) - ); - -const renderFilters = (current, q) => - section( - div({ class: "activity-filter-grid" }, - ...FILTER_COLUMNS.map(col => - div({ class: "activity-filter-col" }, - ...col.map(mode => filterButton(mode, current, q)) - ) - ) - ), - div({ class: "data-search" }, - form({ method: "GET", action: "/data", class: "filter-box" }, - input({ type: "hidden", name: "filter", value: current }), - input({ type: "text", name: "q", value: q || "", placeholder: i18n.dataSearchPlaceholder, class: "filter-box__input" }), - div({ class: "filter-box__controls" }, - button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) - ) - ) - ) - ); - -const scorePct = (score) => ((Number(score) || 0) * 100).toFixed(1).replace(/\.0$/, ''); -const scaleOf = (pct) => Math.min(4, Math.floor((Number(pct) || 0) / 20)); - -const renderCohesion = (c) => { - const measurable = c.entities >= 2; - const pct = measurable ? Number(c.percent) || 0 : 0; - return section( - div({ class: "tags-header" }, - h2(i18n.dataCohesionTitle), - p(i18n.dataCohesionHint) - ), - div({ class: "data-cohesion" }, - p({ class: "data-cohesion-value" }, - span({ class: "card-label data-cohesion-label" }, `${i18n.dataCcShort}: `), - measurable ? `${c.percent}%` : "—"), - div({ class: "poll-bar data-cohesion-bar" }, - div({ class: `poll-bar-fill poll-bar-fill-${Math.round(pct / 5) * 5} data-scale-${scaleOf(pct)}` })), - div({ class: "data-cohesion-figures" }, - div({ class: "data-figure" }, - span({ class: "data-figure-value" }, String(c.entities)), - span({ class: "data-figure-label" }, i18n.dataStatEntities) - ), - div({ class: "data-figure" }, - span({ class: "data-figure-value" }, String(c.distinctTerms)), - span({ class: "data-figure-label" }, i18n.dataStatTerms) - ), - div({ class: "data-figure" }, - span({ class: "data-figure-value" }, String(c.pairs)), - span({ class: "data-figure-label" }, i18n.dataStatPairs) - ), - div({ class: "data-figure" }, - span({ class: "data-figure-value" }, String(c.isolated || 0)), - span({ class: "data-figure-label" }, i18n.dataStatIsolated) - ), - div({ class: "data-figure" }, - span({ class: "data-figure-value" }, String(c.connected || 0)), - span({ class: "data-figure-label" }, i18n.dataStatConnected) - ) - ) - ) - ); -}; - -const shortId = (id) => { - const value = String(id || ""); - return value.length > 14 ? `${value.slice(0, 12)}…` : value; -}; - -const renderMatchRow = (m, isBest) => { - const common = Array.isArray(m.common) ? m.common : []; - return div({ class: isBest ? "data-card data-best-card" : "data-card" }, - div({ class: "data-side-top" }, - span({ class: "data-kind-chip" }, String(kindLabel(m.kind)).toUpperCase()), - span({ class: "card-label data-score-label" }, `${isBest ? i18n.dataBestMatch : (i18n.dataCcShort || "CC")}: `), - span({ class: isBest ? "data-score-value data-best-score" : "data-score-value" }, `${scorePct(m.score)}%`), - a({ href: m.href, class: "btn-singleview btn-content data-side-visit", title: i18n.visitContent }, "↗") - ), - a({ href: m.href, class: "data-card-title" }, m.title || shortId(m.id)), - span({ class: "data-card-author" }, userLink(m.author)), - common.length - ? div({ class: "data-common-row" }, - span({ class: "card-label" }, `${i18n.dataCommonTerms}: `), - ...common.slice(0, 8).map(t => - a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`)) - ) - : null - ); -}; - -exports.dataView = async (payload = {}) => { - const filter = String(payload.filter || 'ALL').toUpperCase(); - const q = payload.q || ''; - - const matches = Array.isArray(payload.matches) ? payload.matches : []; - - return template( - i18n.dataTitle, - section(div({ class: "tags-header" }, h2(i18n.dataTitle), p(i18n.dataDescription))), - renderFilters(filter, q), - payload.cohesion ? renderCohesion(payload.cohesion) : null, - section( - div({ class: "tags-header" }, - h2(q ? `${i18n.dataTopicTitle} #${q}` : i18n.dataMatchesTitle), - p(q ? i18n.dataTopicHint : i18n.dataMatchesHint), - typeof payload.total === "number" && payload.total > matches.length - ? p({ class: "data-cohesion-hint" }, `${matches.length} / ${payload.total}`) - : null - ), - matches.length - ? div({ class: "data-list" }, ...matches.map((m, idx) => renderMatchRow(m, idx === 0))) - : p({ class: "no-content" }, payload.hasProfile === false - ? i18n.dataNoProfile - : i18n.dataNoMatches) - ) - ); -}; diff --git a/src/views/dev_view.js b/src/views/dev_view.js deleted file mode 100644 index 90d705f..0000000 --- a/src/views/dev_view.js +++ /dev/null @@ -1,275 +0,0 @@ -const { form, button, div, h2, p, section, input, a, span, ul, li, table, thead, tbody, tr, td, th, pre, code, select, option } = require("../server/node_modules/hyperaxe") -const { template, i18n } = require("./main_views") - -const FILTERS = [ - { key: "tree", href: "/dev", i18n: "devFilterFiles", fallback: "FILES" }, - { key: "map", href: "/dev/map", i18n: "devFilterModules", fallback: "MODULES" }, - { key: "translations", href: "/dev?group=translations", i18n: "devFilterTranslations", fallback: "TRANSLATIONS" }, - { key: "styles", href: "/dev?group=styles", i18n: "devFilterStyles", fallback: "STYLES" }, - { key: "tests", href: "/dev?group=tests", i18n: "devFilterTests", fallback: "TESTS" } -] - -const ICONS = { - folder: "📁", - parent: "↰", - ".js": "📜", - ".mjs": "📜", - ".json": "🧾", - ".css": "🎨", - ".md": "📖", - ".sh": "⌨", - ".html": "🌐", - ".txt": "📄", - ".yml": "🧾", - ".yaml": "🧾" -} - -const safeText = (v) => String(v == null ? "" : v) - -const iconFor = (file) => (file.viewable ? (ICONS[file.ext] || "📄") : "📦") - -const fmtSize = (bytes) => { - const n = Number(bytes) || 0 - if (n < 1024) return `${n} B` - if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB` - return `${(n / (1024 * 1024)).toFixed(1)} MB` -} - -const fmtCount = (n) => String(Number(n) || 0) - -const fileHref = (path, from) => `/dev/file?path=${encodeURIComponent(path)}${from ? `&from=${from}` : ""}` -const dirHref = (path) => (path ? `/dev?path=${encodeURIComponent(path)}` : "/dev") - -const renderHeader = (active) => [ - div({ class: "tags-header" }, - h2(i18n.devTitle || "Developer"), - p(i18n.devDescription || "Explore the source code running on this node.") - ), - div({ class: "filters" }, - div({ class: "ui-toolbar ui-toolbar--filters dev-toolbar" }, - FILTERS.map(f => a({ - class: active === f.key ? "filter-btn active" : "filter-btn", - href: f.href - }, i18n[f.i18n] || f.fallback)) - ) - ) -] - -const groupLabel = (key) => { - const f = FILTERS.find(x => x.key === key) - return f ? (i18n[f.i18n] || f.fallback) : String(key || "") -} - -const renderPathRow = (parents, currentIsFile, group) => (group - ? div({ class: "dev-breadcrumb" }, - a({ href: "/dev" }, i18n.devRoot || "root"), - span({ class: "dev-breadcrumb-sep" }, " / "), - span({ class: "dev-breadcrumb-current" }, groupLabel(group))) - : renderBreadcrumb(parents, currentIsFile)) - -const renderStats = (stats) => { - const bit = (labelKey, fallback, value) => [ - span({ class: "dev-label" }, `${i18n[labelKey] || fallback}: `), - span(String(value)) - ] - return div({ class: "dev-stats" }, - p( - ...bit("devVersion", "Version", stats.version || "-"), span({ class: "dev-sep" }, " · "), - ...bit("devNodeVersion", "Node", stats.node || "-"), span({ class: "dev-sep" }, " · "), - ...bit("devStatsFiles", "Files", fmtCount(stats.files)), span({ class: "dev-sep" }, " · "), - ...bit("devStatsLines", "Lines", fmtCount(stats.lines)), span({ class: "dev-sep" }, " · "), - ...bit("devStatsModules", "Modules", fmtCount(stats.modules)), span({ class: "dev-sep" }, " · "), - ...bit("devStatsLanguages", "Languages", fmtCount(stats.languages)), span({ class: "dev-sep" }, " · "), - ...bit("devStatsTests", "Test suites", fmtCount(stats.testMods)) - ) - ) -} - -const renderBreadcrumb = (parents, currentIsFile) => div({ class: "dev-breadcrumb" }, - a({ href: "/dev" }, i18n.devRoot || "root"), - (parents || []).map((seg, idx) => [ - span({ class: "dev-breadcrumb-sep" }, " / "), - (currentIsFile && idx === parents.length - 1) - ? span({ class: "dev-breadcrumb-current" }, safeText(seg.name)) - : a({ href: dirHref(seg.path) }, safeText(seg.name)) - ]) -) - -const renderSearchForm = (query, ext) => div({ class: "dev-search" }, - form({ method: "GET", action: "/dev/search", class: "filter-box" }, - input({ type: "text", name: "q", value: safeText(query), placeholder: i18n.devSearchPlaceholder || "Text to find in the code", minlength: "2", required: true, class: "filter-box__input" }), - div({ class: "filter-box__controls" }, - select({ name: "ext", class: "filter-box__select" }, - option({ value: "", ...(ext ? {} : { selected: true }) }, i18n.devAnyExtension || "Any file"), - [".js", ".json", ".css", ".md", ".sh"].map(x => option({ value: x, ...(ext === x ? { selected: true } : {}) }, x)) - ), - button({ type: "submit", class: "filter-btn" }, i18n.devFilterSearch || "SEARCH") - ) - ) -) - -const renderDesktop = (tree) => { - const dirs = (tree && tree.dirs) || [] - const files = (tree && tree.files) || [] - const parents = (tree && tree.parents) || [] - const items = [] - if (tree.path) { - const up = parents.length > 1 ? parents[parents.length - 2].path : "" - items.push(a({ class: "dev-icon dev-icon-up", href: dirHref(up) }, - span({ class: "dev-icon-glyph" }, ICONS.parent), - span({ class: "dev-icon-name" }, ".."), - span({ class: "dev-icon-meta" }, i18n.devParentFolder || "Parent folder") - )) - } - for (const d of dirs) { - items.push(a({ class: "dev-icon", href: dirHref(d.path) }, - span({ class: "dev-icon-glyph" }, ICONS.folder), - span({ class: "dev-icon-name" }, safeText(d.name)), - span({ class: "dev-icon-meta" }, i18n.devOpenFolder || "open") - )) - } - for (const f of files) { - const glyph = span({ class: "dev-icon-glyph" }, iconFor(f)) - const name = span({ class: "dev-icon-name" }, safeText(f.name)) - const meta = span({ class: "dev-icon-meta" }, f.viewable ? fmtSize(f.size) : (i18n.devNotViewable || "not viewable")) - items.push(f.viewable - ? a({ class: "dev-icon", href: fileHref(f.path) }, glyph, name, meta) - : div({ class: "dev-icon dev-icon-off" }, glyph, name, meta)) - } - if (!items.length) return p({ class: "dev-hint" }, i18n.devEmptyFolder || "This folder is empty.") - return div({ class: "dev-desktop" }, ...items) -} - -const renderPager = (file) => div({ class: "dev-pager" }, - file.hasPrev - ? a({ class: "filter-btn", href: fileHref(file.path, Math.max(1, file.from - file.pageLines)) }, i18n.devPrevPage || "Previous lines") - : null, - file.hasNext - ? a({ class: "filter-btn", href: fileHref(file.path, file.to + 1) }, i18n.devNextPage || "Next lines") - : null -) - -exports.devTreeView = async (tree, stats, active) => { - return template( - i18n.devTitle || "Developer", - section( - ...renderHeader(active || (tree.group ? tree.group : "tree")), - renderStats(stats), - renderSearchForm("", ""), - renderPathRow(tree.parents, false, tree.group), - renderDesktop(tree) - ) - ) -} - -exports.devFileView = async (file) => { - const reportTitle = `${file.path}:${file.from}` - const reportDescription = `${i18n.devReportContext || "Found while reading the Oasis source code"}: /${file.path} (${i18n.devLine || "line"} ${file.from})` - const taskTitle = `${i18n.devTaskPrefix || "Review"} ${file.path}` - return template( - i18n.devTitle || "Developer", - section( - ...renderHeader("tree"), - renderPathRow(file.parents, true, null), - div({ class: "dev-file-head" }, - span({ class: "dev-icon-glyph dev-file-glyph" }, iconFor({ ext: `.${String(file.name).split('.').pop()}`, viewable: true })), - h2({ class: "dev-file-name" }, safeText(file.name)) - ), - div({ class: "dev-file-meta" }, - p( - span({ class: "dev-label" }, `${i18n.devStatsLines || "Lines"}: `), fmtCount(file.totalLines), - span({ class: "dev-sep" }, " · "), - span({ class: "dev-label" }, `${i18n.devSize || "Size"}: `), fmtSize(file.size), - span({ class: "dev-sep" }, " · "), - span({ class: "dev-label" }, `${i18n.devShowing || "Showing"}: `), `${fmtCount(file.from)}-${fmtCount(file.to)}` - ) - ), - div({ class: "dev-actions" }, - form({ method: "GET", action: "/reports" }, - input({ type: "hidden", name: "filter", value: "create" }), - input({ type: "hidden", name: "category", value: "BUGS" }), - input({ type: "hidden", name: "title", maxlength: "100", value: reportTitle }), - input({ type: "hidden", name: "description", value: reportDescription }), - button({ type: "submit", class: "filter-btn" }, i18n.devReportBug || "Report Bug") - ), - form({ method: "GET", action: "/tasks" }, - input({ type: "hidden", name: "filter", value: "create" }), - input({ type: "hidden", name: "title", maxlength: "100", value: taskTitle }), - input({ type: "hidden", name: "description", value: reportDescription }), - button({ type: "submit", class: "filter-btn" }, i18n.devCreateTask || "Create Task") - ), - form({ method: "GET", action: "/dev/raw" }, - input({ type: "hidden", name: "path", value: file.path }), - button({ type: "submit", class: "filter-btn" }, i18n.devRaw || "RAW") - ) - ), - div({ class: "dev-code" }, - table({ class: "dev-code-table" }, - tbody( - file.lines.map(l => tr({ id: `L${l.n}`, class: "dev-code-row" }, - td({ class: "dev-code-num" }, a({ href: `${fileHref(file.path, file.from)}#L${l.n}` }, String(l.n))), - td({ class: "dev-code-line" }, pre(code(safeText(l.text)))) - )) - ) - ) - ), - renderPager(file) - ) - ) -} - -exports.devSearchView = async (result, ext) => { - const results = (result && result.results) || [] - return template( - i18n.devTitle || "Developer", - section( - ...renderHeader("search"), - renderSearchForm(result.query, ext), - result.query - ? p({ class: "dev-hint" }, `${fmtCount(results.length)}${result.truncated ? "+" : ""} ${i18n.devMatches || "matches"} · ${fmtCount(result.scanned)} ${i18n.devFilesScanned || "files scanned"}`) - : null, - results.length - ? ul({ class: "dev-list dev-results" }, - results.map(r => li({ class: "dev-entry" }, - div({ class: "dev-result-head" }, - span({ class: "dev-icon-glyph dev-result-glyph" }, ICONS[`.${r.path.split('.').pop()}`] || "📄"), - a({ href: `${fileHref(r.path, Math.max(1, r.line - 20))}#L${r.line}` }, `${safeText(r.path)}:${r.line}`) - ), - div({ class: "dev-result-text" }, pre(code(safeText(r.text)))) - )) - ) - : (result.query ? p({ class: "dev-hint" }, i18n.devNoResults || "No matches, yet.") : null) - ) - ) -} - -exports.devMapView = async (modules) => { - return template( - i18n.devTitle || "Developer", - section( - ...renderHeader("map"), - div({ class: "dev-listing" }, - table({ class: "dev-table" }, - thead(tr( - th(i18n.devColModule || "Module"), - th(i18n.devColState || "State"), - th(i18n.devColModel || "Model"), - th(i18n.devColView || "View"), - th(i18n.devColRoutes || "Routes"), - th(i18n.devColTests || "Tests") - )), - tbody( - (modules || []).map(m => tr( - td(safeText(m.name)), - td(m.enabled ? (i18n.devOn || "on") : (i18n.devOff || "off")), - td(m.model ? a({ href: fileHref(m.model) }, safeText(m.model)) : span({ class: "dev-hint" }, "-")), - td(m.view ? a({ href: fileHref(m.view) }, safeText(m.view)) : span({ class: "dev-hint" }, "-")), - td(String(m.routes || 0)), - td(m.tests ? a({ href: dirHref(m.tests) }, safeText(m.tests)) : span({ class: "dev-hint" }, "-")) - )) - ) - ) - ) - ) - ) -} diff --git a/src/views/document_view.js b/src/views/document_view.js index 70bcca7..e1e98f2 100644 --- a/src/views/document_view.js +++ b/src/views/document_view.js @@ -1,9 +1,8 @@ const { form, button, div, h2, p, section, input, label, br, a, span, textarea, select, option } = require("../server/node_modules/hyperaxe"); -const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); const { config } = require("../server/SSB_server.js"); const { renderUrl } = require("../backend/renderUrl"); @@ -34,6 +33,21 @@ const renderTags = (tags) => { : null; }; +const renderFavoriteToggle = (doc, returnTo) => + form( + { + method: "POST", + action: doc.isFavorite + ? `/documents/favorites/remove/${encodeURIComponent(doc.key)}` + : `/documents/favorites/add/${encodeURIComponent(doc.key)}` + }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + button( + { type: "submit", class: "filter-btn" }, + doc.isFavorite ? i18n.documentRemoveFavoriteButton : i18n.documentAddFavoriteButton + ) + ); + const renderDocumentActions = (filter, doc, params = {}) => { const returnTo = buildReturnTo(filter, params); const isAuthor = String(doc.author) === String(userId); @@ -59,11 +73,70 @@ const renderDocumentActions = (filter, doc, params = {}) => { }; const renderDocumentCommentsSection = (documentKey, rootId, comments = [], returnTo = null) => { - return renderSharedCommentsSection({ - action: `/documents/${encodeURIComponent(documentKey)}/comments`, - comments: comments, - returnTo: returnTo + const list = safeArr(comments).filter(c => { + const t = c && c.value && c.value.content && c.value.content.text; + return t && String(t).trim(); }); + const commentsCount = list.length; + + return div( + { class: "vote-comments-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), + span({ class: "card-value" }, String(commentsCount)) + ), + div( + { class: "comment-form-wrapper" }, + h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), + form( + { method: "POST", action: `/documents/${encodeURIComponent(documentKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + rootId ? input({ type: "hidden", name: "rootId", value: rootId }) : null, + textarea({ + id: "comment-text", + name: "text", + rows: 4, + class: "comment-textarea", + placeholder: i18n.voteNewCommentPlaceholder + }), + div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), + br(), + button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) + ) + ), + list.length + ? div( + { class: "comments-list" }, + list.map((c) => { + const author = c?.value?.author || ""; + const ts = c?.value?.timestamp || c?.timestamp; + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; + const relDate = ts ? moment(ts).fromNow() : ""; + + const content = c?.value?.content || {}; + const text = content.text || ""; + const threadRoot = content.fork || content.root || null; + + return div( + { class: "votations-comment-card" }, + span( + { class: "created-at" }, + span(i18n.createdBy), + author ? userLink(author) : span("(unknown)"), + absDate ? span(" | ") : "", + absDate ? span({ class: "votations-comment-date" }, absDate) : "", + relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", + relDate && threadRoot + ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) + : "" + ), + p({ class: "votations-comment-text" }, ...renderUrl(text)) + ); + }) + ) + : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) + ); }; const renderDocumentList = exports.renderDocumentList = (documents, filter, params = {}) => { @@ -90,19 +163,49 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para div( { class: "card-header activity-card-header" }, span(), - renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`, { spread: (params.spreadMap && params.spreadMap.get(doc.key)) || params.spreads || null, author: doc.author, favKind: 'documents', isFavorite: doc.isFavorite, reportTitle: doc.title }) + renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`) ), div( { class: "card-section document-card-body" }, + div( + { class: "bookmark-topbar" }, + div( + { class: "bookmark-topbar-left" }, + renderFavoriteToggle(doc, returnTo), + topbarLeft + ), + renderDocumentActions(filter, doc, params) + ), title ? h2(title) : null, doc.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(doc.lifetime, i18n)) : null, doc?.url ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` }) : p(i18n.documentNoFile), - renderEngagement(doc.key, - renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), - renderCommentsLink({ href: `/documents/${encodeURIComponent(doc.key)}`, count: commentCount }) + div( + { class: "card-comments-summary" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), + span({ class: "card-value" }, String(commentCount)), + br(), + br(), + form( + { method: "GET", action: `/documents/${encodeURIComponent(doc.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "filter", value: filter || "all" }), + params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, + params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, + button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) + ) ), + div( + { class: "card-comments-summary feed-opinions-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), + span({ class: "card-value" }, String(Object.values(doc.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) + ), + renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants) + ), + div({ class: "card-spread-left" }, renderSpreadButton(doc.key, (params.spreadMap && params.spreadMap.get(doc.key)) || params.spreads)), br(), (() => { const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN; @@ -111,12 +214,12 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), userLink(doc.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}` + ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` ) : null ); @@ -134,7 +237,6 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => { return div( { class: "div-center document-form" }, - params.spreadWarning || null, form( { action: filter === "edit" ? `/documents/update/${encodeURIComponent(documentId)}` : "/documents/create", @@ -149,7 +251,7 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => { br(), label(i18n.documentTitleLabel), br(), - input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.documentTitlePlaceholder, value: docToEdit?.title || "" }), + input({ type: "text", name: "title", placeholder: i18n.documentTitlePlaceholder, value: docToEdit?.title || "" }), br(), label(i18n.documentDescriptionLabel), br(), @@ -166,8 +268,20 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => { }; exports.documentView = async (documents, filter = "all", documentId = null, params = {}) => { - if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(documentId) }; - const title = i18n.documentTitle; + const title = + filter === "mine" + ? i18n.documentMineSectionTitle + : filter === "create" + ? i18n.documentCreateSectionTitle + : filter === "edit" + ? i18n.documentUpdateSectionTitle + : filter === "recent" + ? i18n.documentRecentSectionTitle + : filter === "top" + ? i18n.documentTopSectionTitle + : filter === "favorites" + ? i18n.documentFavoritesSectionTitle + : i18n.documentAllSectionTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); @@ -194,14 +308,14 @@ exports.documentView = async (documents, filter = "all", documentId = null, para { method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterMine).toUpperCase()), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase()), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterAll), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterMine), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterTop), button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - String(i18n.documentFilterFavorites).toUpperCase() + i18n.documentFilterFavorites ), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterTop).toUpperCase()), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterRecent), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.documentCreateButton) ) ) @@ -220,9 +334,9 @@ exports.documentView = async (documents, filter = "all", documentId = null, para { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.documentSortRecent), - option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.documentSortOldest), - option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.documentSortTop) + option({ value: "recent", selected: sort === "recent" }, i18n.documentSortRecent), + option({ value: "oldest", selected: sort === "oldest" }, i18n.documentSortOldest), + option({ value: "top", selected: sort === "top" }, i18n.documentSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.documentSearchButton) ) @@ -254,6 +368,7 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = ].filter(Boolean); const sideActions = []; + sideActions.push(renderFavoriteToggle(doc, returnTo)); if (doc.author && String(doc.author) !== String(userId)) { sideActions.push(form( { method: "GET", action: "/pm" }, @@ -278,17 +393,6 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = const tagsNode = renderTags(doc.tags); - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(doc.key, null, { - author: doc.author, - favKind: 'documents', - isFavorite: doc.isFavorite, - spread: params.spreads || null, - returnTo, - reportTitle: doc.title - }) - ); - const docSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, @@ -299,11 +403,11 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = ? p({ class: "tribe-side-description" }, ...renderUrl(doc.description)) : null, tagsNode, + div({ class: "card-spread-centered" }, renderSpreadButton(doc.key, params.spreads)), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const docMain = div({ class: "tribe-main" }, - detailActions, doc?.url ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` }) : p(i18n.documentNoFile), @@ -314,20 +418,18 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), userLink(doc.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}` + ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` ) : null ); })(), - renderEngagement(doc.key, - renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), - renderDocumentCommentsSection(doc.key, doc.rootId || doc.key, comments, returnTo) - ) + renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), + renderDocumentCommentsSection(doc.key, doc.rootId || doc.key, comments, returnTo) ); const tpl = template( @@ -343,14 +445,14 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = { method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterMine).toUpperCase()), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase()), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterAll), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterMine), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterTop), button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - String(i18n.documentFilterFavorites).toUpperCase() + i18n.documentFilterFavorites ), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterTop).toUpperCase()), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.documentFilterRecent), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.documentCreateButton) ) ), diff --git a/src/views/event_view.js b/src/views/event_view.js index 6b16f48..c3b29fa 100644 --- a/src/views/event_view.js +++ b/src/views/event_view.js @@ -1,8 +1,5 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td, details, summary, ul, li } = require("../server/node_modules/hyperaxe"); -const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderOpenClosedChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions } = require("./main_views"); -const { renderPhotoGallery, renderGalleryFields } = require("./gallery_view"); -const { renderIntervalBlock } = require("./calendars_view"); +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { template, i18n, renderOpinionsVoting, userLink, renderStateChip, renderOpenClosedChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderUrl } = require("../backend/renderUrl"); @@ -21,7 +18,7 @@ exports.renderEventInvitePage = (code) => { }; const opt = (value, isSelected, text) => - option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text); + option(Object.assign({ value }, isSelected ? { selected: "selected" } : {}), text); const safeArray = (v) => (Array.isArray(v) ? v : []); @@ -108,12 +105,70 @@ const renderEventAttendAction = (e, isAttending, returnTo) => { }; const renderEventCommentsSection = (eventId, comments = [], currentFilter = "all") => { + const commentsCount = Array.isArray(comments) ? comments.length : 0; const returnTo = `/events/${encodeURIComponent(eventId)}?filter=${encodeURIComponent(currentFilter || "all")}`; - return renderSharedCommentsSection({ - action: `/events/${encodeURIComponent(eventId)}/comments`, - comments: comments, - returnTo: returnTo - }); + + return div( + { class: "vote-comments-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), + span({ class: "card-value" }, String(commentsCount)) + ), + div( + { class: "comment-form-wrapper" }, + h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), + form( + { method: "POST", action: `/events/${encodeURIComponent(eventId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + textarea({ + id: "comment-text", + name: "text", + rows: 4, + class: "comment-textarea", + placeholder: i18n.voteNewCommentPlaceholder + }), + div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), + br(), + button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) + ) + ), + (() => { + const visibleComments = (comments || []).filter(c => { + const t = c && c.value && c.value.content && c.value.content.text; + return t && String(t).trim(); + }); + return visibleComments.length + ? div( + { class: "comments-list" }, + visibleComments.map((c) => { + const author = c.value && c.value.author ? c.value.author : ""; + const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp; + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; + const relDate = ts ? moment(ts).fromNow() : ""; + + const content = c.value && c.value.content ? c.value.content : {}; + const root = content.fork || content.root || ""; + const text = content.text || ""; + + return div( + { class: "votations-comment-card" }, + span( + { class: "created-at" }, + span(i18n.createdBy), + author ? userLink(author) : span("(unknown)"), + absDate ? span(" | ") : "", + absDate ? span({ class: "votations-comment-date" }, absDate) : "", + relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", + relDate && root ? a({ href: `/thread/${encodeURIComponent(root)}#${encodeURIComponent(c.key)}` }, relDate) : "" + ), + p({ class: "votations-comment-text" }, ...renderUrl(String(text))) + ); + }) + ) + : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet); + })() + ); }; const renderEventStatusChip = (status) => { @@ -143,7 +198,7 @@ const renderEventItem = exports.renderEventItem = (e, filter, spreadInfo) => { div( { class: "card-header activity-card-header" }, span(), - renderContentActions(e.id, `/events/${encodeURIComponent(e.id)}?filter=${encodeURIComponent(currentFilter)}`, { spread: spreadInfo || null, author: e.organizer || e.author, favKind: 'events', isFavorite: e.isFavorite, reportTitle: e.title }) + renderContentActions(e.id, `/events/${encodeURIComponent(e.id)}?filter=${encodeURIComponent(currentFilter)}`) ), div({ class: "card-section event-card-body" }, div({ class: "shop-title-row" }, @@ -161,7 +216,8 @@ const renderEventItem = exports.renderEventItem = (e, filter, spreadInfo) => { : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${attendees.length}`) - ) + ), + div({ class: "card-spread-centered" }, renderSpreadButton(e.id, spreadInfo)) ) ); }; @@ -172,12 +228,14 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { const { renderReachChip: renderReachChipEvents } = require('./clearnet_view'); const viewerClearnetEvents = !!(params.viewerPrefs && params.viewerPrefs.clearnetEvents); - const title = i18n.eventsTitle; + const title = + currentFilter === "mine" ? i18n.eventMineSectionTitle : + currentFilter === "create" ? i18n.eventCreateSectionTitle : + currentFilter === "edit" ? i18n.eventUpdateSectionTitle : + i18n.eventAllSectionTitle; const eventToEdit = list.find((e) => e.id === eventId) || {}; - const formData = currentFilter === "edit" ? eventToEdit : (params.draft || {}); - const spreadWarning = currentFilter === "edit" ? await renderSpreadEditWarning(eventToEdit && eventToEdit.id) : null; - const editTags = Array.isArray(formData.tags) ? formData.tags.filter(Boolean) : (typeof formData.tags === "string" ? formData.tags.split(",").map(t => t.trim()).filter(Boolean) : []); + const editTags = Array.isArray(eventToEdit.tags) ? eventToEdit.tags.filter(Boolean) : []; const canSee = (e) => { const isPub = normalizePrivacy(e.isPublic) === "public"; @@ -212,12 +270,7 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { const minCreate = moment().add(1, "minute").format("YYYY-MM-DDTHH:mm"); const ret = typeof returnTo === "string" && returnTo.startsWith("/events") ? returnTo : "/events?filter=mine"; - const editPrivacy = normalizePrivacy(formData.isPublic); - const editInterval = formData.interval - || (formData.intervalWeekly ? "weekly" : formData.intervalMonthly ? "monthly" : formData.intervalYearly ? "yearly" : ""); - const editIntervalUntil = (formData.recurrenceUntil || formData.intervalDeadline) - ? moment(formData.recurrenceUntil || formData.intervalDeadline).format("YYYY-MM-DDTHH:mm") - : ""; + const editPrivacy = normalizePrivacy(eventToEdit.isPublic); return template( title, @@ -232,33 +285,21 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { { class: "filters" }, form( { method: "GET", action: "/events" }, - button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterAll).toUpperCase()), - button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMine).toUpperCase()), - button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterToday).toUpperCase()), - button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterWeek).toUpperCase()), - button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMonth).toUpperCase()), - button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterYear).toUpperCase()), - button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterArchived).toUpperCase()), + button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterAll), + button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMine), + button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterToday), + button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterWeek), + button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMonth), + button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterYear), + button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterArchived), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.eventCreateButton) ) - ), - currentFilter === "edit" || currentFilter === "create" - ? null - : div({ class: "filters" }, - form({ method: "GET", action: "/events", class: "filter-box" }, - input({ type: "hidden", name: "filter", value: currentFilter }), - input({ type: "text", name: "q", value: params.q || "", placeholder: i18n.eventSearchPlaceholder, class: "filter-box__input" }), - div({ class: "filter-box__controls" }, - button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) - ) - ) - ) + ) ), section( currentFilter === "edit" || currentFilter === "create" ? div( { class: "event-form" }, - spreadWarning, form( { action: currentFilter === "edit" ? `/events/update/${encodeURIComponent(eventId)}` : "/events/create", @@ -270,20 +311,23 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { br(), input({ type: "text", - name: "title", maxlength: "100", + name: "title", id: "title", required: true, - value: formData.title || "" + value: currentFilter === "edit" ? eventToEdit.title || "" : "" }), br(), label(i18n.eventDescriptionLabel), br(), textarea( { name: "description", id: "description", placeholder: i18n.eventDescriptionPlaceholder, rows: "4" }, - formData.description || "" + currentFilter === "edit" ? eventToEdit.description || "" : "" ), br(), - ...renderGalleryFields(formData, currentFilter === "edit"), + label(i18n.uploadMedia), + br(), + input({ type: "file", name: "image", accept: "image/*" }), + br(), br(), label(i18n.eventDateLabel), br(), @@ -293,10 +337,19 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { id: "date", required: true, min: currentFilter === "create" ? minCreate : undefined, - value: formData.date ? moment(formData.date).format("YYYY-MM-DDTHH:mm") : "" + value: currentFilter === "edit" && eventToEdit.date ? moment(eventToEdit.date).format("YYYY-MM-DDTHH:mm") : "" }), br(), br(), + label(i18n.eventPrivacyLabel), + br(), + select( + { name: "isPublic", id: "isPublic" }, + opt("public", editPrivacy !== "private", i18n.eventPublic), + opt("private", editPrivacy === "private", i18n.eventPrivate) + ), + br(), + br(), label(i18n.eventLocationLabel), br(), input({ @@ -304,17 +357,17 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { name: "location", id: "location", required: true, - value: formData.location || "" + value: currentFilter === "edit" ? eventToEdit.location || "" : "" }), br(), label(i18n.mapLocationTitle || "Map Location"), br(), - input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: formData.mapUrl || "" }), + input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: eventToEdit?.mapUrl || "" }), br(), br(), label(i18n.eventUrlLabel), br(), - input({ type: "url", name: "url", id: "url", value: formData.url || "" }), + input({ type: "url", name: "url", id: "url", value: currentFilter === "edit" ? eventToEdit.url || "" : "" }), br(), br(), label(i18n.eventPriceLabel), @@ -325,28 +378,13 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { id: "price", min: "0.000000", step: "0.000001", - value: parseFloat(formData.price || 0).toFixed(6) + value: currentFilter === "edit" ? parseFloat(eventToEdit.price || 0).toFixed(6) : (0).toFixed(6) }), br(), br(), label(i18n.eventTagsLabel), br(), - input({ type: "text", name: "tags", id: "tags", value: editTags.join(", ") }), - br(), - br(), - div({ class: "event-recurrence-block" }, - label(i18n.eventRecurrenceLabel), - br(), - renderIntervalBlock({ min: minCreate }, editInterval, editIntervalUntil) - ), - br(), - label(i18n.eventPrivacyLabel), - br(), - select( - { name: "isPublic", id: "isPublic" }, - opt("public", editPrivacy !== "private", i18n.eventPublic), - opt("private", editPrivacy === "private", i18n.eventPrivate) - ), + input({ type: "text", name: "tags", id: "tags", value: currentFilter === "edit" ? editTags.join(", ") : "" }), br(), br(), ...(currentFilter === "create" ? [ @@ -381,13 +419,13 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { { class: "filters" }, form( { method: "GET", action: "/events" }, - button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterAll).toUpperCase()), - button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMine).toUpperCase()), - button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterToday).toUpperCase()), - button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterWeek).toUpperCase()), - button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterMonth).toUpperCase()), - button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterYear).toUpperCase()), - button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, String(i18n.eventFilterArchived).toUpperCase()), + button({ type: "submit", name: "filter", value: "all", class: currentFilter === "all" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterAll), + button({ type: "submit", name: "filter", value: "mine", class: currentFilter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMine), + button({ type: "submit", name: "filter", value: "today", class: currentFilter === "today" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterToday), + button({ type: "submit", name: "filter", value: "week", class: currentFilter === "week" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterWeek), + button({ type: "submit", name: "filter", value: "month", class: currentFilter === "month" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterMonth), + button({ type: "submit", name: "filter", value: "year", class: currentFilter === "year" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterYear), + button({ type: "submit", name: "filter", value: "archived", class: currentFilter === "archived" ? "filter-btn active" : "filter-btn" }, i18n.eventFilterArchived), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.eventCreateButton) ) ); @@ -417,14 +455,20 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { ].filter(Boolean); const sideActions = []; + if (event.organizer && event.organizer !== userId) { + sideActions.push(form({ method: "GET", action: "/pm" }, + input({ type: "hidden", name: "recipients", value: event.organizer }), + button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) + )); + } const attendNode = renderEventAttendAction(event, isAttending, returnToSelf); if (attendNode) sideActions.push(attendNode); if (params.linkedCalendarId) { sideActions.push(form({ method: "GET", action: `/calendars/${encodeURIComponent(params.linkedCalendarId)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.eventVisitCalendar || "Visit calendar") + button({ type: "submit", class: "filter-btn" }, i18n.eventVisitCalendar || "Visit calendar") )); } - const ownerActions = isOrganizer ? renderEventOwnerActions(event, returnToSelf) : []; + if (isOrganizer) sideActions.push(...renderEventOwnerActions(event, returnToSelf)); const tagsNode = event.tags && event.tags.filter(Boolean).length ? div({ class: "card-tags" }, @@ -438,16 +482,7 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { td({ class: "tribe-info-label" }, labelText), td({ class: "tribe-info-value" }, valueNode) )); - if (event.date) pushRow(i18n.eventDateLabel, moment(event.recurring && event.nextDate ? event.nextDate : event.date).format("YYYY/MM/DD HH:mm")); - if (event.recurring) { - const parts = [ - event.intervalWeekly ? (i18n.calendarIntervalWeekly || "Weekly") : null, - event.intervalMonthly ? (i18n.calendarIntervalMonthly || "Monthly") : null, - event.intervalYearly ? (i18n.calendarIntervalYearly || "Yearly") : null - ].filter(Boolean); - pushRow(i18n.eventRecurrenceLabel, parts.join(" · ")); - if (event.recurrenceUntil) pushRow(i18n.eventRecurrenceUntil, moment(event.recurrenceUntil).format("YYYY/MM/DD HH:mm")); - } + if (event.date) pushRow(i18n.eventDateLabel, moment(event.date).format("YYYY/MM/DD HH:mm")); pushRow(i18n.eventStatus, eventStatusLabel(event.status)); pushRow(i18n.eventPrivacyLabel, privacyLabel(event.isPublic)); if (event.location && String(event.location).trim()) pushRow(i18n.eventLocationLabel, event.location); @@ -462,31 +497,18 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { : null; const eventSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(event.id, null, { spread: params.spreads || null, author: event.organizer || event.author, favKind: 'events', isFavorite: event.isFavorite, reportTitle: event.title }) - ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, event.title) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - renderPhotoGallery(event, 'event'), + div({ class: "card-spread-centered" }, renderSpreadButton(event.id, params.spreads)), table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), - event.recurring && Array.isArray(event.occurrences) && event.occurrences.length > 1 - ? details({ class: "event-occurrences" }, - summary({ class: "chat-thread-summary" }, `${i18n.eventUpcomingDates} (${event.occurrences.length})`), - ul({ class: "event-occurrence-list" }, - ...event.occurrences.map(d => li(moment(d).format("YYYY/MM/DD HH:mm"))) - ) - ) - : null, tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${attendees.length}`) ), attendeesListNode, - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, - ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, - renderDocumentActions('events', event.id) + sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const returnToOpinions = `/events/${encodeURIComponent(event.id)}?filter=${encodeURIComponent(currentFilter)}`; @@ -504,13 +526,13 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { span({ class: "date-link" }, `${moment(event.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), userLink(event.organizer) ), - renderEngagement(event.id, opinionsBar, renderEventCommentsSection(event.id, comments, currentFilter)) + opinionsBar, + renderEventCommentsSection(event.id, comments, currentFilter) ); return template( event.title, section( - div({ class: "tags-header" }, h2(i18n.eventsTitle), p(i18n.eventsDescription)), filterBar, div({ class: "tribe-details" }, eventSide, eventMain) ) @@ -521,7 +543,7 @@ exports.clearnetEventView = async (event) => { const { escapeHtml: esc, renderClearnetPage } = require('./clearnet_view'); const title = esc(event.title || 'Event'); const desc = esc(event.description || ''); - const dateStr = event.date ? esc(moment(event.date).format("YYYY/MM/DD HH:mm")) : ''; + const dateStr = event.date ? esc(moment(event.date).format("YYYY-MM-DD HH:mm")) : ''; const loc = esc(event.location || ''); const price = parseFloat(event.price || 0); const priceStr = price > 0 ? `${price.toFixed(2)} ECO` : ''; diff --git a/src/views/favorites_view.js b/src/views/favorites_view.js index 0260a3c..a5e3106 100644 --- a/src/views/favorites_view.js +++ b/src/views/favorites_view.js @@ -10,30 +10,6 @@ const userId = config.keys.id; const safeArr = (v) => (Array.isArray(v) ? v : []); const safeText = (v) => String(v || "").trim(); -const FILTER_KINDS = [ - { value: "audios", label: () => i18n.favoritesFilterAudios }, - { value: "bookmarks", label: () => i18n.favoritesFilterBookmarks }, - { value: "calendars", label: () => i18n.favoritesFilterCalendars }, - { value: "chats", label: () => i18n.favoritesFilterChats }, - { value: "documents", label: () => i18n.favoritesFilterDocuments }, - { value: "events", label: () => i18n.favoritesFilterEvents }, - { value: "forum", label: () => i18n.favoritesFilterForum }, - { value: "housing", label: () => i18n.favoritesFilterHousing }, - { value: "images", label: () => i18n.favoritesFilterImages }, - { value: "jobs", label: () => i18n.favoritesFilterJobs }, - { value: "maps", label: () => i18n.favoritesFilterMaps }, - { value: "market", label: () => i18n.favoritesFilterMarket }, - { value: "pads", label: () => i18n.favoritesFilterPads }, - { value: "projects", label: () => i18n.favoritesFilterProjects }, - { value: "reports", label: () => i18n.favoritesFilterReports }, - { value: "shopProducts", label: () => i18n.favoritesFilterShopProducts }, - { value: "tasks", label: () => i18n.favoritesFilterTasks }, - { value: "torrents", label: () => i18n.favoritesFilterTorrents }, - { value: "transfers", label: () => i18n.favoritesFilterTransfers }, - { value: "videos", label: () => i18n.favoritesFilterVideos }, - { value: "votes", label: () => i18n.favoritesFilterVotes } -]; - const buildReturnTo = (filter) => { const f = safeText(filter || "all"); return `/favorites?filter=${encodeURIComponent(f)}`; @@ -49,47 +25,6 @@ const renderTags = (tags) => { : null; }; -const DETAIL_KEYS = [ - "status", "category", "severity", "priority", "location", "place", - "date", "deadline", "startTime", "endTime", "price", "salary", "amount", - "concept", "question", "housing_type", "property_type", "item_type", "sector" -]; - -const fieldLabel = (key) => { - const camel = key.replace(/_(.)/g, (_, c) => c.toUpperCase()); - return i18n[camel + "Label"] || i18n["search" + camel.charAt(0).toUpperCase() + camel.slice(1)] || camel.toUpperCase(); -}; - -const fieldValue = (v) => { - if (v === null || v === undefined) return ""; - if (Array.isArray(v)) return v.filter(Boolean).join(", "); - if (typeof v === "object") return ""; - return String(v).trim(); -}; - -const renderDetailFields = (item) => { - const content = item && item.content; - if (!content || typeof content !== "object") return null; - const rows = DETAIL_KEYS - .map((k) => [k, fieldValue(content[k])]) - .filter(([, v]) => v) - .map(([k, v]) => - div({ class: "card-field" }, - span({ class: "card-label" }, `${fieldLabel(k)}: `), - span({ class: "card-value" }, v) - ) - ); - const members = ["members", "participants", "attendees", "assignees", "confirmedBy"] - .map((k) => [k, Array.isArray(content[k]) ? content[k].length : null]) - .find(([, n]) => typeof n === "number" && n > 0); - if (members) { - rows.push(div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${fieldLabel(members[0])}: ${members[1]}`) - )); - } - return rows.length ? div({ class: "cv-card-fields" }, ...rows) : null; -}; - const renderBookmarkUrl = (item) => { if (item.kind !== "bookmarks") return null; if (!item.url) return null; @@ -125,7 +60,7 @@ const renderFavoriteCard = (item, filter) => { const title = safeText(item.title) || safeText(item.name) || safeText(item.category) || safeText(item.url) || ""; const ts = item.updatedAt || item.createdAt; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : ""; + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; const isOwn = item.author && String(item.author) === String(userId); return div( @@ -160,7 +95,6 @@ const renderFavoriteCard = (item, filter) => { renderImagePreview(item), renderBookmarkUrl(item), safeText(item.description) ? p(...renderUrl(item.description)) : null, - renderDetailFields(item), renderTags(item.tags), p( { class: "card-footer" }, @@ -171,7 +105,7 @@ const renderFavoriteCard = (item, filter) => { ); }; -exports.favoritesView = async (items, filter = "all", counts = {}, q = "") => { +exports.favoritesView = async (items, filter = "all", counts = {}) => { const c = counts || {}; const total = typeof c.all === "number" ? c.all : safeArr(items).length; @@ -191,22 +125,53 @@ exports.favoritesView = async (items, filter = "all", counts = {}, q = "") => { { type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, `${i18n.favoritesFilterRecent} (${total})` ), - ...FILTER_KINDS.map((k) => - button( - { type: "submit", name: "filter", value: k.value, class: filter === k.value ? "filter-btn active" : "filter-btn" }, - `${k.label()} (${c[k.value] || 0})` - ) - ) - ) - ), - div( - { class: "filters" }, - form( - { method: "GET", action: "/favorites", class: "filter-box" }, - input({ type: "hidden", name: "filter", value: filter }), - input({ type: "text", name: "q", value: q, placeholder: i18n.favoritesSearchPlaceholder, class: "filter-box__input" }), - div({ class: "filter-box__controls" }, - button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) + button( + { type: "submit", name: "filter", value: "audios", class: filter === "audios" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterAudios} (${c.audios || 0})` + ), + button( + { type: "submit", name: "filter", value: "bookmarks", class: filter === "bookmarks" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterBookmarks} (${c.bookmarks || 0})` + ), + button( + { type: "submit", name: "filter", value: "documents", class: filter === "documents" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterDocuments} (${c.documents || 0})` + ), + button( + { type: "submit", name: "filter", value: "images", class: filter === "images" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterImages} (${c.images || 0})` + ), + button( + { type: "submit", name: "filter", value: "maps", class: filter === "maps" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterMaps} (${c.maps || 0})` + ), + button( + { type: "submit", name: "filter", value: "pads", class: filter === "pads" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterPads || "PADS"} (${c.pads || 0})` + ), + button( + { type: "submit", name: "filter", value: "chats", class: filter === "chats" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterChats || "CHATS"} (${c.chats || 0})` + ), + button( + { type: "submit", name: "filter", value: "calendars", class: filter === "calendars" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterCalendars || "CALENDARS"} (${c.calendars || 0})` + ), + button( + { type: "submit", name: "filter", value: "videos", class: filter === "videos" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterVideos} (${c.videos || 0})` + ), + button( + { type: "submit", name: "filter", value: "torrents", class: filter === "torrents" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterTorrents || "TORRENTS"} (${c.torrents || 0})` + ), + button( + { type: "submit", name: "filter", value: "market", class: filter === "market" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterMarket || "MARKET"} (${c.market || 0})` + ), + button( + { type: "submit", name: "filter", value: "shopProducts", class: filter === "shopProducts" ? "filter-btn active" : "filter-btn" }, + `${i18n.favoritesFilterShopProducts || "SHOP ITEMS"} (${c.shopProducts || 0})` ) ) ), diff --git a/src/views/fediverse_view.js b/src/views/fediverse_view.js index dcbd33d..c651941 100644 --- a/src/views/fediverse_view.js +++ b/src/views/fediverse_view.js @@ -6,7 +6,7 @@ const moment = require("../server/node_modules/moment"); const fmtDate = (ts) => { if (!ts) return ""; const m = moment(ts); - return m.isValid() ? m.format("YYYY/MM/DD HH:mm") : ""; + return m.isValid() ? m.format("YYYY-MM-DD HH:mm") : ""; }; const renderMedia = (m) => { @@ -111,7 +111,7 @@ const mastodonBox = (account, stats, actions, showLabel) => { const disconnectedBlock = () => { const text = String(i18n.fediverseDisconnected || ""); - const settingsLink = a({ href: "/settings#multiverse" }, i18n.fediverseEmptyLink || "settings"); + const settingsLink = a({ href: "/settings#fediverse" }, i18n.fediverseEmptyLink || "settings"); const parts = text.split("%LINK%"); const para = parts.length > 1 ? p(parts[0], settingsLink, parts.slice(1).join("%LINK%")) diff --git a/src/views/feed_view.js b/src/views/feed_view.js index 359f9f2..8e1cf97 100644 --- a/src/views/feed_view.js +++ b/src/views/feed_view.js @@ -1,6 +1,5 @@ const { div, h2, p, section, button, form, a, span, textarea, br, input, h1, label } = require("../server/node_modules/hyperaxe"); -const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, userLink, renderContentActions, renderEngagement } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, userLink, renderContentActions } = require("./main_views"); const { config } = require("../server/SSB_server.js"); const { renderTextWithStyles } = require("../backend/renderTextWithStyles"); const moment = require("../server/node_modules/moment"); @@ -57,7 +56,7 @@ const generateFilterButtons = (filters, currentFilter, action, extra = {}) => { { method: "GET", action }, input({ type: "hidden", name: "filter", value: mode }), ...hiddenInputs(extra), - button({ type: "submit", class: cur === mode ? "filter-btn active" : "filter-btn" }, String(i18n[mode + "Button"] || mode).toUpperCase()) + button({ type: "submit", class: cur === mode ? "filter-btn active" : "filter-btn" }, i18n[mode + "Button"] || mode) ) ); }; @@ -80,11 +79,65 @@ const renderCardField = (labelText, value) => ); const renderFeedCommentsSection = (feedKey, comments = []) => { - return renderSharedCommentsSection({ - action: `/feed/${encodeURIComponent(feedKey)}/comments`, - comments: comments, - returnTo: null - }); + const list = Array.isArray(comments) ? comments : []; + const commentsCount = list.length; + + return div( + { class: "vote-comments-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), + span({ class: "card-value" }, String(commentsCount)) + ), + div( + { class: "comment-form-wrapper" }, + h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel || i18n.feedPostComment || "Post a comment"), + form( + { method: "POST", action: `/feed/${encodeURIComponent(feedKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, + textarea({ + id: "comment-text", + name: "text", + rows: 4, + class: "comment-textarea", + placeholder: i18n.voteNewCommentPlaceholder || "" + }), + div({ class: "comment-file-upload" }, label(i18n.uploadMedia || "Upload media"), input({ type: "file", name: "blob" })), + br(), + button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton || i18n.feedPostComment || "Send") + ) + ), + list.length + ? div( + { class: "comments-list" }, + list.map((c) => { + const author = c?.value?.author || ""; + const ts = c?.value?.timestamp || c?.timestamp; + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; + const relDate = ts ? moment(ts).fromNow() : ""; + + const content = c?.value?.content || {}; + const text = content.text || c?.value?.text || ""; + const threadRoot = content.fork || content.root || null; + + return div( + { class: "votations-comment-card" }, + span( + { class: "created-at" }, + span(i18n.createdBy), + author ? userLink(author) : span("(unknown)"), + absDate ? span(" | ") : "", + absDate ? span({ class: "votations-comment-date" }, absDate) : "", + relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", + relDate && threadRoot + ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) + : "" + ), + p({ class: "votations-comment-text" }, ...renderUrl(text)) + ); + }) + ) + : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet || i18n.noComments || "") + ); }; const renderFeedCard = (feed) => { @@ -110,7 +163,7 @@ const renderFeedCard = (feed) => { div( { class: "card-header activity-card-header" }, span(), - renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`, { author: authorId, reportTitle: safeText }) + renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`) ), div( { class: "card-section feed-card-body" }, @@ -136,8 +189,8 @@ const renderFeedCard = (feed) => { const maxVal = Math.max(...entries.map(([, v]) => Number(v))); const dominant = entries.filter(([, v]) => Number(v) === maxVal).map(([k]) => i18n['vote' + k.charAt(0).toUpperCase() + k.slice(1)] || k); return [ - span({ class: 'feed-vote-sep' }, '|'), - span({ class: 'feed-vote-dominant' }, `${i18n.moreVoted || 'More Voted'}: ${dominant.join(' + ')}`) + span({ style: 'margin:0 8px;opacity:0.5;' }, '|'), + span({ style: 'font-weight:700;' }, `${i18n.moreVoted || 'More Voted'}: ${dominant.join(' + ')}`) ]; })() ), @@ -149,9 +202,25 @@ const renderFeedCard = (feed) => { ) ) ), - renderEngagement(feed.key, - renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants), - renderCommentsLink({ href: `/feed/${encodeURIComponent(feed.key)}`, count: commentCount }) + div( + { class: "card-comments-summary" }, + span({ class: "card-label" }, `${i18n.voteCommentsLabel || "Comments"}:`), + span({ class: "card-value" }, String(commentCount)), + br(), + br(), + form( + { method: "GET", action: `/feed/${encodeURIComponent(feed.key)}` }, + button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton || i18n.feedOpenDiscussion || "Open Discussion") + ) + ), + div( + { class: "card-comments-summary feed-opinions-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), + span({ class: "card-value" }, String(totalCount)) + ), + renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants) ) ) ); @@ -195,13 +264,11 @@ exports.feedView = (feeds, opts = "ALL") => { div( { class: "feed-tools-row" }, form( - { method: "GET", action: "/feed", class: "filter-box" }, + { method: "GET", action: "/feed", class: "feed-search-form" }, input({ type: "hidden", name: "filter", value: filter }), tag ? input({ type: "hidden", name: "tag", value: tag }) : null, - input({ type: "text", name: "q", value: q, placeholder: i18n.searchPlaceholder || "Search", class: "filter-box__input" }), - div({ class: "filter-box__controls" }, - button({ type: "submit", class: "filter-box__button" }, i18n.searchButton || "Search") - ) + input({ type: "text", name: "q", value: q, placeholder: i18n.searchPlaceholder || "Search", class: "feed-search-input" }), + button({ type: "submit", class: "filter-btn feed-search-btn" }, i18n.searchButton || "Search") ) ), section( @@ -254,7 +321,7 @@ exports.feedCreateView = (opts = {}) => { ); }; -exports.singleFeedView = (feed, comments = [], params = {}) => { +exports.singleFeedView = (feed, comments = []) => { const content = feed.value?.content || {}; const rawText = typeof content.text === "string" ? content.text : ""; const safeText = rawText.trim(); @@ -268,7 +335,6 @@ exports.singleFeedView = (feed, comments = [], params = {}) => { return template( i18n.feedDetailTitle || "Feed", - section(div({ class: "tags-header" }, h2(i18n.feedTitle), p(i18n.FeedshareYourOpinions))), section( div( { class: "filters" }, @@ -283,10 +349,6 @@ exports.singleFeedView = (feed, comments = [], params = {}) => { ), div( { class: "bookmark-item card feed-detail-card" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(feed.key, null, { spread: params.spreads || null, author: authorId, reportTitle: safeText }) - ), br, div( { class: "feed-row" }, @@ -318,9 +380,15 @@ exports.singleFeedView = (feed, comments = [], params = {}) => { ) ) ), - renderEngagement(feed.key, - renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants), - renderFeedCommentsSection(feed.key, comments) + renderFeedCommentsSection(feed.key, comments), + div( + { class: "card-comments-summary feed-opinions-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), + span({ class: "card-value" }, String(Object.values(content.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) + ), + renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants) ) ) ); diff --git a/src/views/forum_view.js b/src/views/forum_view.js index a452797..4b201a1 100644 --- a/src/views/forum_view.js +++ b/src/views/forum_view.js @@ -3,7 +3,7 @@ const { input, label, br, select, option, h2, textarea } = require("../server/node_modules/hyperaxe"); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip, renderContentActions } = require('./main_views'); +const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip } = require('./main_views'); const { renderEncryptedChip: renderForumEncryptedChip } = require('./clearnet_view'); const { config } = require('../server/SSB_server.js'); const { renderUrl } = require('../backend/renderUrl'); @@ -23,8 +23,8 @@ exports.renderForumInvitePage = (code) => { }; const BASE_FILTERS = ['hot','all','mine','recent','top']; -const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.']; -const CAT_BLOCK2 = ['POLITICS','TECH','SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS']; +const CAT_BLOCK1 = ['GENERAL','OASIS','L.A.R.P.','POLITICS','TECH']; +const CAT_BLOCK2 = ['SCIENCE','MUSIC','ART','GAMING','BOOKS','FILMS']; const CAT_BLOCK3 = ['PHILOSOPHY','SOCIETY','PRIVACY','CYBERWARFARE','SURVIVALISM']; const ALL_CATS = [...CAT_BLOCK1, ...CAT_BLOCK2, ...CAT_BLOCK3]; @@ -99,7 +99,7 @@ const renderForumForm = () => label(i18n.forumTitleLabel), br(), input({ type: 'text', - name: 'title', maxlength: '100', + name: 'title', required: true, placeholder: i18n.forumTitlePlaceholder }), br(), br(), @@ -138,7 +138,7 @@ const renderThread = (nodes, level = 0, forumId) => { userLink(m.author), div({ class: 'comment-votes' }, span({ class: 'votes-count' }, `▲: ${m.positiveVotes || 0}`), - span({ class: 'votes-count votes-count-negative' }, + span({ class: 'votes-count', style: 'margin-left:12px;' }, `▼: ${m.negativeVotes || 0}`) ) ), @@ -190,9 +190,6 @@ const renderForumList = (forums, currentFilter, spreadMap = new Map()) => { renderVotes(f.key, f.score, f.key) ), div({ class: 'forum-main-col' }, - div({ class: 'card-header activity-card-header' }, - renderContentActions(f.key, `/forum/${encodeURIComponent(f.key)}`, { spread: spreadMap.get(f.key) || null, author: f.author, favKind: 'forum', isFavorite: f.isFavorite, reportTitle: f.title }) - ), div({ class: 'forum-header-row' }, a({ class: 'forum-category', @@ -213,13 +210,20 @@ const renderForumList = (forums, currentFilter, spreadMap = new Map()) => { div({ class: 'forum-meta' }, span({ class: 'forum-positive-votes' }, `▲: ${f.positiveVotes || 0}`), - span({ class: 'forum-negative-votes' }, + span({ class: 'forum-negative-votes', style: 'margin-left:12px;' }, `▼: ${f.negativeVotes || 0}`), span({ class: 'forum-participants' }, `${i18n.forumParticipants.toUpperCase()}: ${f.participants?.length || 1}`), span({ class: 'forum-messages' }, - `${i18n.forumMessages.toUpperCase()}: ${(f.messagesCount || 1) - 1}`) + `${i18n.forumMessages.toUpperCase()}: ${(f.messagesCount || 1) - 1}`), + form({ method: 'GET', action: `/forum/${encodeURIComponent(f.key)}`, class: 'visit-forum-form' }, + button({ type: 'submit', class: 'filter-btn' }, i18n.forumVisitButton) + ) ), + (() => { + const btn = renderSpreadButton(f.key, spreadMap.get(f.key)); + return btn ? div({ class: 'card-spread-left' }, btn) : null; + })(), div({ class: 'forum-footer' }, span({ class: 'date-link' }, `${moment(f.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), @@ -249,7 +253,9 @@ exports.forumView = async (forums, currentFilter, params = {}) => { return template(i18n.forumTitle, section( div({ class: 'tags-header' }, - h2(i18n.forumTitle), + h2(currentFilter === 'create' + ? i18n.forumCreateSectionTitle + : i18n.forumTitle), p(i18n.forumDescription) ), div({ class: 'mode-buttons-cols' }, @@ -265,17 +271,6 @@ exports.forumView = async (forums, currentFilter, params = {}) => { generateFilterButtons(CAT_BLOCK3, currentFilter, '/forum', CAT_I18N_MAP_UP), renderCreateForumButton() ), - currentFilter === 'create' - ? null - : div({ class: 'filters' }, - form({ method: 'GET', action: '/forum', class: 'filter-box' }, - input({ type: 'hidden', name: 'filter', value: currentFilter || 'all' }), - input({ type: 'text', name: 'q', value: params.q || '', placeholder: i18n.forumSearchPlaceholder, class: 'filter-box__input' }), - div({ class: 'filter-box__controls' }, - button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) - ) - ) - ), currentFilter === 'create' ? renderForumForm() : renderForumList( @@ -310,22 +305,22 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => { ) ), div({ class: 'forum-thread-container' }, - div({ class: 'forum-card forum-thread-header' }, - div({ class: 'root-vote-col' }, renderVotes( + div({ + class: 'forum-card forum-thread-header', + style: 'display:flex;align-items:flex-start;' + }, + div({ + class: 'root-vote-col', + style: 'width:60px;text-align:center;' + }, renderVotes( forum.key, messagesData.totalScore, forum.key )), - div({ class: 'forum-main-col' }, - div({ class: 'card-header activity-card-header' }, - renderContentActions(forum.key, null, { - author: forum.author, - favKind: 'forum', - isFavorite: forum.isFavorite, - spread: null, - reportTitle: forum.title - }) - ), + div({ + class: 'forum-main-col', + style: 'flex:1;padding-left:10px;' + }, div({ class: 'forum-header-row' }, a({ class: 'forum-category', @@ -368,15 +363,21 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => { innerHTML: sanitizeHtml(renderTextWithStyles(forum.text || '')) }), div({ class: 'forum-meta' }, - span({ class: 'forum-positive-votes' }, + span({ class: 'votes-count' }, `▲: ${messagesData.positiveVotes}`), - span({ class: 'forum-negative-votes' }, - `▼: ${messagesData.negativeVotes}`), + span({ + class: 'votes-count', + style: 'margin-left:12px;' + }, `▼: ${messagesData.negativeVotes}`), span({ class: 'forum-participants' }, `${i18n.forumParticipants.toUpperCase()}: ${forum.participants?.length || 1}`), span({ class: 'forum-messages' }, `${i18n.forumMessages.toUpperCase()}: ${messagesData.total}`) ), + (() => { + const btn = renderSpreadButton(forum.key); + return btn ? div({ class: 'card-spread-left' }, btn) : null; + })(), div({ class: 'forum-footer' }, span({ class: 'date-link' }, `${moment(forum.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed}`), @@ -384,7 +385,10 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => { ) ) ), - div({ class: 'new-message-wrapper' }, + div({ + class: 'new-message-wrapper', + style: 'margin-top:12px;' + }, form({ method: 'POST', action: `/forum/${encodeURIComponent(forum.key)}/message`, @@ -395,9 +399,13 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => { rows: 4, required: true, placeholder: i18n.forumMessagePlaceholder, - class: 'new-message-textarea' + style: 'width:100%;' }), br(), - button({ type: 'submit', class: 'forum-send-btn' }, i18n.forumSendButton) + button({ + type: 'submit', + class: 'forum-send-btn', + style: 'margin-top:4px;' + }, i18n.forumSendButton) ) ), ...renderThread(messagesData.messages, 0, forum.key) diff --git a/src/views/gallery_view.js b/src/views/gallery_view.js deleted file mode 100644 index 3f9663e..0000000 --- a/src/views/gallery_view.js +++ /dev/null @@ -1,118 +0,0 @@ -const { div, a, span, img, video, input, button, label, br } = require("../server/node_modules/hyperaxe") -const { i18n } = require("./main_views") -const { MAX_IMAGES } = require("../models/media_gallery") - -const safeArr = (v) => (Array.isArray(v) ? v : []) -const safeText = (v) => String(v == null ? "" : v).trim() - -const blobIdOf = (entry) => { - const value = safeText(entry) - const m = value.match(/\(([^)\s]+)\)/) - return m ? m[1] : value -} - -const blobUrl = (id, size) => size - ? `/image/${size}/${encodeURIComponent(id)}` - : `/blob/${encodeURIComponent(id)}` - -const isVideoEntry = (entry) => /\[video:[^\]]*\]\(/.test(String(entry || "")) - -const imagesOf = (item) => { - const list = safeArr(item && item.images).filter(Boolean) - if (list.length) return list - return item && item.image ? [item.image] : [] -} - -const videoOf = (item) => safeText(item && item.video) - -const renderMediaThumb = (entry, alt = "") => isVideoEntry(entry) - ? video({ controls: true, class: "gallery-image", src: blobUrl(blobIdOf(entry)) }) - : img({ src: blobUrl(blobIdOf(entry), 256), class: "gallery-image", alt }) - -const lightboxId = (scope, itemId, index) => `${scope}-photo-${encodeURIComponent(itemId)}-${index}` - -let zoomSeq = 0 -const renderZoomableImage = (src, { alt = "", imgClass = "" } = {}) => { - if (!src) return null - zoomSeq = (zoomSeq + 1) % 1000000 - const id = `zoom-${zoomSeq}-${String(src).replace(/[^a-zA-Z0-9]/g, "").slice(-10)}` - return [ - a({ href: `#${id}`, class: "zoom-link" }, img({ src, class: imgClass, alt })), - div({ id, class: "lightbox" }, - a({ href: "#", class: "lightbox-close" }, "×"), - img({ src, class: "lightbox-image", alt }) - ) - ] -} - -const renderPhotoGallery = (item, scope = "media") => { - const list = imagesOf(item) - const clip = videoOf(item) - if (!list.length && !clip) return null - const id = (item && (item.id || item.key || item.rootId)) || "" - return div({ class: "media-gallery" }, - list.length - ? div({ class: "gallery" }, - list.map((entry, i) => - isVideoEntry(entry) - ? span({ class: "gallery-item" }, renderMediaThumb(entry)) - : a({ href: `#${lightboxId(scope, id, i)}`, class: "gallery-item" }, renderMediaThumb(entry)) - ) - ) - : null, - list.filter(entry => !isVideoEntry(entry)).map((entry) => - div({ id: lightboxId(scope, id, list.indexOf(entry)), class: "lightbox" }, - a({ href: "#", class: "lightbox-close" }, "×"), - img({ src: blobUrl(blobIdOf(entry)), class: "lightbox-image", alt: "" }) - ) - ), - clip - ? div({ class: "media-video media-video-centered" }, - video({ controls: true, class: "media-video-player", src: blobUrl(blobIdOf(clip)) }) - ) - : null - ) -} - -const renderGalleryFields = (item = {}, isEdit = false, maxImages = MAX_IMAGES) => { - const list = imagesOf(item) - return [ - label(i18n.galleryImages), - br(), - ...list.map(entry => input({ type: "hidden", name: "keepImages", value: entry })), - list.length < maxImages - ? div({ class: "media-file-row" }, - input({ type: "file", name: "images", accept: "image/*", multiple: true }), - button({ type: "submit", name: "action", value: "addPhoto", attrs: { formnovalidate: "formnovalidate" }, class: "filter-btn media-add-btn" }, i18n.galleryAddPhoto) - ) - : null, - br(), - list.length - ? [ - div({ class: "gallery media-form-gallery" }, - list.map((entry, i) => - span({ class: "gallery-item" }, - renderMediaThumb(entry), - button({ type: "submit", name: "removePhoto", value: String(i), attrs: { formnovalidate: "formnovalidate" }, class: "media-remove-btn" }, i18n.galleryRemovePhoto) - ) - ) - ), - br() - ] - : null, - br() - ] -} - -module.exports = { - MAX_IMAGES, - blobIdOf, - blobUrl, - isVideoEntry, - imagesOf, - videoOf, - renderMediaThumb, - renderPhotoGallery, - renderGalleryFields, - renderZoomableImage -} diff --git a/src/views/games_view.js b/src/views/games_view.js index e4c6135..5619f82 100644 --- a/src/views/games_view.js +++ b/src/views/games_view.js @@ -99,13 +99,13 @@ exports.gamesView = (filter = 'all', hall = null) => { form({ method: 'GET', action: '/games' }, input({ type: 'hidden', name: 'filter', value: 'all' }), button({ type: 'submit', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, - String(i18n.gamesFilterAll).toUpperCase() + i18n.gamesFilterAll ) ), form({ method: 'GET', action: '/games' }, input({ type: 'hidden', name: 'filter', value: 'scoring' }), button({ type: 'submit', class: filter === 'scoring' ? 'filter-btn active' : 'filter-btn' }, - String(i18n.gamesFilterScoring).toUpperCase() + i18n.gamesFilterScoring ) ) ); diff --git a/src/views/graphos_view.js b/src/views/graphos_view.js index 0a3ba2b..ea2332e 100644 --- a/src/views/graphos_view.js +++ b/src/views/graphos_view.js @@ -88,9 +88,9 @@ const buildGraphSvg = (me, peers) => { + ``; }; -const kpi = (label, value) => div({ class: 'stats-kpi stats-kpi-inline' }, - span({ class: 'stats-kpi-label' }, `${String(label).toUpperCase()}: `), - span({ class: 'stats-kpi-value' }, String(value)) +const kpi = (label, value) => div({ class: 'stats-kpi' }, + div({ class: 'stats-kpi-label' }, label), + div({ class: 'stats-kpi-value' }, String(value)) ); const legendItem = (kind, label) => @@ -126,27 +126,27 @@ exports.graphosView = ({ filter, me, peers, kpis, focus = null, focusId = null, modes.map(m => form({ method: 'GET', action: '/graphos' }, input({ type: 'hidden', name: 'filter', value: m }), - button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, String(i18n[m + 'Button']).toUpperCase()) + button({ type: 'submit', class: filter === m ? 'filter-btn active' : 'filter-btn' }, i18n[m + 'Button']) ) ) ), capped ? p({ class: 'graphos-cap-note' }, `${i18n.graphosShowing || 'Showing'} ${shown} / ${total}`) : null, - div({ class: 'stats-block graphos-stats' }, + div({ class: 'graphos-legend' }, + legendItem('me', i18n.graphosYou || 'You'), + legendItem('online', i18n.online || 'Online'), + filter !== 'MINE' ? legendItem('discovered', i18n.discovered || 'Discovered') : null, + filter !== 'MINE' ? legendItem('unknown', i18n.unknown || 'Unknown') : null + ), + div({ class: 'graphos-canvas', innerHTML: buildGraphSvg(me, peers) }), + div({ class: 'stats-block' }, div({ class: 'stats-grid' }, kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total), kpi(i18n.online || 'Online', kpis.online), filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null, filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null ) - ), - div({ class: 'graphos-canvas', innerHTML: buildGraphSvg(me, peers) }), - div({ class: 'graphos-legend' }, - legendItem('me', i18n.graphosYou || 'You'), - legendItem('online', i18n.online || 'Online'), - filter !== 'MINE' ? legendItem('discovered', i18n.discovered || 'Discovered') : null, - filter !== 'MINE' ? legendItem('unknown', i18n.unknown || 'Unknown') : null ) ) ); diff --git a/src/views/hive_views.js b/src/views/hive_views.js deleted file mode 100644 index ea30059..0000000 --- a/src/views/hive_views.js +++ /dev/null @@ -1,244 +0,0 @@ -"use strict"; -// Navegacion propia de la rama experimental para movil: topbar, barra inferior -// personalizable y rejilla de hexagonos. -// -// Vive fuera de main_views.js a proposito: upstream reescribe ese fichero en cada -// release (de 0.9.1 a 0.9.5 movio +256/-502 lineas) y esta interfaz se perdia en -// cada integracion. Aqui solo depende de i18n, que se inyecta porque main_views -// lo muta en cada peticion sin reasignarlo. - -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 sharedState = require("../configs/shared-state"); - -module.exports = ({ i18n }) => { - // === Oasis UX (portado de 0.8.7_UX): topbar + bottombar + hive === - // topbar fija arriba: [logo] [Personal] [Community] [avatar] en una línea (sin título OASIS) - const renderMobileTopbar = () => { - // solo en movil: en escritorio manda la cabecera de upstream - if (process.env.OASIS_MOBILE !== '1') return ""; - // FORK_IA_UX v3: Personal/Community VUELVEN al topbar (ya funcionan: la hoja cerrada es - // visibility:hidden y no bloquea los taps). Llena el top y da el filtro a un toque. - const hiveFilter = (typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || ""; - return div( - { class: "oasis-mobile-topbar" }, - a({ class: "omt-logo", href: "/" }, img({ src: "/assets/images/snh-oasis.jpg", alt: "Oasis" })), - div({ class: "omt-menus" }, - a({ class: hiveFilter === "personal" ? "oasis-menu-btn active" : "oasis-menu-btn", href: hiveFilter === "personal" ? "/activity" : "/activity?hive=personal" }, span(i18n.menuPersonal || "Personal")), - a({ class: hiveFilter === "community" ? "oasis-menu-btn active" : "oasis-menu-btn", href: hiveFilter === "community" ? "/activity" : "/activity?hive=community" }, span(i18n.menuSocial || "Social")) - ), - a({ class: "omt-avatar", href: "/profile" }, img({ src: (typeof global !== "undefined" && global.__OASIS_SELF_AVATAR__) || "/assets/images/default-avatar.png", alt: i18n.profile || "Profile" })) - ); - }; - - const PINNABLE_MODULES = [ - { key: 'search', href: '/search', glyph: 'ꔅ', labelKey: 'searchTitle', mod: null }, - { key: 'inbox', href: '/inbox', glyph: '☂', labelKey: 'inbox', mod: null }, - { key: 'publish', href: '/publish', glyph: '❂', labelKey: 'publish', mod: null }, - { key: 'activity', href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, - { key: 'tribes', href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, - { key: 'chats', href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' }, - { key: 'karvan', href: '/karvan', glyph: '⧗', labelKey: 'karvanTitle', mod: 'karvanMod' }, - { key: 'polls', href: '/polls', glyph: 'ꖙ', labelKey: 'pollsTitle', mod: 'pollsMod' }, - { key: 'blogs', href: '/blogs', glyph: 'ꔫ', labelKey: 'blogTitle', mod: 'blogsMod' }, - { key: 'data', href: '/data', glyph: 'ꘑ', labelKey: 'dataTitle', mod: 'dataMod' }, - { key: 'market', href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, - { key: 'events', href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, - { key: 'calendars', href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, - { key: 'banking', href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, - { key: 'forum', href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, - { key: 'videos', href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' }, - { key: 'images', href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' }, - { key: 'audios', href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, - { key: 'documents', href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, - { key: 'bookmarks', href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' }, - { key: 'maps', href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, - { key: 'trending', href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, - { key: 'jobs', href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' }, - { key: 'agenda', href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, - { key: 'wallet', href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' }, - { key: 'tasks', href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' }, - { key: 'torrents', href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' }, - { key: 'tags', href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' }, - { key: 'pads', href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, - { key: 'projects', href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, - { key: 'shops', href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, - { key: 'transfers', href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' }, - { key: 'feed', href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, - { key: 'pixelia', href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, - { key: 'melody', href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' }, - { key: 'games', href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' }, - { key: 'votes', href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, - { key: 'reports', href: '/reports', glyph: 'ꕥ', labelKey: 'reportsTitle', mod: 'reportsMod' }, - { key: 'opinions', href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' }, - { key: 'parliament',href: '/parliament',glyph: 'ꗞ', labelKey: 'parliamentTitle',mod: 'parliamentMod' }, - { key: 'courts', href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' }, - { key: 'larp', href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, - { key: 'ai', href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, - { key: 'cipher', href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, - { key: 'fediverse', href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' }, - { key: 'inhabitants', href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null }, - { key: 'cv', href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null }, - { key: 'stats', href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null }, - { key: 'blockexplorer', href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null }, - ]; - - const renderBottomBar = () => { - if (process.env.OASIS_MOBILE !== '1') return ""; - const inboxN = sharedState.getInboxCount(); - const cfg = getConfig(); - const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; - const bbLink = (href, ico, label, badge) => a( - { class: "bb-item", href }, - span({ class: "bb-ico" }, ico), - (badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "", - span({ class: "bb-lbl" }, label) - ); - // FORK_IA_UX v4: barra PERSONALIZABLE (pins) + lápiz ✎ para editar (/settings/bottombar) - // + par FIJO Peers/Invites a la derecha. Sin FAB "+". (restaurada de la versión que gustaba) - const MAX = 4; - const pins = (cfg.bottomBarPins || []) - .map(key => PINNABLE_MODULES.find(m => m.key === key)) - .filter(m => m && modOn(m.mod)) - .slice(0, MAX); - const slots = pins.map(m => bbLink( - m.href, m.glyph, i18n[m.labelKey] || m.key, m.key === 'inbox' ? inboxN : 0 - )); - const editSlot = a( - { class: "bb-item bb-add", href: "/settings/bottombar" }, - span({ class: "bb-ico" }, "✎"), - span({ class: "bb-lbl" }, i18n.bbManage || "Edit") - ); - const pair = [ bbLink("/peers", "⧖", i18n.peers || "Peers") ]; - if (modOn('invitesMod')) pair.push(bbLink("/invites", "ꔮ", i18n.invites || "Invites")); - return nav( - { class: "oasis-bottombar", attrs: { "aria-label": i18n.bbQuickActions || "Quick actions" } }, - ...slots, - editSlot, - div({ class: "bb-pair" }, ...pair) - ); - }; - - const renderHiveNav = () => { - if (process.env.OASIS_MOBILE !== '1') return ""; - const cfg = getConfig(); - const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; - const CATS = [ - { id: 'personal', emoji: '⚉', title: i18n.menuPersonal, mods: [ - { href: '/profile', glyph: '⚉', labelKey: 'profile', mod: null }, { href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null }, - { href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, { href: '/favorites', glyph: 'ꘝ', labelKey: 'favoritesTitle', mod: 'favoritesMod' }, - { href: '/logs', glyph: 'ꗯ', labelKey: 'logsTitle', mod: 'logsMod' }, { href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' }, - { href: '/modules', glyph: 'ꗣ', labelKey: 'modules', mod: null }, { href: '/settings', glyph: '⚙', labelKey: 'settings', mod: null } ] }, - { id: 'governance', emoji: '⚖', title: i18n.menuGovernance, mods: [ - { href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null }, { href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, - { href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, { href: '/parliament', glyph: 'ꗞ', labelKey: 'parliamentTitle', mod: 'parliamentMod' }, - { href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' } ] }, - { id: 'office', emoji: '⌂', title: i18n.menuOffice, mods: [ - { href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, { href: '/polls', glyph: 'ꖙ', labelKey: 'pollsTitle', mod: 'pollsMod' }, { href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, - { href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, { href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' }, - { href: '/reports', glyph: 'ꕥ', labelKey: 'reportsTitle', mod: 'reportsMod' } ] }, - { id: 'economy', emoji: '¤', title: i18n.menuEconomy, mods: [ - { href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, { href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, - { href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, { href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' }, - { href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, { href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' } ] }, - { id: 'tools', emoji: '⚒', title: i18n.menuTools, mods: [ - { href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, { href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null }, - { href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, { href: '/invites', glyph: 'ꔹ', labelKey: 'invites', mod: 'invitesMod' }, - { href: '/legacy', glyph: 'ꖤ', labelKey: 'legacy', mod: 'legacyMod' }, { href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null }, - { href: '/data', glyph: 'ꘑ', labelKey: 'dataTitle', mod: 'dataMod' } ] }, - { id: 'network', emoji: '☍', title: i18n.menuNetwork, mods: [ - { href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, { href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' }, - { href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, { href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' }, - { href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, { href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, - { href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, { href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' }, - { href: '/karvan', glyph: '⧗', labelKey: 'karvanTitle', mod: 'karvanMod' } ] }, - { id: 'blogs', emoji: '✦', title: i18n.menuBlogs, mods: [ - { href: '/blogs', glyph: 'ꔫ', labelKey: 'blogTitle', mod: 'blogsMod' }, { href: '/mentions', glyph: '✺', labelKey: 'mentions', mod: null }, { href: '/public/latest', glyph: '☄', labelKey: 'latest', mod: 'latestMod' }, - { href: '/public/latest/threads', glyph: '♺', labelKey: 'threads', mod: 'threadsMod' }, { href: '/public/latest/topics', glyph: 'ϟ', labelKey: 'topics', mod: 'topicsMod' }, - { href: '/public/latest/summaries', glyph: '※', labelKey: 'summaries', mod: 'summariesMod' }, { href: '/public/popular/day', glyph: '⌘', labelKey: 'popular', mod: 'popularMod' }, - { href: '/public/latest/extended', glyph: '∞', labelKey: 'multiverse', mod: 'multiverseMod' } ] }, - { id: 'creative', emoji: '✎', title: i18n.menuCreative, mods: [ - { href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, { href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' }, - { href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, { href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' } ] }, - { id: 'media', emoji: '▤', title: i18n.menuMedia, mods: [ - { href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, { href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' }, - { href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, { href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' }, - { href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' }, { href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' } ] }, - { id: 'fediverse', emoji: '⌬', title: i18n.fediverse, mods: [ - { href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' } ] }, - ]; - // filtro server-side (lo pone el middleware desde ?hive=): personal = CATS[0..4], community = CATS[5..9], vacío = las 10 - const hiveFilter = (typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || ""; - const shown = hiveFilter === "personal" ? CATS.slice(0, 5) : hiveFilter === "community" ? CATS.slice(5) : CATS; - const rowSizes = shown.length <= 5 ? [3, 2] : [3, 4, 3]; - const catHex = (c) => label({ for: `hive-${c.id}`, class: 'hex-cat' }, - span({ class: 'hx-ico' }, c.emoji), span({ class: 'hx-lbl' }, c.title)); - const modHex = (m) => a({ class: 'hex-mod', href: m.href }, - span({ class: 'hx-ico' }, m.glyph), span({ class: 'hx-lbl' }, i18n[m.labelKey] || '')); - const radios = shown.map(c => input({ type: 'radio', name: 'hive-cat', id: `hive-${c.id}`, class: 'hive-radio' })); - const catRows = []; - let idx = 0; - for (const size of rowSizes) { catRows.push(div({ class: 'hive-row' }, ...shown.slice(idx, idx + size).map(catHex))); idx += size; } - const grid = div({ class: 'hive-grid hive-cats' }, ...catRows); - const panels = shown.map(c => div({ class: 'hive-panel', id: `hive-panel-${c.id}` }, - div({ class: 'hive-mods' }, ...c.mods.filter(m => modOn(m.mod)).map(modHex)) - )); - return div({ class: 'hive-nav' }, ...radios, grid, ...panels); - }; - - - // pantalla para personalizar la barra inferior (/settings/bottombar) - const bottomBarPickerView = (template) => { - const cfg = getConfig(); - const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; - const MAX = 4; - const pinnedKeys = cfg.bottomBarPins || []; - const pinned = pinnedKeys - .map(key => PINNABLE_MODULES.find(m => m.key === key)) - .filter(Boolean); - const full = pinned.length >= MAX; - const actionForm = (action, key, cls, node) => form({ method: 'POST', action: '/settings/bottombar', class: cls }, - input({ type: 'hidden', name: 'action', value: action }), - input({ type: 'hidden', name: 'key', value: key }), - node - ); - const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form', - button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' }, - span({ class: 'bb-manage-ico' }, m.glyph), - span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key), - span({ class: 'bb-manage-x' }, '✕') - ) - ); - const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form', - button({ type: 'submit', class: 'bb-pick-btn' }, - span({ class: 'bb-pick-ico' }, m.glyph), - span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key) - ) - ); - const addable = PINNABLE_MODULES.filter(m => modOn(m.mod) && !pinnedKeys.includes(m.key)); - return template( - i18n.bbManage || 'Edit bar', - section( - div({ class: 'tags-header' }, - h2(i18n.bbPickTitle || 'Customize the bar'), - p(i18n.bbPickDesc || 'Choose up to 4 shortcuts for the bottom bar.') - ), - div({ class: 'bb-manage' }, - h3(`${i18n.bbYourBar || 'Your bar'} (${pinned.length}/${MAX})`), - pinned.length - ? div({ class: 'bb-manage-list' }, ...pinned.map(pinnedRow)) - : p({ class: 'bb-manage-empty' }, i18n.bbPickNone || 'None') - ), - div({ class: 'bb-manage-add' }, - h3(i18n.bbAdd || 'Add'), - full - ? p({ class: 'bb-manage-full' }, i18n.bbFull || 'Maximum reached') - : div({ class: 'bb-pick-grid' }, ...addable.map(addBtn)) - ), - a({ class: 'filter-btn bb-manage-done', href: '/activity' }, i18n.bbDone || 'Done') - ) - ); - }; - - return { renderMobileTopbar, renderBottomBar, renderHiveNav, PINNABLE_MODULES, bottomBarPickerView }; -}; diff --git a/src/views/housing_view.js b/src/views/housing_view.js deleted file mode 100644 index 92c5ab3..0000000 --- a/src/views/housing_view.js +++ /dev/null @@ -1,496 +0,0 @@ -const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, video, table, tr, td } = require("../server/node_modules/hyperaxe") -const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { template, i18n, userLink, renderOpenClosedChip, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderOpinionsVoting, renderEngagement, renderSpreadEditWarning } = require("./main_views") -const { blobUrl, blobIdOf, isVideoEntry, imagesOf, renderMediaThumb, renderPhotoGallery, renderGalleryFields } = require("./gallery_view") -const moment = require("../server/node_modules/moment") -const { config } = require("../server/SSB_server.js") -const { renderUrl } = require("../backend/renderUrl") -const opinionCategories = require("../backend/opinion_categories") -const { renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") - -const userId = config.keys.id - -const FILTERS = [ - { key: "ALL", i18n: "housingFilterAll" }, - { key: "RECENT", i18n: "housingFilterRecent" }, - { key: "MINE", i18n: "housingFilterMine" }, - { key: "TOP", i18n: "housingFilterTop" }, - { key: "REQUESTED", i18n: "housingFilterRequested" }, - { key: "SALE", i18n: "housingFilterSale" }, - { key: "RENT", i18n: "housingFilterRent" }, - { key: "COUCHSURFING", i18n: "housingFilterCouchsurfing" }, - { key: "OPEN", i18n: "housingFilterOpen" }, - { key: "CLOSED", i18n: "housingFilterClosed" } -] - -const MAX_IMAGES = 8 - -const safeArr = (v) => (Array.isArray(v) ? v : []) -const safeText = (v) => String(v || "").trim() - -const parseNum = (v) => { - const n = parseFloat(String(v ?? "").replace(",", ".")) - return Number.isFinite(n) ? n : NaN -} - -const fmtPrice = (v) => { - const n = parseNum(v) - return Number.isFinite(n) ? n.toFixed(2) : String(v ?? "") -} - - - -const priceLabel = (item) => { - if (item.housing_type === "couchsurfing") return i18n.housingFree || "FREE" - const suffix = item.housing_type === "rent" ? ` ${i18n.housingPerMonth || "/month"}` : "" - return `${fmtPrice(item.price)} ECO${suffix}` -} - -const sumCats = (opinions = {}, cats = []) => (cats || []).reduce((s, c) => s + (Number((opinions || {})[c]) || 0), 0) - -const renderStarRating = (opinions, voterCount) => { - const pos = sumCats(opinions, opinionCategories.positive) - const neg = sumCats(opinions, opinionCategories.constructive) + sumCats(opinions, opinionCategories.moderation) - const total = pos + neg - const full = total > 0 ? Math.round((pos / total) * 5) : 0 - const stars = "★".repeat(full) + "☆".repeat(5 - full) - return span({ class: "housing-stars" }, `${stars} (${voterCount})`) -} - -const buildReturnTo = (filter, params = {}) => { - const parts = [`filter=${encodeURIComponent(safeText(filter || "ALL"))}`] - const q = safeText(params.search || "") - if (q) parts.push(`search=${encodeURIComponent(q)}`) - if (String(params.minPrice ?? "") !== "") parts.push(`minPrice=${encodeURIComponent(String(params.minPrice))}`) - if (String(params.maxPrice ?? "") !== "") parts.push(`maxPrice=${encodeURIComponent(String(params.maxPrice))}`) - if (safeText(params.place)) parts.push(`place=${encodeURIComponent(safeText(params.place))}`) - if (safeText(params.sort)) parts.push(`sort=${encodeURIComponent(safeText(params.sort))}`) - return `/housing?${parts.join("&")}` -} - -const renderTags = (tags = []) => { - const arr = safeArr(tags).map(t => String(t || "").trim()).filter(Boolean) - return arr.length - ? div({ class: "card-tags" }, arr.map(tag => a({ class: "tag-link", href: `/search?query=%23${encodeURIComponent(tag)}` }, `#${tag}`))) - : null -} - -const renderTypeChip = (item) => { - const t = String(item.housing_type || "").toUpperCase() - const emoji = item.housing_type === "sale" ? "🏷" : item.housing_type === "rent" ? "🔑" : "🛋" - return renderStateChip("whole", emoji, i18n["housingType" + t] || t) -} - -const renderStatusChip = (status) => { - const s = String(status || "").toUpperCase() - return renderOpenClosedChip(s, { - statusChipOPEN: i18n.housingStatusOPEN || "OPEN", - statusChipCLOSED: i18n.housingStatusCLOSED || "CLOSED" - }) -} - -const renderRequestedChip = () => renderStateChip("whole", "★", i18n.housingRequestedBadge || "REQUESTED") - -const renderInfoTable = (item) => { - const rows = [] - const pushRow = (labelText, valueNode, valueClass) => - rows.push(tr( - td({ class: "tribe-info-label" }, labelText), - td({ class: `tribe-info-value${valueClass ? " " + valueClass : ""}` }, valueNode) - )) - pushRow(i18n.housingProperty, i18n["housingProperty" + String(item.property_type || "").toUpperCase()] || "—") - pushRow(i18n.housingPrice, priceLabel(item), "card-salary") - if (item.rooms > 0) pushRow(i18n.housingRooms, String(item.rooms)) - if (item.size > 0) pushRow(i18n.housingSize, `${item.size} m²`) - if (item.capacity > 0) pushRow(i18n.housingCapacity, String(item.capacity)) - if (item.availableFrom) pushRow(i18n.housingAvailableFrom, moment(item.availableFrom).format("YYYY/MM/DD")) - if (item.availableTo) pushRow(i18n.housingAvailableTo, moment(item.availableTo).format("YYYY/MM/DD")) - if (safeText(item.place)) pushRow(i18n.housingPlace, safeText(item.place)) - if (item.mapUrl) { - const mapNode = renderMapLocationVisitLabel(item.mapUrl) - if (mapNode) pushRow(i18n.mapLocationTitle || "Map", mapNode) - } - return table({ class: "tribe-info-table housing-info-table" }, ...rows) -} - -const today = () => moment().format("YYYY-MM-DD") - -const renderStatusRow = (item, returnTo) => { - if (String(item.author) !== String(userId)) return null - const isOpen = item.status === "OPEN" - return div({ class: "tribe-side-actions housing-status-row" }, - span({ class: "card-label" }, `${i18n.housingStatus}: `), - renderStatusChip(item.status), - form({ method: "POST", action: `/housing/status/${encodeURIComponent(item.id)}`, class: "inline-form" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "tribe-action-btn", type: "submit", name: "status", value: isOpen ? "CLOSED" : "OPEN" }, - isOpen ? (i18n.housingSetClosed || "Close") : (i18n.housingSetOpen || "Reopen")) - ) - ) -} - -const renderOwnerActions = (item, returnTo) => { - if (String(item.author) !== String(userId)) return [] - return [ - form({ method: "GET", action: `/housing/edit/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.housingUpdateButton) - ), - form({ method: "POST", action: `/housing/delete/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "delete-btn", type: "submit" }, i18n.housingDeleteButton) - ) - ] -} - -const renderRequestToggle = (item, returnTo) => { - if (String(item.author) === String(userId)) return null - if (item.status !== "OPEN") return null - const requested = safeArr(item.requests).includes(userId) - return requested - ? form({ method: "POST", action: `/housing/cancel/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "filter-btn" }, i18n.housingCancelButton) - ) - : form({ method: "POST", action: `/housing/request/${encodeURIComponent(item.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "filter-btn" }, i18n.housingRequestButton) - ) -} - -const renderHousingList = (items, filter, params = {}) => { - const list = safeArr(items) - if (!list.length) return p(i18n.housingNoItems) - - return div({ class: "housing-grid" }, - list.map((item) => { - const clip = safeText(item.video) - const cover = imagesOf(item)[0] - const isOwn = item.author && String(item.author) === String(userId) - const requested = safeArr(item.requests).includes(userId) - const chips = [ - renderTypeChip(item), - renderStatusChip(item.status), - item.visibility === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, - requested ? renderRequestedChip() : null, - renderLifespanChip(item.lifetime, i18n) - ].filter(Boolean) - - return div({ class: "trending-card housing-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(item.id, `/housing/${encodeURIComponent(item.id)}`, { spread: params.spreadMap && params.spreadMap.get(item.id) || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title }) - ), - div({ class: "card-section housing-card-body" }, - clip || (cover && isVideoEntry(cover)) - ? div({ class: "tribe-card-image-wrapper housing-card-video" }, - video({ controls: true, class: "housing-card-hero-video", src: blobUrl(blobIdOf(clip || cover)) }) - ) - : cover - ? div({ class: "tribe-card-image-wrapper" }, - a({ href: `/housing/${encodeURIComponent(item.id)}` }, - img({ src: blobUrl(blobIdOf(cover), 256), class: "tribe-card-hero-image", alt: "" }) - ) - ) - : null, - div({ class: "tribe-card-body" }, - div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, - a({ href: `/housing/${encodeURIComponent(item.id)}` }, safeText(item.title) || i18n.housingTitle) - ) - ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - renderStarRating(item.opinions, safeArr(item.opinions_inhabitants).length), - div({ class: "card-date-highlight" }, priceLabel(item)), - safeText(item.place) - ? div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.housingPlace}: `), - span({ class: "card-value" }, safeText(item.place)) - ) - : null, - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${i18n.housingRequests}: ${item.requestCount || 0}`) - ), - imagesOf(item).length > 1 - ? div({ class: "card-field" }, - span({ class: "card-label" }, `${i18n.galleryPhotos}: `), - span({ class: "card-value" }, String(imagesOf(item).length)) - ) - : null - ) - ) - ) - }) - ) -} - -const renderHousingForm = (item = {}, mode = "create", maxImages = MAX_IMAGES, spreadWarning = null) => { - const isEdit = mode === "edit" - const type = String(item.housing_type || "").toLowerCase() - return div({ class: "div-center housing-form" }, - form({ - action: isEdit ? `/housing/update/${encodeURIComponent(item.id)}` : "/housing/create", - method: "POST", - enctype: "multipart/form-data" - }, - input({ type: "hidden", name: "returnTo", value: "/housing?filter=MINE" }), - isEdit ? spreadWarning : null, - label(i18n.housingType), - br(), - select({ name: "housing_type", required: true }, - option({ value: "sale", selected: type === "sale" ? "selected" : undefined }, i18n.housingTypeSALE), - option({ value: "rent", selected: type === "rent" ? "selected" : undefined }, i18n.housingTypeRENT), - option({ value: "couchsurfing", selected: type === "couchsurfing" ? "selected" : undefined }, i18n.housingTypeCOUCHSURFING) - ), - br(), - br(), - label(i18n.housingProperty), - br(), - select({ name: "property_type" }, - option({ value: "apartment", ...(item.property_type === "apartment" ? { selected: true } : {})}, i18n.housingPropertyAPARTMENT), - option({ value: "house", ...(item.property_type === "house" ? { selected: true } : {})}, i18n.housingPropertyHOUSE), - option({ value: "room", ...(item.property_type === "room" ? { selected: true } : {})}, i18n.housingPropertyROOM), - option({ value: "land", ...(item.property_type === "land" ? { selected: true } : {})}, i18n.housingPropertyLAND), - option({ value: "other", ...(item.property_type === "other" ? { selected: true } : {})}, i18n.housingPropertyOTHER) - ), - br(), - br(), - label(i18n.housingTitleLabel), - br(), - input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.housingTitlePlaceholder, value: item.title || "" }), - br(), - br(), - ...renderGalleryFields(item, isEdit, maxImages), - label(i18n.housingDescription), - br(), - textarea({ name: "description", rows: "6", required: true, placeholder: i18n.housingDescriptionPlaceholder }, item.description || ""), - br(), - br(), - label(i18n.housingRules), - br(), - textarea({ name: "rules", rows: "4", placeholder: i18n.housingRulesPlaceholder }, item.rules || ""), - br(), - br(), - label(i18n.housingPlace), - br(), - input({ type: "text", name: "place", placeholder: i18n.housingPlacePlaceholder, value: item.place || "" }), - br(), - br(), - label(i18n.mapLocationTitle || "Map Location"), - br(), - input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: item.mapUrl || "" }), - br(), - br(), - label(i18n.housingPriceLabel), - br(), - input({ type: "number", name: "price", step: "0.01", min: "0", value: item.price || "" }), - br(), - br(), - label(i18n.housingRooms), - br(), - input({ type: "number", name: "rooms", min: "0", value: item.rooms || "" }), - br(), - br(), - label(i18n.housingSizeLabel), - br(), - input({ type: "number", name: "size", step: "0.01", min: "0", value: item.size || "" }), - br(), - br(), - label(i18n.housingCapacity), - br(), - input({ type: "number", name: "capacity", min: "0", value: item.capacity || "" }), - br(), - br(), - label(i18n.housingAvailableFrom), - br(), - input({ type: "date", name: "availableFrom", required: true, min: isEdit ? undefined : today(), value: item.availableFrom ? String(item.availableFrom).slice(0, 10) : "" }), - br(), - br(), - label(i18n.housingAvailableTo), - br(), - input({ type: "date", name: "availableTo", min: item.availableFrom ? String(item.availableFrom).slice(0, 10) : today(), value: item.availableTo ? String(item.availableTo).slice(0, 10) : "" }), - br(), - br(), - label(i18n.housingTags), - br(), - input({ type: "text", name: "tags", value: Array.isArray(item.tags) ? item.tags.join(", ") : (item.tags || "") }), - br(), - br(), - label(i18n.visibilityLabel || "Visibility"), - br(), - select({ name: "visibility" }, - option({ value: "PUBLIC", ...((item.visibility || "PUBLIC") === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"), - option({ value: "HIDDEN", ...(item.visibility === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden") - ), - br(), - br(), - button({ type: "submit" }, isEdit ? i18n.housingUpdateButton : i18n.housingCreateButton) - ) - ) -} - -const renderFiltersBar = (filter, params = {}) => - div({ class: "filters" }, - form({ method: "GET", action: "/housing", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "search", value: safeText(params.search || "") }), - input({ type: "hidden", name: "minPrice", value: String(params.minPrice ?? "") }), - input({ type: "hidden", name: "maxPrice", value: String(params.maxPrice ?? "") }), - input({ type: "hidden", name: "place", value: safeText(params.place || "") }), - input({ type: "hidden", name: "sort", value: safeText(params.sort || "") }), - ...FILTERS.map(f => - button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) - ), - button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.housingCreateButton) - ) - ) - -exports.housingView = async (items, filter = "ALL", params = {}) => { - const search = safeText(params.search || "") - const minPrice = params.minPrice ?? "" - const maxPrice = params.maxPrice ?? "" - const place = safeText(params.place || "") - const sort = safeText(params.sort || "recent") - - const isForm = filter === "CREATE" || filter === "EDIT" - - return template( - i18n.housingTitle, - section( - div({ class: "tags-header" }, - h2(i18n.housingTitle), - p(i18n.housingDescriptionText) - ), - renderFiltersBar(filter, { search, minPrice, maxPrice, place, sort }) - ), - section( - isForm - ? renderHousingForm(filter === "EDIT" ? (Array.isArray(items) ? items[0] : items) || {} : (params.draft || {}), filter === "EDIT" ? "edit" : "create", Number(params.maxImages) > 0 ? Number(params.maxImages) : MAX_IMAGES, await renderSpreadEditWarning(filter === "EDIT" ? ((Array.isArray(items) ? items[0] : items) || {}).id : null)) - : section( - div({ class: "housing-search" }, - form({ method: "GET", action: "/housing", class: "filter-box" }, - input({ type: "hidden", name: "filter", value: filter || "ALL" }), - input({ type: "text", name: "search", value: search, placeholder: i18n.housingSearchPlaceholder, class: "filter-box__input" }), - div({ class: "filter-box__controls" }, - input({ type: "text", name: "place", value: place, placeholder: i18n.housingPlacePlaceholder, class: "filter-box__input housing-place-input" }), - div({ class: "transfer-range" }, - input({ type: "number", name: "minPrice", step: "0.01", min: "0", value: String(minPrice ?? ""), placeholder: i18n.housingMinPrice, class: "filter-box__number transfer-amount-input" }), - input({ type: "number", name: "maxPrice", step: "0.01", min: "0", value: String(maxPrice ?? ""), placeholder: i18n.housingMaxPrice, class: "filter-box__number transfer-amount-input" }) - ), - select({ name: "sort", class: "filter-box__select" }, - option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.housingSortRecent), - option({ value: "price", ...(sort === "price" ? { selected: true } : {})}, i18n.housingSortPrice), - option({ value: "requests", ...(sort === "requests" ? { selected: true } : {})}, i18n.housingSortRequests), - option({ value: "rating", ...(sort === "rating" ? { selected: true } : {})}, i18n.housingSortRating) - ), - button({ type: "submit", class: "filter-box__button" }, i18n.housingSearchButton) - ) - ) - ), - br(), - div({ class: "housing-list" }, renderHousingList(items, filter, params)) - ) - ) - ) -} - -const renderCommentsSection = (itemId, returnTo, comments = []) => { - return renderSharedCommentsSection({ - action: `/housing/${encodeURIComponent(itemId)}/comments`, - comments: comments, - returnTo: returnTo - }); -}; - -exports.singleHousingView = async (item, filter = "ALL", comments = [], params = {}) => { - const returnTo = safeText(params.returnTo) || buildReturnTo(filter, params) - const isAuthor = String(item.author) === String(userId) - const requested = safeArr(item.requests).includes(userId) - const voters = safeArr(item.opinions_inhabitants) - - const chips = [ - renderTypeChip(item), - renderStatusChip(item.status), - item.visibility === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, - requested ? renderRequestedChip() : null, - renderLifespanChip(item.lifetime, i18n), - renderEcoTax(item.msgSize, item.id) - ].filter(Boolean) - - const sideActions = [] - const requestToggle = renderRequestToggle(item, returnTo) - if (requestToggle) sideActions.push(requestToggle) - const ownerActions = renderOwnerActions(item, returnTo) - - const nextVisibility = item.visibility === "PUBLIC" ? "HIDDEN" : "PUBLIC" - const isHidden = item.visibility === "HIDDEN" - const visibilityRow = isAuthor - ? div({ class: "tribe-side-actions shop-visibility-row housing-visibility-row" }, - span({ class: "card-label" }, `${i18n.visibilityLabel || "Visibility"}: `), - isHidden - ? renderStateChip("encrypted", "🔒", i18n.encryptedChipLabel || "E2E") - : renderStateChip("mutuals", "👁", i18n.visibilityPublic || "PUBLIC"), - form({ method: "POST", action: `/housing/visibility/${encodeURIComponent(item.id)}`, class: "inline-form" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "visibility", value: nextVisibility }), - button({ type: "submit", class: "tribe-action-btn" }, - nextVisibility === "PUBLIC" ? (i18n.visibilityMakePublic || "Make public") : (i18n.visibilityMakeHidden || "Make hidden")) - ) - ) - : null - - const cover = imagesOf(item)[0] - const housingSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(item.id, null, { spread: params.spreads || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title }) - ), - div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, safeText(item.title) || i18n.housingTitle) - ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - renderStarRating(item.opinions, voters.length), - cover && !isVideoEntry(cover) ? img({ src: blobUrl(blobIdOf(cover), 256), class: "tribe-detail-image", alt: "" }) : null, - renderInfoTable(item), - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${i18n.housingRequests}: ${item.requestCount || 0}`) - ), - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, - renderStatusRow(item, returnTo), - ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, - visibilityRow, - renderTags(item.tags) - ) - - const renderSection = (titleText, bodyText) => - safeText(bodyText) - ? div({ class: "job-section" }, - h2({ class: "job-section-title" }, titleText), - p({ class: "tribe-side-description" }, ...renderUrl(bodyText)) - ) - : null - - const housingMain = div({ class: "tribe-main" }, - renderPhotoGallery(item), - renderSection(i18n.housingDescription, item.description), - renderSection(i18n.housingRules, item.rules), - item.mapUrl ? div({ class: "job-section" }, renderMapEmbed(params.mapData, item.mapUrl)) : null, - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(item.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), - userLink(item.author) - ), - renderEngagement(item.id, - !isAuthor && item.everRequestedByViewer - ? renderOpinionsVoting("/housing/opinions", item.id, item.opinions, returnTo, voters) - : null, - renderCommentsSection(item.id, returnTo, comments) - ) - ) - - return template( - i18n.housingTitle, - section( - div({ class: "tags-header" }, h2(i18n.housingTitle), p(i18n.housingDescriptionText)), - renderFiltersBar(filter, params), - div({ class: "tribe-details" }, housingSide, housingMain) - ) - ) -} diff --git a/src/views/identities_view.js b/src/views/identities_view.js deleted file mode 100644 index c1e09b2..0000000 --- a/src/views/identities_view.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Selector de identidades para los ajustes. - * - * Vive aparte de settings_view.js para que las reescrituras de upstream sobre ese - * fichero no arrastren esto: alli solo hay una linea que llama a renderIdentities(). - * - * No cambia de identidad en caliente. Anota cual sera la del proximo arranque y lo - * dice; el porque esta en src/backend/accounts.js. - */ - -module.exports = ({ i18n, html }) => { - const { form, button, div, h2, p, section, input, span, ul, li, a } = html; - const accounts = require('../backend/accounts.js'); - - const renderIdentities = (activeAccount) => { - const names = accounts.list(); - const feeds = new Map(names.map((n) => [n, accounts.feedIdOf(n)])); - - // dos carpetas con el mismo feed son la misma identidad copiada: publicar desde - // las dos parte el registro y no tiene arreglo, asi que se avisa en la lista - const seen = new Map(); - for (const [n, id] of feeds) { - if (!id) continue; - seen.set(id, (seen.get(id) || 0) + 1); - } - - const row = (name) => { - const id = feeds.get(name); - const isActive = name === activeAccount; - const duplicated = id && seen.get(id) > 1; - return li({ class: "identity-row" }, - div({ class: "identity-info" }, - span({ class: "identity-name" }, name), - span({ class: "identity-feed" }, id ? `${id.slice(0, 18)}…` : "—"), - duplicated ? span({ class: "identity-warn" }, i18n.identitiesSameFeed) : null - ), - isActive - ? span({ class: "identity-active" }, i18n.identitiesActive) - : form({ method: "POST", action: "/settings/identity/use" }, - input({ type: "hidden", name: "account", value: name }), - button({ type: "submit" }, i18n.identitiesUse) - ) - ); - }; - - // en movil el backend lo mantiene vivo un servicio en primer plano, asi que - // cerrar la app no basta para reiniciarlo: hace falta pedirlo - const restart = process.env.OASIS_MOBILE === '1' - ? form({ method: "POST", action: "/settings/identity/restart" }, - button({ type: "submit" }, i18n.identitiesRestart)) - : null; - - return section( - div({ class: "tags-header" }, - h2(i18n.identitiesTitle), - p(i18n.identitiesDescription), - ul({ class: "identity-list" }, names.map(row)), - form({ method: "POST", action: "/settings/identity/create" }, - input({ type: "text", name: "account", placeholder: i18n.identitiesNewName, - maxlength: "32", autocomplete: "off" }), - button({ type: "submit" }, i18n.identitiesCreate) - ), - // sacar o meter una identidad ya existente es cosa del modulo legacy, que - // es el que sabe cifrar el secret con una contraseña - p(a({ href: "/legacy", class: "identity-legacy" }, i18n.identitiesBackup)), - restart - ) - ); - }; - - return { renderIdentities }; -}; diff --git a/src/views/image_view.js b/src/views/image_view.js index 8071fac..36e74e8 100644 --- a/src/views/image_view.js +++ b/src/views/image_view.js @@ -1,9 +1,8 @@ const { form, button, div, h2, p, section, input, label, br, a, img, span, textarea, select, option } = require("../server/node_modules/hyperaxe"); -const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions , renderSpreadEditWarning } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions } = require("./main_views"); const { config } = require("../server/SSB_server.js"); const { renderUrl } = require("../backend/renderUrl") const { renderMapLocationVisitLabel } = require("./maps_view"); @@ -23,6 +22,18 @@ const buildReturnTo = (filter, params = {}) => { return `/images?${parts.join("&")}`; }; +const renderPMButton = (recipient, className = "filter-btn") => { + const r = safeText(recipient); + if (!r) return null; + if (String(r) === String(userId)) return null; + + return form( + { method: "GET", action: "/pm" }, + input({ type: "hidden", name: "recipients", value: r }), + button({ type: "submit", class: className }, i18n.privateMessage) + ); +}; + const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -33,12 +44,27 @@ const renderTags = (tags) => { : null; }; +const renderImageFavoriteToggle = (imgObj, returnTo = "") => + form( + { + method: "POST", + action: imgObj.isFavorite + ? `/images/favorites/remove/${encodeURIComponent(imgObj.key)}` + : `/images/favorites/add/${encodeURIComponent(imgObj.key)}` + }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + button( + { type: "submit", class: "filter-btn" }, + imgObj.isFavorite ? i18n.imageRemoveFavoriteButton : i18n.imageAddFavoriteButton + ) + ); + const renderImageMedia = (imgObj, filter, params = {}) => { const src = imgObj?.url ? `/blob/${encodeURIComponent(imgObj.url)}` : ""; return imgObj?.url ? div( - { class: "image-container image-container-row" }, + { class: "image-container", style: "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" }, a( { href: `/images/${encodeURIComponent(imgObj.key)}?filter=${encodeURIComponent(filter || "all")}${ @@ -85,6 +111,7 @@ const renderImageList = exports.renderImageList = (images, filter, params = {}) ? images.map((imgObj) => { const commentCount = typeof imgObj.commentCount === "number" ? imgObj.commentCount : 0; const title = safeText(imgObj.title); + const ownerActions = renderImageOwnerActions(filter, imgObj, params); const isOwn = imgObj.author && String(imgObj.author) === String(userId); return div( @@ -92,19 +119,50 @@ const renderImageList = exports.renderImageList = (images, filter, params = {}) div( { class: "card-header activity-card-header" }, span(), - renderContentActions(imgObj.key, `/images/${encodeURIComponent(imgObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(imgObj.key)) || params.spreads || null, author: imgObj.author, favKind: 'images', isFavorite: imgObj.isFavorite, reportTitle: imgObj.title }) + renderContentActions(imgObj.key, `/images/${encodeURIComponent(imgObj.key)}`) ), div( { class: "card-section image-card-body" }, + div( + { class: "bookmark-topbar" }, + div( + { class: "bookmark-topbar-left" }, + renderImageFavoriteToggle(imgObj, returnTo), + renderPMButton(imgObj.author) + ), + ownerActions.length ? div({ class: "bookmark-actions" }, ...ownerActions) : null + ), title ? h2(title) : null, - imgObj.lifetime ? div({ class: "card-chips-row" }, + (imgObj.meme || imgObj.lifetime) ? div({ class: "card-chips-row" }, + imgObj.meme ? a({ href: "/images?filter=meme", class: "chip-link" }, renderStateChip("mutuals", null, i18n.imageFilterMeme || "MEME")) : null, imgObj.lifetime ? renderLifespanChip(imgObj.lifetime, i18n) : null ) : null, renderImageMedia(imgObj, filter, params), - renderEngagement(imgObj.key, - renderOpinionsVoting('/images/opinions', imgObj.key, imgObj.opinions, returnTo, imgObj.opinions_inhabitants), - renderCommentsLink({ href: `/images/${encodeURIComponent(imgObj.key)}`, count: commentCount }) + div( + { class: "card-comments-summary" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), + span({ class: "card-value" }, String(commentCount)), + br(), + br(), + form( + { method: "GET", action: `/images/${encodeURIComponent(imgObj.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + input({ type: "hidden", name: "filter", value: filter || "all" }), + params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, + params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, + button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) + ) ), + div( + { class: "card-comments-summary feed-opinions-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), + span({ class: "card-value" }, String(Object.values(imgObj.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) + ), + renderOpinionsVoting('/images/opinions', imgObj.key, imgObj.opinions, returnTo, imgObj.opinions_inhabitants) + ), + div({ class: "card-spread-left" }, renderSpreadButton(imgObj.key, (params.spreadMap && params.spreadMap.get(imgObj.key)) || params.spreads)), renderMapLocationVisitLabel(imgObj.mapUrl), br(), (() => { @@ -114,12 +172,12 @@ const renderImageList = exports.renderImageList = (images, filter, params = {}) return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(imgObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(imgObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), userLink(imgObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.imageUpdatedAt}: ${moment(imgObj.updatedAt).format("YYYY/MM/DD HH:mm")}` + ` | ${i18n.imageUpdatedAt}: ${moment(imgObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` ) : null ); @@ -137,7 +195,6 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => { return div( { class: "div-center image-form" }, - params.spreadWarning || null, form( { action: filter === "edit" ? `/images/update/${encodeURIComponent(imageId)}` : "/images/create", @@ -155,7 +212,7 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => { : null, label(i18n.imageTitleLabel), br(), - input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.imageTitlePlaceholder, value: imageToEdit?.title || "" }), + input({ type: "text", name: "title", placeholder: i18n.imageTitlePlaceholder, value: imageToEdit?.title || "" }), br(), label(i18n.imageDescriptionLabel), br(), @@ -165,10 +222,21 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => { br(), input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: imageToEdit?.mapUrl || "" }), br(), + input({ type: "hidden", name: "meme", value: "0" }), label(i18n.imageTagsLabel), br(), input({ type: "text", name: "tags", placeholder: i18n.imageTagsPlaceholder, value: tagsValue }), br(), + label(i18n.imageMemeLabel), + input({ + id: "meme-checkbox", + type: "checkbox", + name: "meme", + value: "1", + class: "meme-checkbox", + ...(imageToEdit?.meme ? { checked: true } : {}) + }), + br(), br(), button({ type: "submit" }, filter === "edit" ? i18n.imageUpdateButton : i18n.imageCreateButton) ) @@ -201,16 +269,88 @@ const renderLightbox = (images) => }); const renderImageCommentsSection = (imageKey, comments = [], returnTo = null) => { - return renderSharedCommentsSection({ - action: `/images/${encodeURIComponent(imageKey)}/comments`, - comments: comments, - returnTo: returnTo + const list = safeArr(comments).filter(c => { + const t = c && c.value && c.value.content && c.value.content.text; + return t && String(t).trim(); }); + const commentsCount = list.length; + + return div( + { class: "vote-comments-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), + span({ class: "card-value" }, String(commentsCount)) + ), + div( + { class: "comment-form-wrapper" }, + h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), + form( + { method: "POST", action: `/images/${encodeURIComponent(imageKey)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, + returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, + textarea({ + id: "comment-text", + name: "text", + rows: 4, + class: "comment-textarea", + placeholder: i18n.voteNewCommentPlaceholder + }), + div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), + br(), + button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) + ) + ), + list.length + ? div( + { class: "comments-list" }, + list.map((c) => { + const author = c?.value?.author || ""; + const ts = c?.value?.timestamp || c?.timestamp; + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; + const relDate = ts ? moment(ts).fromNow() : ""; + + const content = c?.value?.content || {}; + const text = content.text || ""; + const threadRoot = content.fork || content.root || null; + + return div( + { class: "votations-comment-card" }, + span( + { class: "created-at" }, + span(i18n.createdBy), + author ? userLink(author) : span("(unknown)"), + absDate ? span(" | ") : "", + absDate ? span({ class: "votations-comment-date" }, absDate) : "", + relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", + relDate && threadRoot ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) : "" + ), + p({ class: "votations-comment-text" }, ...renderUrl(text)) + ); + }) + ) + : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) + ); }; exports.imageView = async (images, filter = "all", imageId = null, params = {}) => { - if (filter === "edit") params = { ...params, spreadWarning: await renderSpreadEditWarning(imageId) }; - const title = i18n.imageTitle; + const title = + filter === "mine" + ? i18n.imageMineSectionTitle + : filter === "create" + ? i18n.imageCreateSectionTitle + : filter === "edit" + ? i18n.imageUpdateSectionTitle + : filter === "gallery" + ? i18n.imageGallerySectionTitle + : filter === "meme" + ? i18n.imageMemeSectionTitle + : filter === "recent" + ? i18n.imageRecentSectionTitle + : filter === "top" + ? i18n.imageTopSectionTitle + : filter === "favorites" + ? i18n.imageFavoritesSectionTitle + : i18n.imageAllSectionTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); @@ -237,18 +377,19 @@ exports.imageView = async (images, filter = "all", imageId = null, params = {}) { method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterMine).toUpperCase()), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase()), button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterAll), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMine), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterRecent), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterTop), + button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - String(i18n.imageFilterFavorites).toUpperCase() + i18n.imageFilterFavorites ), - - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterTop).toUpperCase()), button( { type: "submit", name: "filter", value: "gallery", class: filter === "gallery" ? "filter-btn active" : "filter-btn" }, - String(i18n.imageFilterGallery).toUpperCase() + i18n.imageFilterGallery ), + button({ type: "submit", name: "filter", value: "meme", class: filter === "meme" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMeme), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.imageCreateButton) ) ) @@ -273,9 +414,9 @@ exports.imageView = async (images, filter = "all", imageId = null, params = {}) { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.imageSortRecent), - option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.imageSortOldest), - option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.imageSortTop) + option({ value: "recent", selected: sort === "recent" }, i18n.imageSortRecent), + option({ value: "oldest", selected: sort === "oldest" }, i18n.imageSortOldest), + option({ value: "top", selected: sort === "top" }, i18n.imageSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.imageSearchButton) ) @@ -297,28 +438,25 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params const { renderReachChip } = require('./clearnet_view'); const isClearnet = !!(params.authorPrefs && params.authorPrefs.clearnetImages); + const memeChip = imageObj.meme + ? a({ href: "/images?filter=meme", class: "chip-link" }, renderStateChip("mutuals", null, i18n.imageFilterMeme || "MEME")) + : null; const chips = [ + memeChip, renderLifespanChip(imageObj.lifetime, i18n), imageObj.sizeBytes ? renderEcoTax(imageObj.sizeBytes, imageObj.key) : null ].filter(Boolean); const ownerActions = renderImageOwnerActions(filter, imageObj, { q, sort }); const sideActions = []; + sideActions.push(renderImageFavoriteToggle(imageObj, returnTo)); + if (imageObj.author && String(imageObj.author) !== String(userId)) { + sideActions.push(renderPMButton(imageObj.author)); + } for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(imageObj.tags); - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(imageObj.key, null, { - author: imageObj.author, - favKind: 'images', - isFavorite: imageObj.isFavorite, - spread: params.spreads || null, - returnTo, - reportTitle: imageObj.title - }) - ); - const imageSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, @@ -329,12 +467,12 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params ? p({ class: "tribe-side-description" }, ...renderUrl(imageObj.description)) : null, tagsNode, + div({ class: "card-spread-centered" }, renderSpreadButton(imageObj.key, params.spreads)), renderMapLocationVisitLabel(imageObj.mapUrl), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const imageMain = div({ class: "tribe-main" }, - detailActions, imageObj?.url ? div( { class: "image-container" }, @@ -353,20 +491,18 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(imageObj.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(imageObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), userLink(imageObj.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.imageUpdatedAt}: ${moment(imageObj.updatedAt).format("YYYY/MM/DD HH:mm")}` + ` | ${i18n.imageUpdatedAt}: ${moment(imageObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` ) : null ); })(), - renderEngagement(imageObj.key, - renderOpinionsVoting('/images/opinions', imageObj.key, imageObj.opinions, returnTo, imageObj.opinions_inhabitants), - renderImageCommentsSection(imageObj.key, comments, returnTo) - ) + renderOpinionsVoting('/images/opinions', imageObj.key, imageObj.opinions, returnTo, imageObj.opinions_inhabitants), + renderImageCommentsSection(imageObj.key, comments, returnTo) ); return template( @@ -382,18 +518,19 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params { method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "q", value: q }), input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterMine).toUpperCase()), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase()), button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterAll), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMine), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterRecent), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterTop), + button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, - String(i18n.imageFilterFavorites).toUpperCase() + i18n.imageFilterFavorites ), - - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterTop).toUpperCase()), button( { type: "submit", name: "filter", value: "gallery", class: filter === "gallery" ? "filter-btn active" : "filter-btn" }, - String(i18n.imageFilterGallery).toUpperCase() + i18n.imageFilterGallery ), + button({ type: "submit", name: "filter", value: "meme", class: filter === "meme" ? "filter-btn active" : "filter-btn" }, i18n.imageFilterMeme), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.imageCreateButton) ) ), diff --git a/src/views/industry_view.js b/src/views/industry_view.js deleted file mode 100644 index c04be4d..0000000 --- a/src/views/industry_view.js +++ /dev/null @@ -1,984 +0,0 @@ -const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, ul, li, img, video, audio, table, thead, tbody, tr, td, th } = require("../server/node_modules/hyperaxe") -const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions , renderSpreadEditWarning } = require("./main_views") -const moment = require("../server/node_modules/moment") -const { config } = require("../server/SSB_server.js") -const { renderMapEmbedWithZoom } = require("./maps_view") -const { renderUrl } = require("../backend/renderUrl") - -const userId = config.keys.id - -const renderMediaBlob = (value, attrs = {}) => { - if (!value) return null - const s = String(value).trim() - if (!s) return null - if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, ...attrs }) - const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` }) - const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) - const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: attrs.class || 'post-image' }) - return null -} - -const SECTORS = ["software", "hardware", "agriculture", "textile", "energy", "food", "construction", "media", "services", "other"] -const POLICIES = ["open", "vote", "invite"] - -const FILTERS = [ - { key: "ALL", i18n: "industryFilterAll", title: "industryAllTitle" }, - { key: "MINE", i18n: "industryFilterMine", title: "industryMineTitle" }, - { key: "ACTIVE", i18n: "industryFilterActive", title: "industryActiveTitle" }, - { key: "PAUSED", i18n: "industryFilterPaused", title: "industryPausedTitle" }, - { key: "DISSOLVED", i18n: "industryFilterDissolved", title: "industryDissolvedTitle" }, - { key: "BLUEPRINTS", i18n: "industryFilterBlueprints", title: "industryBlueprints" }, - { key: "BUILDS", i18n: "industryFilterBuilds", title: "industryBuilds" }, - { key: "MEMBER", i18n: "industryFilterMember", title: "industryMemberTitle" }, - { key: "RULES", i18n: "industryFilterRules", title: "industryRulesTitle" } -] - -const renderIndustryRules = () => { - const points = [ - i18n.industryRulesIntro, - i18n.industryRulesSteward, - i18n.industryRulesMembership, - i18n.industryRulesQuorum, - i18n.industryRulesMajority, - i18n.industryRulesDecisions, - i18n.industryRulesBlueprints, - i18n.industryRulesBuilds, - i18n.industryRulesContributions, - i18n.industryRulesLaborRate, - i18n.industryRulesShares, - i18n.industryRulesDistribution, - i18n.industryRulesTreasury - ] - return div({ class: "card" }, - h2(i18n.industryRulesTitle || "Rules"), - div({ class: "rules-points" }, - points.filter(Boolean).map((t, i) => - div({ class: "rules-point" }, - span({ class: "rules-point-num" }, String(i + 1)), - span({ class: "rules-point-text" }, t) - ) - ) - ) - ) -} - -const safeArr = (v) => (Array.isArray(v) ? v : []) -const safeText = (v) => String(v || "").trim() - -const sectorLabel = (key) => i18n[`industrySector_${key}`] || key -const policyLabel = (key) => i18n[`industryPolicy_${key}`] || key - -const renderStatusChip = (status) => { - const s = String(status || "ACTIVE").toUpperCase() - const map = { - ACTIVE: ["whole", "🟢", i18n.industryStatusActive || "ACTIVE"], - PAUSED: ["half", "⏸", i18n.industryStatusPaused || "PAUSED"], - DISSOLVED: ["tombstoned", "⚰", i18n.industryStatusDissolved || "DISSOLVED"] - } - const [variant, icon, text] = map[s] || map.ACTIVE - return renderStateChip(variant, icon, text) -} - -const renderStatusBlock = (fc, returnTo) => { - const isMember = safeArr(fc.members).includes(userId) - const status = String(fc.status || "ACTIVE").toUpperCase() - const myPause = safeArr(fc.pauseVoters).includes(userId) - return div({ class: "industry-section industry-status-block" }, - div({ class: "card-chips-row" }, renderStatusChip(status)), - (isMember && status !== "DISSOLVED") - ? [ - p({ class: "industry-hint" }, `${i18n.industryPauseVotes || "Pause votes"}: ${fc.pauseYes || 0}`), - form({ method: "POST", action: `/industry/govern/${encodeURIComponent(fc.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "subject", value: "pause" }), - input({ type: "hidden", name: "ref", value: "" }), - button({ type: "submit", name: "choice", value: myPause ? "no" : "yes", class: "filter-btn" }, myPause ? (i18n.industryResumeButton || "Vote to resume") : (i18n.industryPauseButton || "Vote to pause")) - ) - ] - : null - ) -} - -const renderFacilityList = exports.renderFacilityList = (facilities, filter, spreadMap = new Map()) => { - const list = safeArr(facilities) - if (!list.length) return p(i18n.industryNoFacilitiesFound || "No facilities found.") - return div({ class: "tribe-grid" }, - list.map((fc) => { - const isOwn = fc.steward && String(fc.steward) === String(userId) - const isMember = safeArr(fc.members).includes(userId) - const href = `/industry/${encodeURIComponent(fc.id)}` - const chips = [ - renderStatusChip(fc.status), - renderStateChip("whole", "🏭", sectorLabel(fc.sector)), - renderStateChip("half", "⚖", policyLabel(fc.membershipPolicy)), - isMember ? renderStateChip("whole", "★", i18n.industryMemberBadge || "MEMBER") : null, - renderLifespanChip(fc.lifetime, i18n) - ].filter(Boolean) - return div({ class: "trending-card tribes-card industry-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(fc.id || fc.key, href, { spread: spreadMap.get(fc.id || fc.key) || null }) - ), - div({ class: "card-section tribes-card-body" }, - div({ class: "tribe-card-image-wrapper" }, - a({ href }, renderMediaBlob(fc.image, { class: "tribe-card-hero-image" }) || div({ class: "industry-card-noimage" }, span("🏭"))), - isMember - ? form({ method: "GET", action: href, class: "tribe-visit-btn-wrapper" }, - button({ type: "submit", class: "filter-btn" }, String(i18n.industryVisitButton || "VISIT").toUpperCase()) - ) - : null - ), - div({ class: "tribe-card-body" }, - div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, a({ href }, safeText(fc.name) || (i18n.industryTitle || "Industry"))) - ), - div({ class: "card-chips-row" }, ...chips), - fc.description ? p({ class: "tribe-card-description" }, safeText(fc.description).slice(0, 220)) : null, - div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${i18n.industryMembers || "Members"}: ${fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length}`) - ), - ) - ) - ) - }) - ) -} - -const renderFacilityForm = (facility, mode, spreadWarning = null) => { - const fc = facility || {} - const isEdit = mode === "edit" - const returnTo = "/industry?filter=MINE" - const curPolicy = fc.membershipPolicy || "vote" - const curSector = fc.sector || "other" - return div({ class: "div-center industry-form" }, - isEdit ? spreadWarning : null, - form({ - action: isEdit ? `/industry/update/${encodeURIComponent(fc.id)}` : "/industry/create", - method: "POST", - enctype: "multipart/form-data" - }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - label(i18n.industryName || "Name"), - br(), - input({ type: "text", name: "name", required: true, maxlength: "80", placeholder: i18n.industryNamePlaceholder || "Facility name", value: fc.name || "" }), - br(), - label(i18n.industryDescriptionLabel || "Description"), - br(), - textarea({ name: "description", rows: "5", maxlength: "2000", placeholder: i18n.industryDescriptionPlaceholder || "What does this facility produce?" }, fc.description || ""), - br(), - label(i18n.uploadMedia || "Upload media (max-size: 50MB)"), - br(), - input({ type: "file", name: "image" }), - fc.image ? div({ class: "industry-form-media" }, renderMediaBlob(fc.image, { class: "industry-hero-image" })) : null, - br(), - label(i18n.industrySector || "Sector"), - br(), - select({ name: "sector" }, - SECTORS.map((s) => option({ value: s, ...(s === curSector ? { selected: true } : {}) }, sectorLabel(s))) - ), - br(), - label(i18n.mapLocationTitle || "Map Location"), - br(), - input({ type: "text", name: "mapUrl", placeholder: i18n.mapUrlPlaceholder || "/maps/MAP_ID", value: fc.mapUrl || "" }), - br(), - label(i18n.shopTags || "Tags"), - br(), - input({ type: "text", name: "tags", placeholder: i18n.shopTagsPlaceholder || "tag1, tag2, tag3", value: safeArr(fc.tags).join(", ") }), - br(), - label(i18n.industryMembershipPolicy || "Membership policy"), - br(), - select({ name: "membershipPolicy" }, - POLICIES.map((pol) => option({ value: pol, ...(pol === curPolicy ? { selected: true } : {}) }, policyLabel(pol))) - ), - br(), - label(i18n.industryQuorum || "Quorum"), - br(), - input({ type: "number", name: "quorum", min: "1", step: "1", value: fc.quorum != null ? fc.quorum : 1 }), - br(), - label(i18n.industryMajority || "Majority"), - br(), - input({ type: "number", name: "majority", min: "0.5", max: "1", step: "0.01", value: fc.majority != null ? fc.majority : 0.5 }), - br(), - label(i18n.industryLaborRate || "Labor rate"), - br(), - input({ type: "number", name: "laborRate", min: "0", step: "0.01", value: fc.laborRate != null ? fc.laborRate : 0 }), - br(), - button({ type: "submit" }, isEdit ? (i18n.industryUpdateButton || "Update facility") : (i18n.industryCreateButton || "Create facility")) - ) - ) -} - -const renderGlobalBlueprints = (blueprints, spreadMap = new Map()) => { - const list = safeArr(blueprints) - if (!list.length) return p(i18n.industryNoBlueprints || "No blueprints yet.") - return div({ class: "industry-blueprints" }, - list.map((bp) => div({ class: "industry-card-wrap" }, - div({ class: "card-header activity-card-header" }, span(), renderContentActions(bp.id, "/industry/blueprint/" + encodeURIComponent(bp.id))), - div({ class: "industry-blueprint-card" + (bp.author === userId ? " own-content" : "") }, - div({ class: "card-chips-row" }, - renderSpreadButton(bp.id, spreadMap.get(bp.id)), - renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind), - renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase()) - ), - p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: `/industry/${encodeURIComponent(bp.facilityId)}` }, safeText(bp.facilityName))), - div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/industry/blueprint/${encodeURIComponent(bp.id)}` }, safeText(bp.name)))), - bp.image ? renderMediaBlob(bp.image, { class: "post-image" }) : null, - bp.description ? p({ class: "tribe-card-description" }, ...renderUrl(safeText(bp.description))) : null, - renderBlueprintLaborHours(bp), - safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} × ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, - renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), - safeArr(bp.skills).length ? p(span({ class: "industry-meta-label" }, `${i18n.industrySkills || "Skills"}: `), bp.skills.join(", ")) : null, - (() => { - const acts = renderBlueprintGovernActions(bp.facilityId, bp, safeArr(bp.members).includes(userId) || bp.author === userId, "/industry?filter=BLUEPRINTS") - return acts.length ? div({ class: "industry-actions" }, ...acts) : null - })() - ) - )) - ) -} - -const renderGovernVoteButton = (facilityId, subject, ref, voters, yes, need, labelKey, fallback, returnTo) => { - const mine = safeArr(voters).includes(userId) - return form({ method: "POST", action: `/industry/govern/${encodeURIComponent(facilityId)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "subject", value: subject }), - input({ type: "hidden", name: "ref", value: ref }), - button({ type: "submit", name: "choice", value: mine ? "no" : "yes", class: "filter-btn" }, - `${mine ? (i18n.industryRevokeVote || "Revoke") : (i18n[labelKey] || fallback)} (${yes || 0}/${need || 1})`) - ) -} - -const renderBuildGovernActions = (b, isMember, returnTo) => { - const isProposer = b.proposer === userId - const multi = (b.facilityMembers || 1) > 1 - const facilityId = b.facilityId || "" - return [ - (isMember && isProposer && b.updateApproved !== false) - ? form({ method: "GET", action: `/industry/builds/${encodeURIComponent(b.id)}/edit` }, button({ type: "submit", class: "update-btn" }, i18n.industryUpdateButton || "Update")) - : null, - (isMember && isProposer && b.deleteApproved !== false) - ? form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/delete` }, button({ type: "submit", class: "delete-btn danger-btn" }, i18n.industryDeleteButton || "Delete")) - : null, - (isMember && multi) ? renderGovernVoteButton(facilityId, "buildUpdate", b.id, b.updateVoters, b.updateYes, b.voteNeed, "industryVoteUpdateButton", "Vote update", returnTo) : null, - (isMember && multi) ? renderGovernVoteButton(facilityId, "buildDelete", b.id, b.deleteVoters, b.deleteYes, b.voteNeed, "industryVoteDeleteButton", "Vote delete", returnTo) : null - ].filter(Boolean) -} - -const renderBuildCard = (b, opts = {}) => { - const href = `/industry/build/${encodeURIComponent(b.id)}` - const card = div({ class: "industry-blueprint-card" + (b.proposer === userId ? " own-content" : "") }, - div({ class: "card-chips-row" }, - renderSpreadButton(b.id, opts.spread), - renderBuildStatusChip(b.status), - ...((b.blueprintKind && opts.withBlueprintChips !== false) ? [ - renderStateChip("half", b.blueprintKind === "digital" ? "💾" : "📦", i18n["industryKind_" + b.blueprintKind] || b.blueprintKind), - renderStateChip("half", "⚖", String(b.blueprintLicense || "copyleft").toUpperCase()) - ] : []) - ), - opts.withFacility ? p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: `/industry/${encodeURIComponent(b.facilityId)}` }, safeText(b.facilityName))) : null, - opts.withProposer !== false ? p(span({ class: "industry-meta-label" }, `${i18n.industryProposer || "Proposer"}: `), userLink(b.proposer)) : null, - div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, safeText(b.title)))), - b.blueprintImage ? renderMediaBlob(b.blueprintImage, { class: "post-image" }) : null, - b.notes ? p({ class: "tribe-card-description" }, ...renderUrl(safeText(b.notes))) : null, - div({ class: "industry-blueprint-meta industry-dates-row" }, - b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY/MM/DD")) : null, - b.endDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildEnd || "End date"}: `), moment(b.endDate).format("YYYY/MM/DD")) : null, - ), - renderBlueprintEstimate(b, i18n.industryFinalPrice || "Final price"), - p(span({ class: "industry-meta-label" }, `${i18n.industryContributions || "Contributions"}: `), String(safeArr(b.contributions).length)), - (opts.actions && opts.actions.length) ? div({ class: "industry-actions" }, ...opts.actions) : null - ) - if (!opts.withActions) return card - return div({ class: "industry-card-wrap" }, - div({ class: "card-header activity-card-header" }, span(), renderContentActions(b.id, href)), - card - ) -} - -const renderGlobalBuilds = (builds, spreadMap = new Map()) => { - const list = safeArr(builds) - if (!list.length) return p(i18n.industryNoBuilds || "No builds yet.") - return div({ class: "industry-blueprints" }, - list.map((b) => renderBuildCard(b, { withFacility: true, withActions: true, spread: spreadMap.get(b.id), actions: renderBuildGovernActions(b, safeArr(b.members).includes(userId) || b.proposer === userId, "/industry?filter=BUILDS") })) - ) -} - -exports.industryView = async (facilitiesOrForm, filter, params = {}) => { - const f = String(filter || "ALL").toUpperCase() - const facilityEditWarning = f === "EDIT" ? await renderSpreadEditWarning((safeArr(facilitiesOrForm)[0] || {}).id) : null - const search = safeText(params.search) - const sectorSel = safeText(params.sector) - const isForm = f === "CREATE" || f === "EDIT" - const isRules = f === "RULES" - return template( - i18n.industryTitle || "Industry", - section( - div({ class: "tags-header" }, - h2(i18n.industryTitle || "Industry"), - p(i18n.industryDescription || "Network-owned production facilities.") - ), - br(), - div({ class: "filters" }, - form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, - input({ type: "hidden", name: "search", value: search }), - input({ type: "hidden", name: "sector", value: sectorSel }), - FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) - .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) - ) - ), - isRules - ? renderIndustryRules() - : isForm - ? renderFacilityForm(f === "EDIT" ? (safeArr(facilitiesOrForm)[0] || {}) : {}, f === "EDIT" ? "edit" : "create", facilityEditWarning) - : f === "BLUEPRINTS" - ? div({ class: "industry-list" }, renderGlobalBlueprints(facilitiesOrForm, params.spreadMap || new Map())) - : f === "BUILDS" - ? div({ class: "industry-list" }, renderGlobalBuilds(facilitiesOrForm, params.spreadMap || new Map())) - : section( - div({ class: "industry-search" }, - form({ method: "GET", action: "/industry", class: "filter-box" }, - input({ type: "hidden", name: "filter", value: f || "ALL" }), - input({ type: "text", name: "search", value: search, placeholder: i18n.industrySearchPlaceholder || "Search facilities…", class: "filter-box__input" }), - div({ class: "filter-box__controls" }, - select({ name: "sector", class: "filter-box__number" }, - [option({ value: "", ...(sectorSel ? {} : { selected: true }) }, i18n.industryAllSectors || "All sectors")] - .concat(SECTORS.map((s) => option({ value: s, ...(s === sectorSel ? { selected: true } : {}) }, sectorLabel(s)))) - ), - button({ type: "submit", class: "filter-box__button" }, i18n.searchButton) - ) - ) - ), - br(), - div({ class: "industry-list" }, renderFacilityList(facilitiesOrForm, f, params.spreadMap)) - ) - ) - ) -} - -const membershipActions = (fc, returnTo) => { - const isSteward = fc.steward === userId - const isMember = safeArr(fc.members).includes(userId) - const status = String(fc.status || "ACTIVE").toUpperCase() - const policy = fc.membershipPolicy - const invited = safeArr(fc.invites).includes(userId) - const rows = [] - if (!isMember && status !== "DISSOLVED") { - if (policy === "vote") { - rows.push(form({ method: "POST", action: `/industry/apply/${encodeURIComponent(fc.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "subscribe-btn" }, i18n.industryApplyButton || "Request to join") - )) - } else if (policy === "invite") { - rows.push(invited - ? form({ method: "POST", action: `/industry/join/${encodeURIComponent(fc.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "subscribe-btn" }, i18n.industryJoinButton || "Join") - ) - : p({ class: "industry-hint" }, i18n.industryInviteNeeded || "You need an invitation to join.") - ) - } else { - rows.push(form({ method: "POST", action: `/industry/join/${encodeURIComponent(fc.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "subscribe-btn" }, i18n.industryJoinButton || "Join") - )) - } - } - if (isMember && !isSteward) { - rows.push(form({ method: "POST", action: `/industry/leave/${encodeURIComponent(fc.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "unsubscribe-btn" }, i18n.industryLeaveButton || "Leave") - )) - } - return rows -} - -const renderInviteSection = (fc, returnTo) => { - if (!safeArr(fc.members).includes(userId) || String(fc.status || "ACTIVE").toUpperCase() === "DISSOLVED") return null - if (fc.membershipPolicy !== "invite") return null - return div({ class: "industry-section" }, - h2(i18n.industryInviteButton || "Invite"), - form({ method: "POST", action: `/industry/invite/${encodeURIComponent(fc.id)}`, class: "industry-invite-form" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "text", name: "invitee", required: true, placeholder: i18n.industryInvitePlaceholder || "Oasis ID (@...ed25519)" }), - button({ type: "submit", class: "create-button" }, i18n.industryCreateInvite || "Generate invite") - ) - ) -} - -const renderPendingApplicants = (fc, returnTo) => { - const isMember = safeArr(fc.members).includes(userId) - const pending = safeArr(fc.pendingApplicants) - if (!isMember || !pending.length) return null - return div({ class: "industry-section" }, - h2(i18n.industryPendingApplicants || "Pending applicants"), - ul({ class: "industry-applicants" }, - pending.map((ap) => li( - userLink(ap.id), - span({ class: "industry-vote-count" }, ` (${i18n.industryVotesYes || "yes"}: ${ap.yes} / ${i18n.industryVotesNo || "no"}: ${ap.no})`), - ap.voters.includes(userId) - ? span({ class: "industry-hint" }, ` — ${i18n.industryAlreadyVoted || "voted"}`) - : form({ method: "POST", action: `/industry/govern/${encodeURIComponent(fc.id)}`, class: "industry-vote-form" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "subject", value: "admit" }), - input({ type: "hidden", name: "ref", value: ap.id }), - button({ type: "submit", name: "choice", value: "yes", class: "subscribe-btn" }, i18n.industryAdmit || "Admit"), - button({ type: "submit", name: "choice", value: "no", class: "unsubscribe-btn" }, i18n.industryReject || "Reject") - ) - )) - ) - ) -} - -const renderDissolveBlock = (fc, returnTo) => { - const isMember = safeArr(fc.members).includes(userId) - const status = String(fc.status || "ACTIVE").toUpperCase() - const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length - if (!isMember || status === "DISSOLVED" || memberCount <= 1) return null - return div({ class: "industry-section" }, - h2(i18n.industryDissolveTitle || "Dissolve facility"), - form({ method: "POST", action: `/industry/govern/${encodeURIComponent(fc.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "subject", value: "dissolve" }), - input({ type: "hidden", name: "ref", value: "" }), - button({ type: "submit", name: "choice", value: "yes", class: "unsubscribe-btn danger-btn" }, i18n.industryDissolveVote || "Vote to dissolve") - ) - ) -} - -const BUILD_STATUSES = ["PROPOSED", "APPROVED", "STOCKING", "IN_PRODUCTION", "COMPLETED", "FAILED"] -const BUILD_TRANSITIONS = { APPROVED: ["STOCKING", "FAILED"], STOCKING: ["IN_PRODUCTION", "FAILED"], IN_PRODUCTION: ["COMPLETED", "FAILED"], FAILED: ["APPROVED", "STOCKING", "IN_PRODUCTION"], COMPLETED: ["IN_PRODUCTION"] } -const CONTRIBUTABLE_VIEW = new Set(["APPROVED", "STOCKING", "IN_PRODUCTION"]) -const buildStatusLabel = (s) => i18n["industryBuildStatus_" + String(s || "").toUpperCase()] || s - -const renderBuildStatusChip = (status) => { - const s = String(status || "PROPOSED").toUpperCase() - const map = { PROPOSED: ["half", "📋"], REJECTED: ["tombstoned", "🚫"], APPROVED: ["whole", "✅"], STOCKING: ["half", "📦"], IN_PRODUCTION: ["half", "⚙"], COMPLETED: ["whole", "🏁"], FAILED: ["tombstoned", "✗"] } - const [v, ic] = map[s] || map.PROPOSED - return renderStateChip(v, ic, buildStatusLabel(s)) -} - -const fmtEco = (n) => `${Number(n || 0).toFixed(2)} ECO` - -const renderBlueprintLaborHours = (bp) => { - if (bp.laborHours == null) return null - return p({ class: "industry-labor-hours" }, span({ class: "industry-meta-label" }, `${i18n.industryLaborHours || "Labor (hours)"}: `), `${bp.laborHours || 0}h`) -} - -const renderBlueprintEstimate = (bp, totalLabel) => { - if (bp.estTotal == null) return null - return div({ class: "industry-estimate" }, - p(span({ class: "industry-meta-label" }, `${i18n.industryEstMaterials || "Total Materials"}: `), fmtEco(bp.estMaterialsCost)), - p(span({ class: "industry-meta-label" }, `${i18n.industryEstLabor || "Total Labor"}: `), fmtEco(bp.estLaborCost)), - br(), - p({ class: "industry-estimate-total" }, span({ class: "industry-meta-label" }, `${totalLabel || i18n.industryEstTotal || "Estimated price"}: `), fmtEco(bp.estTotal)) - ) -} - -const renderBlueprintGovernActions = (facilityId, bp, isMember, returnTo) => { - const isAuthor = bp.author === userId - const multi = (bp.facilityMembers || 1) > 1 - const ref = bp.root || bp.id - const voteBtn = (subject, voters, yes, labelKey, fallback) => { - const mine = safeArr(voters).includes(userId) - return form({ method: "POST", action: `/industry/govern/${encodeURIComponent(facilityId)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "subject", value: subject }), - input({ type: "hidden", name: "ref", value: ref }), - button({ type: "submit", name: "choice", value: mine ? "no" : "yes", class: "filter-btn" }, - `${mine ? (i18n.industryRevokeVote || "Revoke") : (i18n[labelKey] || fallback)} (${yes || 0}/${bp.voteNeed || 1})`) - ) - } - return [ - (isMember && isAuthor && bp.updateApproved !== false) - ? form({ method: "GET", action: `/industry/blueprints/${encodeURIComponent(bp.id)}/edit` }, button({ type: "submit", class: "update-btn" }, i18n.industryUpdateButton || "Update")) - : null, - (isMember && isAuthor && bp.deleteApproved !== false) - ? form({ method: "POST", action: `/industry/blueprints/${encodeURIComponent(bp.id)}/delete` }, button({ type: "submit", class: "delete-btn danger-btn" }, i18n.industryDeleteButton || "Delete")) - : null, - (isMember && multi) ? voteBtn("bpUpdate", bp.updateVoters, bp.updateYes, "industryVoteUpdateButton", "Vote update") : null, - (isMember && multi) ? voteBtn("bpDelete", bp.deleteVoters, bp.deleteYes, "industryVoteDeleteButton", "Vote delete") : null - ].filter(Boolean) -} - -const renderBlueprintsSection = (fc, blueprints, isMember, spreadMap = new Map()) => { - const list = safeArr(blueprints) - const cards = list.map((bp) => { - const actions = renderBlueprintGovernActions(fc.id, bp, isMember, `/industry/${encodeURIComponent(fc.id)}`) - return div({ class: "industry-card-wrap" }, - div({ class: "card-header activity-card-header" }, span(), renderContentActions(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`, { spread: spreadMap.get(bp.id) || null })), - div({ class: "industry-blueprint-card" }, - div({ class: "card-chips-row" }, - renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind), - renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase()) - ), - div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, safeText(bp.name))), - bp.image ? renderMediaBlob(bp.image, { class: "post-image" }) : null, - bp.description ? p({ class: "tribe-card-description" }, ...renderUrl(safeText(bp.description))) : (bp.outItem ? p(span({ class: "industry-meta-label" }, `${i18n.industryOutput || "Output"}: `), `${bp.outQty || 0} × ${safeText(bp.outItem)}`) : null), - renderBlueprintLaborHours(bp), - safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} × ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, - renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), - safeArr(bp.skills).length ? p(span({ class: "industry-meta-label" }, `${i18n.industrySkills || "Skills"}: `), bp.skills.join(", ")) : null, - actions.length ? div({ class: "industry-actions" }, ...actions) : null - ) - ) - }) - return div({ class: "industry-section" }, - h2(i18n.industryBlueprints || "Blueprints"), - list.length ? div({ class: "industry-blueprints" }, ...cards) : p({ class: "industry-hint" }, i18n.industryNoBlueprints || "No blueprints yet."), - isMember ? renderBlueprintForm(fc, {}, "create") : null - ) -} - -const renderBlueprintForm = (fc, bp, mode, spreadWarning = null) => { - const isEdit = mode === "edit" - const curKind = bp.outKind || "physical" - const materialsText = safeArr(bp.materials).map(m => `${m.item}:${m.qty}:${m.price != null ? m.price : 0}`).join("\n") - return div({ class: "industry-form industry-blueprint-form" }, - h2(isEdit ? (i18n.industryEditBlueprint || "Edit blueprint") : (i18n.industryNewBlueprint || "New blueprint")), - isEdit ? spreadWarning : null, - form({ method: "POST", action: isEdit ? `/industry/blueprints/${encodeURIComponent(bp.id)}/update` : `/industry/${encodeURIComponent(fc.id)}/blueprints`, enctype: "multipart/form-data" }, - label(i18n.industryName || "Name"), - br(), input({ type: "text", name: "name", required: true, maxlength: "80", value: bp.name || "" }), br(), - label(i18n.industryDescriptionLabel || "Description"), - br(), textarea({ name: "description", rows: "4", maxlength: "2000", placeholder: i18n.industryBlueprintDescPlaceholder || "Specs, dimensions, weight, docs…" }, bp.description || ""), br(), - label(i18n.uploadMedia || "Upload media (max-size: 50MB)"), - br(), input({ type: "file", name: "image" }), - bp.image ? div({ class: "industry-form-media" }, renderMediaBlob(bp.image, { class: "post-image" })) : null, - br(), - label(i18n.industryOutputKind || "Product type"), - br(), select({ name: "outKind" }, ["physical", "digital"].map(k => option({ value: k, ...(k === curKind ? { selected: true } : {}) }, i18n["industryKind_" + k] || k))), br(), - label(i18n.industryMaterials || "Materials"), - br(), textarea({ name: "materialsText", rows: "3", placeholder: i18n.industryMaterialsPlaceholder || "solar-panel:2:120\nbattery:1:80\nkit:1:15" }, materialsText), br(), - label(i18n.industryLaborHours || "Labor hours"), - br(), input({ type: "number", name: "laborHours", min: "0", step: "0.01", value: bp.laborHours != null ? bp.laborHours : "" }), br(), - button({ type: "submit" }, isEdit ? (i18n.industryUpdateButton || "Update") : (i18n.industryCreateBlueprintButton || "Create blueprint")) - ) - ) -} - -const renderBuildForm = (fc, blueprints, build, mode, spreadWarning = null) => { - const b = build || {} - const isEdit = mode === "edit" - const today = new Date().toISOString().slice(0, 10) - return div({ class: "industry-form industry-build-form" }, - h2(isEdit ? (i18n.industryEditBuild || "Edit build") : (i18n.industryNewBuild || "Propose a build")), - isEdit ? spreadWarning : null, - form({ method: "POST", action: isEdit ? `/industry/builds/${encodeURIComponent(b.id)}/update` : `/industry/${encodeURIComponent(fc.id)}/builds`, enctype: "multipart/form-data" }, - label(i18n.industryBuildTitle || "Title"), - br(), input({ type: "text", name: "title", required: true, maxlength: "120", value: safeText(b.title) }), br(), - label(i18n.industryBlueprint || "Blueprint"), - br(), isEdit - ? input({ type: "text", disabled: true, value: safeText(b.blueprintName) }) - : select({ name: "blueprintId", required: true }, safeArr(blueprints).map(bp => option({ value: bp.id }, safeText(bp.name)))), br(), - label(i18n.industryBuildStart || "Start date"), - br(), input({ type: "date", name: "startDate", required: true, value: b.startDate || "", ...(isEdit ? {} : { min: today }) }), br(), - label(i18n.industryBuildEnd || "End date"), - br(), input({ type: "date", name: "endDate", required: true, value: b.endDate || "", min: today }), br(), - label(i18n.industryBuildNotes || "Notes"), - br(), textarea({ name: "notes", rows: "3", maxlength: "1000" }, safeText(b.notes)), br(), - button({ type: "submit" }, isEdit ? (i18n.industryUpdateButton || "Update") : (i18n.industryProposeBuildButton || "Propose build")) - ) - ) -} - -const renderBuildsSection = (fc, builds, blueprints, isMember, spreadMap = new Map()) => { - const list = safeArr(builds) - return div({ class: "industry-section" }, - h2(i18n.industryBuilds || "Builds"), - list.length ? div({ class: "industry-blueprints" }, ...list.map((b) => renderBuildCard(b, { withActions: true, withBlueprintChips: false, withProposer: false, spread: spreadMap.get(b.id), actions: renderBuildGovernActions(b, isMember, `/industry/${encodeURIComponent(fc.id)}`) }))) : p({ class: "industry-hint" }, i18n.industryNoBuilds || "No builds yet."), - (isMember && fc.status === "ACTIVE" && !safeArr(blueprints).length) - ? p({ class: "industry-hint" }, i18n.industryNeedBlueprint || "Create a blueprint first: every build produces one.") - : null, - (isMember && fc.status === "ACTIVE" && safeArr(blueprints).length) ? renderBuildForm(fc, blueprints, {}, "create") : null - ) -} - -const renderFacilityJobsSection = (fc, jobs) => { - const list = safeArr(jobs) - if (!list.length) return null - const rows = list.map((j) => li( - a({ href: `/jobs/${encodeURIComponent(j.id || j.key)}` }, safeText(j.title) || (i18n.jobsTitle || "Job")), - j.salary ? span({ class: "industry-hint" }, ` · ${j.salary} ECO · `) : span({ class: "industry-hint" }, " · "), - renderStateChip(String(j.status || "OPEN").toUpperCase() === "OPEN" ? "whole" : "tombstoned", "", String(j.status || "OPEN").toUpperCase()) - )) - return div({ class: "industry-section" }, - h2(i18n.industryJobs || "Jobs"), - ul({ class: "industry-jobs-list" }, ...rows) - ) -} - -const renderFacilitySide = (fc, returnTo, params = {}) => { - const isSteward = fc.steward === userId - const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length - - const chips = [ - renderStateChip("whole", "🏭", sectorLabel(fc.sector)), - renderStateChip("half", "⚖", policyLabel(fc.membershipPolicy)), - renderLifespanChip(fc.lifetime, i18n), - renderEcoTax(fc.msgSize, fc.id || fc.key) - ].filter(Boolean) - - const sideActions = [] - for (const act of membershipActions(fc, returnTo)) sideActions.push(act) - if (!isSteward && fc.steward) { - sideActions.push(form({ method: "GET", action: "/pm" }, - input({ type: "hidden", name: "recipients", value: fc.steward }), - button({ type: "submit", class: "filter-btn" }, i18n.privateMessage) - )) - sideActions.push(a({ href: `/reports?filter=create&category=ABUSE&title=${encodeURIComponent(`${i18n.industryTitle || "Industry"}: ${safeText(fc.name)}`)}`, class: "filter-btn" }, i18n.industryReportButton || "Report")) - } - if (isSteward) { - sideActions.push(form({ method: "GET", action: `/industry/edit/${encodeURIComponent(fc.id)}` }, - button({ type: "submit", class: "tribe-action-btn" }, i18n.industryUpdateButton || "Update") - )) - if (memberCount <= 1) { - sideActions.push(form({ method: "POST", action: `/industry/delete/${encodeURIComponent(fc.id)}` }, - button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.industryDeleteButton || "Delete") - )) - } - } - - return div({ class: "tribe-side" }, - div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/industry/${encodeURIComponent(fc.id)}` }, safeText(fc.name) || (i18n.industryTitle || "Industry")))), - div({ class: "card-chips-row" }, ...chips), - fc.image ? renderMediaBlob(fc.image, { class: "tribe-detail-image" }) : null, - fc.description ? p({ class: "tribe-side-description" }, ...renderUrl(safeText(fc.description))) : null, - table({ class: "tribe-info-table" }, - tr(td({ class: "tribe-info-label" }, i18n.industryLaborRate || "Labor rate"), td({ class: "tribe-info-value", colspan: "3" }, `${fc.laborRate || 0} ECO/h`)), - tr(td({ class: "tribe-info-label" }, i18n.industryQuorum || "Quorum"), td({ class: "tribe-info-value", colspan: "3" }, String(fc.quorum))), - tr(td({ class: "tribe-info-label" }, i18n.industryMajority || "Majority"), td({ class: "tribe-info-value", colspan: "3" }, `${Math.round((fc.majority || 0.5) * 100)}%`)), - tr(td({ class: "tribe-info-value tribe-author-cell", colspan: "4" }, userLink(fc.steward))) - ), - fc.mapUrl ? renderMapEmbedWithZoom(params.mapData, fc.mapUrl, returnTo, params.zoom) : null, - safeArr(fc.tags).length - ? div({ class: "tribe-side-tags" }, safeArr(fc.tags).map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) - : null, - renderStatusBlock(fc, returnTo), - h2({ class: "tribe-members-count" }, `${i18n.industryMembers || "Members"}: ${memberCount}`), - renderInviteSection(fc, returnTo), - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, - renderDissolveBlock(fc, returnTo) - ) -} - -exports.singleFacilityView = async (facility, filter, params = {}) => { - const fc = facility || {} - const f = String(filter || "ALL").toUpperCase() - const returnTo = `/industry/${encodeURIComponent(fc.id)}?filter=${encodeURIComponent(f)}` - const isMember = safeArr(fc.members).includes(userId) - const facilitySide = renderFacilitySide(fc, returnTo, params) - - const facilityMain = div({ class: "tribe-main" }, - renderBlueprintsSection(fc, params.blueprints, isMember, params.childSpreadMap || new Map()), - renderBuildsSection(fc, params.builds, params.blueprints, isMember, params.childSpreadMap || new Map()), - renderFacilityJobsSection(fc, params.facilityJobs), - renderOpinionsVoting('/industry/opinions', fc.id || fc.key, fc.opinions, null, fc.opinions_inhabitants), - div({ class: "industry-section" }, - h2(i18n.industryMembers || "Members"), - ul({ class: "industry-members-list" }, safeArr(fc.members).map((mid) => li( - userLink(mid), - span({ class: "industry-role" }, ` · ${mid === fc.steward ? (i18n.industrySteward || "Steward") : (i18n.industryMember || "Member")}`) - ))) - ), - renderPendingApplicants(fc, returnTo) - ) - - return template( - i18n.industryTitle || "Industry", - section( - div({ class: "card-header activity-card-header" }, - renderContentActions(fc.id || fc.key, null, { spread: params.spreads || null }) - ), - div({ class: "tags-header" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), - div({ class: "filters" }, - form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, - FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: f === x.key ? "filter-btn active" : "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) - .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) - ) - ), - div({ class: "tribe-details" }, facilitySide, facilityMain) - ) - ) -} - -exports.blueprintEditView = async (bp, fc) => template( - i18n.industryTitle || "Industry", - section( - div({ class: "card-header activity-card-header" }, - renderContentActions(fc.id || fc.key, `/industry/${encodeURIComponent(fc.id)}`) - ), - div({ class: "tags-header" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), - div({ class: "filters" }, - form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, - FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) - .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) - ) - ), - renderBlueprintForm(fc, bp, "edit", await renderSpreadEditWarning(bp && bp.id)) - ) -) - -exports.buildEditView = async (b, fc) => template( - i18n.industryTitle || "Industry", - section( - div({ class: "card-header activity-card-header" }, - renderContentActions(b.id, `/industry/build/${encodeURIComponent(b.id)}`) - ), - div({ class: "tags-header" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), - div({ class: "filters" }, - form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, - FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) - .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) - ) - ), - renderBuildForm(fc, [], b, "edit", await renderSpreadEditWarning(b && b.id)) - ) -) - -exports.singleBlueprintView = async (blueprint, params = {}) => { - const bp = blueprint || {} - const isMember = safeArr(bp.members).includes(userId) - const facilityHref = `/industry/${encodeURIComponent(bp.facilityId || "")}` - const returnTo = `/industry/blueprint/${encodeURIComponent(bp.id)}` - const actions = renderBlueprintGovernActions(bp.facilityId, bp, isMember, returnTo) - - return template( - i18n.industryTitle || "Industry", - section(div({ class: "tags-header" }, h2(i18n.industryTitle), p(i18n.industryDescription))), - section( - div({ class: "filters" }, - form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, - FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) - .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) - ) - ) - ), - section( - div({ class: "shop-detail" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(bp.id, null, { spread: params.spreads || null, author: bp.author, reportTitle: bp.name || bp.title }) - ), - div({ class: "card-chips-row" }, - renderStateChip("half", bp.outKind === "digital" ? "💾" : "📦", i18n["industryKind_" + bp.outKind] || bp.outKind), - renderStateChip("half", "⚖", String(bp.license || "copyleft").toUpperCase()) - ), - p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: facilityHref }, safeText(bp.facilityName))), - h2(safeText(bp.name)), - bp.image ? div({ class: "shop-detail-media" }, renderMediaBlob(bp.image, { class: "post-image" })) : null, - bp.description ? p({ class: "tribe-card-description" }, ...renderUrl(safeText(bp.description))) : null, - div({ class: "industry-meta" }, - p(span({ class: "industry-meta-label" }, `${i18n.industryAuthor || "Author"}: `), userLink(bp.author)) - ), - renderBlueprintLaborHours(bp), - safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} × ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, - renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), - safeArr(bp.skills).length ? p(span({ class: "industry-meta-label" }, `${i18n.industrySkills || "Skills"}: `), bp.skills.join(", ")) : null, - actions.length ? div({ class: "industry-actions" }, ...actions) : null, - safeArr(bp.builds).length - ? div({ class: "industry-section" }, - h2(i18n.industryBuilds || "Builds"), - ul({ class: "industry-builds-list" }, bp.builds.map(b => li( - a({ href: `/industry/build/${encodeURIComponent(b.id)}` }, safeText(b.title)), - " ", renderBuildStatusChip(b.status) - ))) - ) - : null - ) - ) - ) -} - -exports.singleBuildView = async (build, params = {}) => { - const b = build || {} - const isMember = safeArr(b.members).includes(userId) - const isSteward = b.steward === userId - const isProposer = b.proposer === userId - const status = String(b.status || "PROPOSED").toUpperCase() - const returnTo = `/industry/build/${encodeURIComponent(b.id)}` - const facilityHref = `/industry/${encodeURIComponent(b.facilityId)}` - - const contribRows = safeArr(b.contributions).map(c => tr( - td(userLink(c.author)), - td(i18n["industryKind_" + c.kind] || c.kind), - td(c.kind === "labor" ? `${c.hours} h` : c.kind === "material" ? `${c.value} ECO · ${safeText(c.item)}` : `${c.eco} ECO`), - td(safeText(c.note)) - )) - - const canUpdateBuild = isMember && (isProposer || isSteward) && !b.distributed && b.updateApproved !== false - const govRows = [] - if ((status === "PROPOSED" || status === "REJECTED") && isMember) { - govRows.push(form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/vote`, class: "industry-build-vote" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", name: "choice", value: "yes", class: "subscribe-btn" }, i18n.industryApproveBuild || "Approve"), - button({ type: "submit", name: "choice", value: "no", class: "unsubscribe-btn" }, i18n.industryReject || "Reject") - )) - } - const isReopen = status === "COMPLETED" || status === "FAILED" - const lockedByDistribution = status === "COMPLETED" && b.distributed - const transitions = ((!isMember || lockedByDistribution) ? [] : (BUILD_TRANSITIONS[status] || [])).filter(nextS => { - const stewardOnly = nextS === "COMPLETED" || nextS === "FAILED" || isReopen - return isSteward || (isProposer && !stewardOnly) - }) - if (transitions.length) { - govRows.push(form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/status`, class: "project-control-form project-control-form--status" }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - select({ name: "status", class: "project-control-select" }, - transitions.map(nextS => option({ value: nextS }, buildStatusLabel(nextS))) - ), - button({ type: "submit", class: "status-btn project-control-btn" }, i18n.industryAdvanceTo || "Set to") - )) - } - if (canUpdateBuild) { - govRows.push(form({ method: "GET", action: `/industry/builds/${encodeURIComponent(b.id)}/edit` }, - button({ type: "submit", class: "update-btn" }, i18n.industryUpdateButton || "Update") - )) - } - if (isMember && isProposer && b.deleteApproved !== false) { - govRows.push(form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/delete` }, - button({ type: "submit", class: "delete-btn danger-btn" }, i18n.industryDeleteButton || "Delete") - )) - } - if (isMember && (b.facilityMembers || 1) > 1) { - govRows.push(renderGovernVoteButton(b.facilityId, "buildUpdate", b.id, b.updateVoters, b.updateYes, b.voteNeed, "industryVoteUpdateButton", "Vote update", returnTo)) - govRows.push(renderGovernVoteButton(b.facilityId, "buildDelete", b.id, b.deleteVoters, b.deleteYes, b.voteNeed, "industryVoteDeleteButton", "Vote delete", returnTo)) - } - - const kind = ["labor", "material", "eco"].includes(String(params.kind)) ? String(params.kind) : "labor" - - const contributeAction = `/industry/builds/${encodeURIComponent(b.id)}/contribute` - const contributeBlock = (CONTRIBUTABLE_VIEW.has(status) && isMember) - ? div({ class: "industry-section", id: "contribute" }, - h2(i18n.industryContribute || "Contribute"), - form({ method: "GET", action: `/industry/build/${encodeURIComponent(b.id)}#contribute`, class: "industry-form industry-contrib-kind" }, - label(i18n.industryKindLabel || "Kind"), - select({ name: "kind", class: "project-control-select" }, - option({ value: "labor", ...(kind === "labor" ? { selected: true } : {}) }, i18n.industryKind_labor || "Labor"), - option({ value: "material", ...(kind === "material" ? { selected: true } : {}) }, i18n.industryKind_material || "Material"), - option({ value: "eco", ...(kind === "eco" ? { selected: true } : {}) }, i18n.industryKind_eco || "Funds") - ), - button({ type: "submit", class: "create-button" }, i18n.apply || "Apply") - ), - form({ method: "POST", action: contributeAction, class: "industry-form industry-contrib-form-single" }, - input({ type: "hidden", name: "kind", value: kind }), - kind === "labor" ? [ - label(i18n.industryHours || "Hours"), - input({ type: "number", name: "hours", min: "0.01", step: "0.01", required: true, class: "industry-input-num" }) - ] : null, - kind === "material" ? [ - label(i18n.industryMaterialItem || "Material"), - textarea({ name: "item", rows: "2", maxlength: "80", required: true }), - label(i18n.industryMaterialValue || "Value (ECO)"), - input({ type: "number", name: "value", min: "0.01", step: "0.01", required: true, class: "industry-input-num" }) - ] : null, - kind === "eco" ? [ - label(i18n.industryEcoAmount || "Amount (ECO)"), - input({ type: "number", name: "eco", min: "0.000001", step: "0.000001", required: true, class: "industry-input-num" }) - ] : null, - label(i18n.industryNote || "Note"), - textarea({ name: "note", rows: "2", maxlength: "140" }), - button({ type: "submit", class: "filter-btn" }, i18n.industryContributeButton || "Contribute") - ) - ) - : null - - const distributeBlock = (status === "COMPLETED" && isSteward && !b.distributed) - ? div({ class: "industry-section" }, - h2(i18n.industryDistribute || "Distribute"), - form({ method: "POST", action: `/industry/builds/${encodeURIComponent(b.id)}/distribute` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - label(i18n.industryOutputValue || "Output value (ECO, e.g. from sale)"), - br(), input({ type: "number", name: "outputValue", min: "0", step: "0.000001", value: "0" }), br(), - button({ type: "submit", class: "subscribe-btn" }, i18n.industryDistributeButton || "Distribute by shares") - ), - p({ class: "industry-hint" }, `${i18n.industryTreasury || "Treasury"}: ${b.treasury || 0} ECO`) - ) - : null - - const allocationBlock = b.distributed && b.allocation - ? div({ class: "industry-section" }, - h2(i18n.industryDistribution || "Distribution"), - p(`${i18n.industryPot || "Pot"}: ${(b.allocation.pot || 0).toFixed(2)} ECO`) - ) - : null - - const buildLabel = `${i18n.industryBuild || "Build"}: ${safeText(b.title)}` - const facilityName = safeText(b.facilityName) - const productName = safeText(b.blueprintName) || safeText(b.title) - const crossTags = [facilityName, productName].filter(Boolean).map(x => x.toLowerCase().replace(/\s+/g, "-")).join(", ") - const crossDates = [ - b.startDate ? `${i18n.industryBuildStart || "Start date"}: ${moment(b.startDate).format("YYYY/MM/DD")}` : null, - b.endDate ? `${i18n.industryBuildEnd || "End date"}: ${moment(b.endDate).format("YYYY/MM/DD")}` : null - ].filter(Boolean).join(" · ") - const crossContext = [ - `${i18n.industryFacility || "Facility"}: ${facilityName}`, - b.blueprintName ? `${i18n.industryBlueprint || "Blueprint"}: ${safeText(b.blueprintName)}` : null, - crossDates || null, - b.estTotal != null ? `${i18n.industryFinalPrice || "Final price"}: ${fmtEco(b.estTotal)}` : null - ].filter(Boolean).join("\n") - const crossDescription = [safeText(b.notes), crossContext].filter(Boolean).join("\n\n") - const crossLinks = isMember - ? div({ class: "industry-actions" }, - a({ href: `/tasks?filter=create&returnTo=${encodeURIComponent(returnTo)}&title=${encodeURIComponent(buildLabel)}&description=${encodeURIComponent(crossDescription)}`, class: "filter-btn" }, i18n.industryCreateTask || "Create Task"), - a({ href: `/jobs?filter=CREATE&industry=${encodeURIComponent(b.facilityId || "")}&title=${encodeURIComponent(buildLabel)}&description=${encodeURIComponent(crossDescription)}&tasks=${encodeURIComponent(safeText(b.notes))}&salary=${encodeURIComponent(b.estLaborCost != null ? b.estLaborCost : "")}`, class: "filter-btn" }, i18n.industryPostJob || "Create Job"), - a({ href: `/projects?filter=CREATE&title=${encodeURIComponent(buildLabel)}&description=${encodeURIComponent(crossDescription)}&goal=${encodeURIComponent(b.estTotal != null ? b.estTotal : "")}`, class: "filter-btn" }, i18n.industrySendToProjects || "Create Project"), - a({ href: `/market?filter=create&industry=${encodeURIComponent(b.facilityId || "")}&title=${encodeURIComponent(productName)}&description=${encodeURIComponent(crossDescription)}&price=${encodeURIComponent(b.estTotal != null ? b.estTotal : "")}&tags=${encodeURIComponent(crossTags)}`, class: "filter-btn" }, i18n.industrySellOnMarket || "Send to Market"), - (!isSteward && b.steward) ? a({ href: `/courts?filter=open&method=MEDIATION&respondentId=${encodeURIComponent(b.steward)}&titleSuffix=${encodeURIComponent(buildLabel)}`, class: "filter-btn" }, i18n.industryOpenDispute || "Open dispute") : null - ) - : null - - return template( - i18n.industryTitle || "Industry", - section(div({ class: "tags-header" }, h2(i18n.industryTitle), p(i18n.industryDescription))), - section( - div({ class: "filters" }, - form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, - FILTERS.map((x) => button({ type: "submit", name: "filter", value: x.key, class: "filter-btn" }, String(i18n[x.i18n] || x.key).toUpperCase())) - .concat(button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.industryCreateFacility || "Create facility")) - ) - ) - ), - section( - div({ class: "shop-detail" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(b.id, null, { spread: params.spreads || null, author: b.proposer, reportTitle: b.title }) - ), - b.image ? div({ class: "shop-detail-media" }, renderMediaBlob(b.image, { class: "post-image" })) : null, - h2(safeText(b.title) || (i18n.industryBuild || "Build")), - div({ class: "card-chips-row" }, renderBuildStatusChip(status)), - div({ class: "industry-meta" }, - b.blueprintName ? p(span({ class: "industry-meta-label" }, `${i18n.industryBlueprint || "Blueprint"}: `), safeText(b.blueprintName)) : null, - b.blueprintImage ? renderMediaBlob(b.blueprintImage, { class: "post-image" }) : null, - p(span({ class: "industry-meta-label" }, `${i18n.industryProposer || "Proposer"}: `), userLink(b.proposer)), - b.notes ? p({ class: "industry-build-notes" }, span({ class: "industry-meta-label" }, `${i18n.industryBuildNotes || "Notes"}: `), safeText(b.notes)) : null, - div({ class: "industry-dates-row" }, - b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY/MM/DD")) : null, - b.endDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildEnd || "End date"}: `), moment(b.endDate).format("YYYY/MM/DD")) : null, - (b.endDate && status !== "COMPLETED" && status !== "FAILED") - ? p(span({ class: "industry-meta-label" }, `${i18n.industryTimeLeft || "Time left"}: `), `${Math.max(0, Math.ceil((new Date(b.endDate) - Date.now()) / 86400000))}d`) - : null - ) - ), - renderBlueprintEstimate(b, i18n.industryFinalPrice || "Final price"), - govRows.length ? div({ class: "industry-actions" }, ...govRows) : null, - crossLinks, - safeArr(b.contributions).length - ? div({ class: "industry-section" }, - h2(i18n.industryContributions || "Contributions"), - table({ class: "industry-table" }, - thead(tr(th(i18n.industryMember || "Member"), th(i18n.industryKindLabel || "Kind"), th(i18n.industryAmount || "Amount"), th(i18n.industryNote || "Note"))), - tbody(...contribRows) - ) - ) - : null, - contributeBlock, - distributeBlock, - allocationBlock - ) - ) - ) -} diff --git a/src/views/inhabitants_view.js b/src/views/inhabitants_view.js index fa7b2c9..58f0ba0 100644 --- a/src/views/inhabitants_view.js +++ b/src/views/inhabitants_view.js @@ -1,6 +1,5 @@ -const { div, h2, p, section, button, form, img, a, textarea, input, span, strong } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderUserSensors, renderContentActions, renderRelationshipBlock } = require('./main_views'); -const { renderZoomableImage } = require('./gallery_view'); +const { div, h2, p, section, button, form, img, a, textarea, input, br, span, strong } = require("../server/node_modules/hyperaxe"); +const { template, i18n, userLink, renderUserSensors, renderContentActions } = require('./main_views'); const { renderContentStats } = require('./clearnet_view'); const { renderUrl } = require('../backend/renderUrl'); const { getConfig } = require('../configs/config-manager'); @@ -49,40 +48,20 @@ function resolvePhoto(photoField, size = 256) { if (photoField.startsWith('/blob/')) return photoField; if (photoField.startsWith('/image/')) { if (isDefaultImageId(photoField)) return '/assets/images/default-avatar.png'; - return photoField.replace('/image/256/','/image/'+size+'/').replace('/image/512/','/image/'+size+'/') + '?fallback=avatar'; + return photoField.replace('/image/256/','/image/'+size+'/').replace('/image/512/','/image/'+size+'/'); } } - return toImageUrl(photoField, size) + '?fallback=avatar'; + return toImageUrl(photoField, size); } -const filterLabel = (mode) => - String(i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || mode).toUpperCase(); - -const generateFilterButtons = (filters, currentFilter, labelOf = filterLabel) => +const generateFilterButtons = (filters, currentFilter) => filters.map(mode => form({ method: 'GET', action: '/inhabitants' }, input({ type: 'hidden', name: 'filter', value: mode }), button({ type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' - }, labelOf(mode)) - ) - ); - -const MAIN_FILTERS = ['all', 'TOP', 'contacts', 'blocked', 'CVs', 'SUGGESTED', 'GALLERY']; - -const renderMainFilters = (filter, isTop) => - div({ class: 'inhabitant-action' }, - ...MAIN_FILTERS.map(mode => - form({ method: 'GET', action: '/inhabitants' }, - input({ type: 'hidden', name: 'filter', value: mode === 'TOP' ? 'TOP ACTIVITY' : mode }), - button({ - type: 'submit', - class: (mode === 'TOP' ? isTop : filter === mode) ? 'filter-btn active' : 'filter-btn' - }, mode === 'TOP' - ? String(i18n.topSectionTitle || 'TOP').toUpperCase() - : filterLabel(mode)) - ) + }, i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || mode) ) ); @@ -110,27 +89,6 @@ function lastActivityBadge(user, isMe) { const lightboxId = (id) => 'inhabitant_' + String(id || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_'); -const cvField = (labelText, value) => value - ? div({ class: 'card-field' }, - span({ class: 'card-label' }, `${labelText}: `), - span({ class: 'card-value' }, value) - ) - : null; - -const renderCvFields = (user) => { - const languages = Array.isArray(user.languages) ? user.languages.filter(Boolean) : []; - const skills = Array.isArray(user.skills) ? user.skills.filter(Boolean) : []; - const fields = [ - cvField(i18n.locationLabel, user.location), - cvField(i18n.languagesLabel, languages.length ? languages.join(', ').toUpperCase() : ''), - cvField(i18n.skillsLabel, skills.length ? skills.join(', ') : ''), - cvField(i18n.statusLabel || 'Status', user.status), - cvField(i18n.preferencesLabel || 'Preferences', user.preferences), - cvField(i18n.createdAtLabel || 'Created at', user.createdAt ? new Date(user.createdAt).toLocaleString() : '') - ].filter(Boolean); - return fields.length ? div({ class: 'cv-card-fields' }, ...fields) : null; -}; - const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) => { const isMe = user.id === currentUserId; const raw = user.visibilityPrefs || {}; @@ -144,7 +102,7 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) wallet: raw.wallet === true, ecoTax: raw.ecoTax !== false, larpSign: raw.larpSign === true, - gpg: raw.gpg === true, + gpg: raw.gpg !== false, clearnet: hasClearnet, fediverse: raw.fediverse === true, fediverseHandle: typeof raw.fediverseHandle === 'string' ? raw.fediverseHandle : '' @@ -153,7 +111,7 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) div( { class: 'card-header activity-card-header' }, span(), - renderContentActions(user.id, `/inhabitant/${encodeURIComponent(user.id)}`, { author: user.id }) + renderContentActions(user.id, `/inhabitant/${encodeURIComponent(user.id)}`) ), div({ class: 'card-section inhabitants-card-body' }, div({ class: 'inhabitant-card' }, @@ -170,30 +128,26 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) estimatedUBI: user.estimatedUBI, lastClaimedDate: user.lastClaimedDate, totalClaimed: user.totalClaimed, larpHouse: user.larpHouse, stats: user.stats }, { relationshipNode: isMe ? span({ class: 'status you' }, i18n.relationshipYou) : null, excludeContent: true }), - filter === 'CVs' - ? div( - { class: 'cv-actions doc-export-actions' }, - form( - { method: 'GET', action: `/cv/pdf/${encodeURIComponent(user.id)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.generatePdf) - ), - form( - { method: 'POST', action: `/cv/share/${encodeURIComponent(user.id)}` }, - button({ type: 'submit', class: 'filter-btn' }, i18n.sharePm) - ) - ) - : null, !isMe ? div( - { class: 'inhabitant-user-actions' }, - div({ class: 'inhabitant-relationship' }, renderRelationshipBlock(user.relationship || {}, user.id)) + { class: 'cv-actions' }, + form( + { method: 'GET', action: '/pm' }, + input({ type: 'hidden', name: 'recipients', value: user.id }), + button({ type: 'submit', class: 'btn' }, i18n.pmCreateButton) + ) ) : null ), div({ class: 'inhabitant-details' }, h2(user.name || 'Anonymous'), user.description ? p(...renderUrl(user.description)) : null, - filter === 'CVs' ? renderCvFields(user) : null, + filter === 'MATCHSKILLS' && user.commonSkills?.length + ? div({ class: 'matchskills' }, + p(`${i18n.commonSkills}: ${user.commonSkills.join(', ')}`), + p(`${i18n.matchScore}: ${Math.round((user.matchScore || 0) * 100)}%`) + ) + : null, filter === 'SUGGESTED' && (user.followsYou || user.commonSkills?.length || user.mutualCount) ? div({ class: 'suggested-meta' }, user.followsYou ? span({ class: 'suggested-badge' }, i18n.suggestedFollowsYou || 'Follows you') : null, @@ -206,6 +160,31 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured) filter === 'blocked' && user.isBlocked ? p(i18n.blockedLabel) : null, p(userLink(user.id)), + !isMe ? (() => { + const rel = user.relationship || {} + const blockedBoth = rel.blocking && rel.blockedBy + const mutual = rel.following && rel.followsMe + const supportAction = rel.following ? 'unfollow' : (rel.blocking ? 'unblock' : 'follow') + return div({ class: 'relationship-status inhabitant-relationship' }, + blockedBoth + ? span({ class: 'status blocked' }, i18n.relationshipMutualBlock) + : [ + rel.blocking ? span({ class: 'status blocked' }, i18n.relationshipBlocking) : null, + rel.blockedBy ? span({ class: 'status blocked-by' }, i18n.relationshipBlockedBy) : null, + mutual + ? span({ class: 'status mutual' }, i18n.relationshipMutuals) + : [ + span({ class: 'status supporting' }, rel.following ? i18n.relationshipFollowing : i18n.relationshipNone), + span({ class: 'status supported-by' }, rel.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing) + ] + ], + div({ class: 'relationship-actions' }, + form({ method: 'POST', action: `/${supportAction}/${encodeURIComponent(user.id)}` }, + button({ type: 'submit', class: 'btn' }, i18n[supportAction]) + ) + ) + ) + })() : null, renderContentStats(user.stats, i18n) ) ) @@ -250,11 +229,19 @@ function msgIdOf(m) { } exports.inhabitantsView = (inhabitants, filter, query, currentUserId, fediverseConfigured) => { - const title = i18n.allInhabitants; + const title = filter === 'contacts' ? i18n.yourContacts + : filter === 'CVs' ? i18n.allCVs + : filter === 'MATCHSKILLS' ? i18n.matchSkills + : filter === 'SUGGESTED' ? i18n.suggestedSectionTitle + : filter === 'blocked' ? i18n.blockedSectionTitle + : filter === 'GALLERY' ? i18n.gallerySectionTitle + : filter === 'TOP KARMA' ? i18n.topkarmaSectionTitle + : filter === 'TOP ECO' ? (i18n.topecoSectionTitle || 'Top Eco') + : filter === 'TOP ACTIVITY' ? i18n.topactivitySectionTitle + : i18n.allInhabitants; - const showCVFilters = filter === 'CVs'; - const TOP_FILTERS = ['TOP ACTIVITY', 'TOP INACTIVITY', 'TOP KARMA', 'TOP ECO']; - const isTop = TOP_FILTERS.includes(filter); + const showCVFilters = filter === 'CVs' || filter === 'MATCHSKILLS'; + const filters = ['all', 'TOP ACTIVITY', 'TOP KARMA', 'TOP ECO', 'contacts', 'SUGGESTED', 'blocked', 'CVs', 'MATCHSKILLS', 'GALLERY']; return template( title, @@ -264,33 +251,28 @@ exports.inhabitantsView = (inhabitants, filter, query, currentUserId, fediverseC p(i18n.discoverPeople) ), div({ class: 'filters' }, - form({ method: 'GET', action: '/inhabitants', class: 'filter-box' }, + form({ method: 'GET', action: '/inhabitants' }, input({ type: 'hidden', name: 'filter', value: filter }), input({ type: 'text', name: 'search', placeholder: i18n.searchInhabitantsPlaceholder, - value: (query && query.search) || '', - class: 'filter-box__input' + value: (query && query.search) || '' }), - div({ class: 'filter-box__controls' }, - showCVFilters - ? [ - input({ type: 'text', name: 'location', placeholder: i18n.filterLocation, value: (query && query.location) || '', class: 'filter-box__number' }), - input({ type: 'text', name: 'language', placeholder: i18n.filterLanguage, value: (query && query.language) || '', class: 'filter-box__number' }), - input({ type: 'text', name: 'skills', placeholder: i18n.filterSkills, value: (query && query.skills) || '', class: 'filter-box__number' }) - ] - : null, - button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton) - ) + showCVFilters + ? [ + input({ type: 'text', name: 'location', placeholder: i18n.filterLocation, value: (query && query.location) || '' }), + input({ type: 'text', name: 'language', placeholder: i18n.filterLanguage, value: (query && query.language) || '' }), + input({ type: 'text', name: 'skills', placeholder: i18n.filterSkills, value: (query && query.skills) || '' }) + ] + : null, + br(), + button({ type: 'submit' }, i18n.applyFilters) ) ), - renderMainFilters(filter, isTop), - isTop - ? div({ class: 'inhabitant-action inhabitant-subfilters' }, - ...generateFilterButtons(TOP_FILTERS, filter, (mode) => filterLabel(mode).replace(/^TOP\s+/, '')) - ) - : null, + div({ class: 'inhabitant-action' }, + ...generateFilterButtons(filters, filter) + ), filter === 'GALLERY' ? renderGalleryInhabitants(inhabitants) : div({ class: 'inhabitants-list' }, @@ -356,7 +338,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = wallet: rawPrefs.wallet === true, ecoTax: rawPrefs.ecoTax !== false, larpSign: rawPrefs.larpSign === true, - gpg: rawPrefs.gpg === true, + gpg: rawPrefs.gpg !== false, clearnet: rawPrefs.clearnet === true || clearnetSubKeys.some(k => rawPrefs[k] === true), fediverse: rawPrefs.fediverse === true, fediverseHandle: typeof rawPrefs.fediverseHandle === 'string' ? rawPrefs.fediverseHandle : '' @@ -368,18 +350,14 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = const providedBucket = typeof safe.lastActivityBucket === 'string' ? safe.lastActivityBucket : null; const dotClass = providedBucket === 'green' ? 'green' : providedBucket === 'orange' ? 'orange' : 'red'; - const fieldNodes = [ - cvField(i18n.locationLabel, location), - cvField(i18n.languagesLabel, languages.length ? languages.join(', ').toUpperCase() : ''), - cvField(i18n.skillsLabel, skills.length ? skills.join(', ') : ''), - cvField(i18n.statusLabel || 'Status', status), - cvField(i18n.preferencesLabel || 'Preferences', preferences), - cvField(i18n.createdAtLabel || 'Created at', createdAt) - ].filter(Boolean); - const detailNodes = [ description ? p(...renderUrl(description)) : null, - fieldNodes.length ? div({ class: 'cv-card-fields' }, ...fieldNodes) : null + location ? p(`${i18n.locationLabel}: ${location}`) : null, + languages.length ? p(`${i18n.languagesLabel}: ${languages.join(', ').toUpperCase()}`) : null, + skills.length ? p(`${i18n.skillsLabel}: ${skills.join(', ')}`) : null, + status ? p(`${i18n.statusLabel || 'Status'}: ${status}`) : null, + preferences ? p(`${i18n.preferencesLabel || 'Preferences'}: ${preferences}`) : null, + createdAt ? p(`${i18n.createdAtLabel || 'Created at'}: ${createdAt}`) : null ].filter(Boolean); return template( @@ -389,7 +367,9 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = h2(title), p(i18n.discoverPeople) ), - renderMainFilters('all', false), + div({ class: 'mode-buttons' }, + ...generateFilterButtons(['all', 'TOP ACTIVITY', 'TOP KARMA', 'TOP ECO', 'contacts', 'SUGGESTED', 'blocked', 'CVs', 'MATCHSKILLS', 'GALLERY'], 'all') + ), div({ class: 'inhabitant-card' }, div({ class: 'inhabitant-left' }, img({ class: 'inhabitant-photo-details', src: image, alt: name || 'Anonymous' }), @@ -402,16 +382,11 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = larpHouse, stats: safe.stats }, { excludeContent: true }), (!isMe && (id || viewedId)) - ? div({ class: 'cv-actions doc-export-actions' }, - form( - { method: 'GET', action: '/pm' }, - input({ type: 'hidden', name: 'recipients', value: id || viewedId }), - button({ type: 'submit', class: 'filter-btn' }, i18n.pmCreateButton) - ) + ? form( + { method: 'GET', action: '/pm' }, + input({ type: 'hidden', name: 'recipients', value: id || viewedId }), + button({ type: 'submit', class: 'btn' }, i18n.pmCreateButton) ) - : null, - (!isMe && (id || viewedId) && safe.relationship) - ? div({ class: 'inhabitant-relationship' }, renderRelationshipBlock(safe.relationship, id || viewedId)) : null ), (() => { @@ -430,7 +405,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = return div({ class: 'post' }, visitBtn, parts.clean && parts.clean.trim() ? p(...renderUrl(parts.clean)) : null, - ...(parts.imgs || []).map(src => renderZoomableImage(src, { imgClass: 'post-image', alt: 'image' })) + ...(parts.imgs || []).map(src => img({ src, class: 'post-image', alt: 'image' })) ); }) ) ] @@ -445,4 +420,3 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) = }; exports.lastActivityBadge = lastActivityBadge; -exports.resolvePhoto = resolvePhoto; diff --git a/src/views/invites_view.js b/src/views/invites_view.js index 15a2dcf..f58703a 100644 --- a/src/views/invites_view.js +++ b/src/views/invites_view.js @@ -107,7 +107,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'pubs-section' }, h2(i18n.invitesPubsTitle), - p(i18n.invitesPubsHint), form( { action: '/settings/invite/accept', method: 'post' }, input({ name: 'invite', type: 'text', placeholder: i18n.invitesPubInviteCodePlaceholder, autofocus: true, required: true }), @@ -130,7 +129,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'federations-section' }, h2(i18n.invitesFederationsTitle || 'Federations'), - p(i18n.invitesFederationsHint), div({ class: 'conn-actions invites-pubs-actions' }, form({ action: '/invites/refresh-pubs', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsRefresh || 'Refresh')), form({ action: '/invites/clear-unreachable', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsClearUnreachable || 'Remove unreachable')), @@ -182,7 +180,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'invites-houses', id: 'invites-houses' }, h2(i18n.invitesHousesTitle || 'Houses'), - p(i18n.invitesHousesHint), form( { action: '/larp/invite/redeem', method: 'post' }, input({ type: 'hidden', name: 'returnTo', value: '/invites' }), @@ -195,7 +192,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'invites-tribes', id: 'invites-tribes' }, h2(i18n.invitesTribesTitle), - p(i18n.invitesTribesHint), form( { action: '/tribes/join-code', method: 'post' }, input({ name: 'inviteCode', type: 'text', placeholder: i18n.invitesTribeInviteCodePlaceholder, required: true }), @@ -207,7 +203,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'invites-chats', id: 'invites-chats' }, h2(i18n.invitesChatsTitle || 'Chats'), - p(i18n.invitesChatsHint), form( { action: '/chats/join-code', method: 'post' }, input({ name: 'code', type: 'text', placeholder: i18n.invitesChatInviteCodePlaceholder || 'Enter chat invite code', required: true }), @@ -219,7 +214,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'invites-pads', id: 'invites-pads' }, h2(i18n.invitesPadsTitle || 'Pads'), - p(i18n.invitesPadsHint), form( { action: '/pads/join-code', method: 'post' }, input({ name: 'code', type: 'text', placeholder: i18n.invitesPadInviteCodePlaceholder || 'Enter pad invite code', required: true }), @@ -231,7 +225,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'invites-calendars', id: 'invites-calendars' }, h2(i18n.invitesCalendarsTitle || 'Calendars'), - p(i18n.invitesCalendarsHint), form( { action: '/calendars/join-code', method: 'post' }, input({ name: 'code', type: 'text', placeholder: i18n.invitesCalendarInviteCodePlaceholder || 'Enter calendar invite code', required: true }), @@ -243,7 +236,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'invites-events', id: 'invites-events' }, h2(i18n.invitesEventsTitle || 'Events'), - p(i18n.invitesEventsHint), form( { action: '/events/join-code', method: 'post' }, input({ name: 'code', type: 'text', placeholder: i18n.invitesEventInviteCodePlaceholder || 'Enter event invite code', required: true }), @@ -255,7 +247,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'invites-forums', id: 'invites-forums' }, h2(i18n.invitesForumsTitle || 'Forums'), - p(i18n.invitesForumsHint), form( { action: '/forum/join-code', method: 'post' }, input({ name: 'code', type: 'text', placeholder: i18n.invitesForumInviteCodePlaceholder || 'Enter forum invite code', required: true }), @@ -267,7 +258,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'invites-maps', id: 'invites-maps' }, h2(i18n.invitesMapsTitle || 'Maps'), - p(i18n.invitesMapsHint), form( { action: '/maps/join-code', method: 'post' }, input({ name: 'code', type: 'text', placeholder: i18n.invitesMapInviteCodePlaceholder || 'Enter map invite code', required: true }), @@ -279,7 +269,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'invites-shops', id: 'invites-shops' }, h2(i18n.invitesShopsTitle || 'Shops'), - p(i18n.invitesShopsHint), form( { action: '/shops/join-code', method: 'post' }, input({ name: 'code', type: 'text', placeholder: i18n.invitesShopInviteCodePlaceholder || 'Enter shop invite code', required: true }), @@ -291,7 +280,6 @@ const invitesView = ({ invitesEnabled, flash }) => { section( div({ class: 'invites-inhabitants', id: 'invites-inhabitants' }, h2(i18n.invitesInhabitantsTitle || 'Inhabitants'), - p(i18n.invitesInhabitantsHint), form( { action: '/invites/inhabitant/follow', method: 'post' }, input({ name: 'feedId', id: 'inh_oasis_id', type: 'text', placeholder: '@...=.ed25519', pattern: '@[A-Za-z0-9+/_\\-]{43}=\\.ed25519', required: true, maxlength: 56 }), diff --git a/src/views/jobs_view.js b/src/views/jobs_view.js index 0ea989f..8c6694b 100644 --- a/src/views/jobs_view.js +++ b/src/views/jobs_view.js @@ -1,11 +1,9 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, progress, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe") -const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning } = require("./main_views") +const { template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions } = require("./main_views") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const { renderUrl } = require("../backend/renderUrl") const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") -const { resolvePhoto } = require("./inhabitants_view") const renderMediaBlob = (value, attrs = {}) => { if (!value) return null @@ -26,8 +24,6 @@ const userId = config.keys.id const FILTERS = [ { key: "ALL", i18n: "jobsFilterAll", title: "jobsAllTitle" }, { key: "MINE", i18n: "jobsFilterMine", title: "jobsMineTitle" }, - { key: "RECENT", i18n: "jobsFilterRecent", title: "jobsRecentTitle" }, - { key: "TOP", i18n: "jobsFilterTop", title: "jobsTopTitle" }, { key: "APPLIED", i18n: "jobsFilterApplied", title: "jobsAppliedTitle" }, { key: "REMOTE", i18n: "jobsFilterRemote", title: "jobsRemoteTitle" }, { key: "PRESENCIAL", i18n: "jobsFilterPresencial", title: "jobsPresencialTitle" }, @@ -36,9 +32,17 @@ const FILTERS = [ { key: "EXCHANGE", i18n: "jobsFilterExchange", title: "jobsExchangeTitle" }, { key: "OPEN", i18n: "jobsFilterOpen", title: "jobsOpenTitle" }, { key: "CLOSED", i18n: "jobsFilterClosed", title: "jobsClosedTitle" }, + { key: "RECENT", i18n: "jobsFilterRecent", title: "jobsRecentTitle" }, + { key: "TOP", i18n: "jobsFilterTop", title: "jobsTopTitle" }, { key: "CV", i18n: "jobsCV", title: "jobsCVTitle" } ] +function resolvePhoto(photoField, size = 256) { + if (typeof photoField === "string" && photoField.startsWith("/image/")) return photoField + if (typeof photoField === "string" && /^&[A-Za-z0-9+/=]+\.sha256$/.test(photoField)) return `/image/${size}/${encodeURIComponent(photoField)}` + return "/assets/images/default-avatar.png" +} + const safeArr = (v) => (Array.isArray(v) ? v : []) const safeText = (v) => String(v || "").trim() @@ -105,7 +109,7 @@ const renderUpdatedLabel = (createdAt, updatedAt) => { const updatedTs = updatedAt ? new Date(updatedAt).getTime() : NaN const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs) return showUpdated - ? span({ class: "votations-comment-date" }, ` | ${i18n.jobsUpdatedAt}: ${moment(updatedAt).format("YYYY/MM/DD HH:mm")}`) + ? span({ class: "votations-comment-date" }, ` | ${i18n.jobsUpdatedAt}: ${moment(updatedAt).format("YYYY/MM/DD HH:mm:ss")}`) : null } @@ -228,7 +232,6 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { renderJobStatusChip(job.status), isHidden ? renderJobHiddenChip() : null, isSubscribed ? renderJobAppliedChip() : null, - job.industry ? a({ href: `/industry/${encodeURIComponent(job.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, renderLifespanChip(job.lifetime, i18n) ].filter(Boolean) const isExchange = String(job.job_type || "").toLowerCase() === "exchange" @@ -241,7 +244,7 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { div( { class: "card-header activity-card-header" }, span(), - renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`, { spread: params.spreadMap && params.spreadMap.get(job.id) || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title }) + renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`) ), div({ class: "card-section job-card-body" }, heroNode, @@ -255,7 +258,8 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { div({ class: "card-date-highlight" }, compensationText), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.jobSubscribers}: ${subs.length}`) - ) + ), + div({ class: "card-spread-centered" }, renderSpreadButton(job.id, params.spreadMap && params.spreadMap.get(job.id))) ) ) ) @@ -263,11 +267,10 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { ) } -const renderJobForm = (job = {}, mode = "create", spreadWarning = null) => { +const renderJobForm = (job = {}, mode = "create") => { const isEdit = mode === "edit" return div( { class: "div-center job-form" }, - isEdit ? spreadWarning : null, form( { action: isEdit ? `/jobs/update/${encodeURIComponent(job.id)}` : "/jobs/create", @@ -275,7 +278,6 @@ const renderJobForm = (job = {}, mode = "create", spreadWarning = null) => { enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: "/jobs?filter=MINE" }), - job.industry ? input({ type: "hidden", name: "industry", value: job.industry }) : null, label(i18n.jobType), br(), select( @@ -288,7 +290,7 @@ const renderJobForm = (job = {}, mode = "create", spreadWarning = null) => { br(), label(i18n.jobTitle), br(), - input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.jobTitlePlaceholder, value: job.title || "" }), + input({ type: "text", name: "title", required: true, placeholder: i18n.jobTitlePlaceholder, value: job.title || "" }), br(), br(), label(i18n.jobImage), @@ -321,8 +323,8 @@ const renderJobForm = (job = {}, mode = "create", spreadWarning = null) => { br(), select( { name: "job_time", required: true }, - option({ value: "partial", ...(job.job_time === "partial" ? { selected: true } : {})}, i18n.jobTimePartial), - option({ value: "complete", ...(job.job_time === "complete" ? { selected: true } : {})}, i18n.jobTimeComplete) + option({ value: "partial", selected: job.job_time === "partial" }, i18n.jobTimePartial), + option({ value: "complete", selected: job.job_time === "complete" }, i18n.jobTimeComplete) ), br(), br(), @@ -335,8 +337,8 @@ const renderJobForm = (job = {}, mode = "create", spreadWarning = null) => { br(), select( { name: "location", required: true }, - option({ value: "remote", ...(job.location === "remote" ? { selected: true } : {})}, i18n.jobLocationRemote), - option({ value: "presencial", ...(job.location === "presencial" ? { selected: true } : {})}, i18n.jobLocationPresencial) + option({ value: "remote", selected: job.location === "remote" }, i18n.jobLocationRemote), + option({ value: "presencial", selected: job.location === "presencial" }, i18n.jobLocationPresencial) ), br(), br(), @@ -349,8 +351,8 @@ const renderJobForm = (job = {}, mode = "create", spreadWarning = null) => { br(), select( { name: "visibility" }, - option({ value: "PUBLIC", ...((job.visibility || "PUBLIC") === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"), - option({ value: "HIDDEN", ...(job.visibility === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden") + option({ value: "PUBLIC", selected: (job.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"), + option({ value: "HIDDEN", selected: job.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden") ), br(), br(), @@ -415,23 +417,9 @@ const renderCVList = (inhabitants) => { class: "inhabitant-details" }, user.description ? p(...renderUrl(user.description)) : null, p(userLink(user.id)), - user.skills && user.skills.length - ? div({ class: "card-tags" }, - user.skills.slice(0, 12).map((skill) => - a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)) - ) - : null, - typeof user.matchScore === "number" - ? div({ class: "cv-match-row" }, - span({ class: "card-label" }, `${i18n.matchScore}: `), - span({ class: "card-value" }, `${Math.round(user.matchScore * 100)}%`) - ) - : null, div( { class: "cv-actions" }, - form({ method: "GET", action: `/inhabitant/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.cvVisitButton)), - form({ method: "GET", action: `/cv/pdf/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.generatePdf)), - form({ method: "POST", action: `/cv/share/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)), + form({ method: "GET", action: `/inhabitant/${encodeURIComponent(user.id)}` }, button({ type: "submit", class: "filter-btn" }, i18n.inhabitantviewDetails)), !isMe ? renderPmButton(user.id) : null ) ) @@ -441,10 +429,6 @@ const renderCVList = (inhabitants) => ) exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { - if (filter === "EDIT") { - const editing = Array.isArray(jobsOrCVs) ? jobsOrCVs[0] : jobsOrCVs - params = { ...params, spreadWarning: await renderSpreadEditWarning(editing && editing.id) } - } const search = safeText(params.search || "") const minSalary = params.minSalary ?? "" const maxSalary = params.maxSalary ?? "" @@ -473,7 +457,7 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { input({ type: "hidden", name: "maxSalary", value: String(maxSalary ?? "") }), input({ type: "hidden", name: "sort", value: sort }), ...FILTERS.map((f) => - button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, i18n[f.i18n]) ), button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.jobsCreateJob) ) @@ -497,16 +481,8 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { ) : filter === "CREATE" || filter === "EDIT" ? (() => { - const jobToEdit = filter === "EDIT" - ? (Array.isArray(jobsOrCVs) ? jobsOrCVs[0] : {}) - : { - ...(params.industry ? { industry: params.industry } : {}), - ...(params.title ? { title: params.title } : {}), - ...(params.description ? { description: params.description } : {}), - ...(params.tasks ? { tasks: params.tasks } : {}), - ...(params.salary ? { salary: params.salary } : {}) - } - return renderJobForm(jobToEdit, filter === "EDIT" ? "edit" : "create", params.spreadWarning) + const jobToEdit = filter === "EDIT" ? (Array.isArray(jobsOrCVs) ? jobsOrCVs[0] : {}) : {} + return renderJobForm(jobToEdit, filter === "EDIT" ? "edit" : "create") })() : section( div( @@ -524,9 +500,9 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { ), select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.jobsSortRecent), - option({ value: "salary", ...(sort === "salary" ? { selected: true } : {})}, i18n.jobsSortSalary), - option({ value: "subscribers", ...(sort === "subscribers" ? { selected: true } : {})}, i18n.jobsSortSubscribers) + option({ value: "recent", selected: sort === "recent" }, i18n.jobsSortRecent), + option({ value: "salary", selected: sort === "salary" }, i18n.jobsSortSalary), + option({ value: "subscribers", selected: sort === "subscribers" }, i18n.jobsSortSubscribers) ), button({ type: "submit", class: "filter-box__button" }, i18n.jobsSearchButton) ) @@ -540,12 +516,61 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { } const renderJobCommentsSection = (jobId, returnTo, comments = []) => { - return renderSharedCommentsSection({ - action: `/jobs/${encodeURIComponent(jobId)}/comments`, - comments: comments, - returnTo: returnTo - }); -}; + const list = safeArr(comments).filter(c => { + const t = c && c.value && c.value.content && c.value.content.text + return t && String(t).trim() + }) + const commentsCount = list.length + + return div( + { id: "comments", class: "vote-comments-section" }, + div( + { class: "comments-count" }, + span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), + span({ class: "card-value" }, String(commentsCount)) + ), + div( + { class: "comment-form-wrapper" }, + h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), + form( + { method: "POST", action: `/jobs/${encodeURIComponent(jobId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + textarea({ id: "comment-text", name: "text", rows: 4, class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), + div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), + br(), + button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) + ) + ), + list.length + ? div( + { class: "comments-list" }, + list.map((c) => { + const author = c?.value?.author || "" + const ts = c?.value?.timestamp || c?.timestamp + const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : "" + const relDate = ts ? moment(ts).fromNow() : "" + const userName = author && author.includes("@") ? author.split("@")[1] : author + const rootId = c?.value?.content ? (c.value.content.fork || c.value.content.root) : null + const text = c?.value?.content?.text || "" + + return div( + { class: "votations-comment-card" }, + span( + { class: "created-at" }, + span(i18n.createdBy), + author ? userLink(author) : span("(unknown)"), + absDate ? span(" | ") : "", + absDate ? span({ class: "votations-comment-date" }, absDate) : "", + relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", + relDate && rootId ? a({ href: `/thread/${encodeURIComponent(rootId)}#${encodeURIComponent(c.key)}` }, relDate) : "" + ), + p({ class: "votations-comment-text" }, ...renderUrl(text)) + ) + }) + ) + : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) + ) +} const renderCandidates = (candidates, jobId) => { if (!Array.isArray(candidates) || candidates.length === 0) return null; @@ -608,12 +633,12 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) const ownerActions = renderJobOwnerActions(job, returnTo) const applyToggle = renderJobApplyToggle(job, returnTo) + const pmBtn = renderPmButton(job.author) const visibility = (job.visibility || 'PUBLIC').toUpperCase() === 'HIDDEN' ? 'HIDDEN' : 'PUBLIC' const chips = [ renderJobStatusChip(job.status), visibility === 'HIDDEN' ? renderJobHiddenChip() : null, isSubscribed ? renderJobAppliedChip() : null, - job.industry ? a({ href: `/industry/${encodeURIComponent(job.industry)}` }, renderStateChip("whole", "🏭", String(i18n.industryTitle || "Industry").toUpperCase())) : null, renderLifespanChip(job.lifetime, i18n), renderEcoTax(job.msgSize, job.id), renderReachChip(isClearnet, i18n) @@ -635,16 +660,15 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) const sideActions = [] if (applyToggle) sideActions.push(applyToggle) + if (pmBtn) sideActions.push(pmBtn) for (const a of ownerActions) sideActions.push(a) const jobSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(job.id, null, { spread: params.spreads || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title }) - ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, safeText(job.title) || i18n.jobsTitle) ), chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + div({ class: "card-spread-centered" }, renderSpreadButton(job.id, params.spreads)), job.image ? renderMediaBlob(job.image, { class: "tribe-detail-image" }) : null, @@ -685,7 +709,6 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) return template( i18n.jobsTitle, - section(div({ class: "tags-header" }, h2(i18n.jobsTitle), p(i18n.jobsDescription))), section( div( { class: "filters" }, @@ -696,7 +719,7 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) input({ type: "hidden", name: "maxSalary", value: String(params.maxSalary ?? "") }), input({ type: "hidden", name: "sort", value: safeText(params.sort || "recent") }), ...FILTERS.map((f) => - button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) + button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, i18n[f.i18n]) ), button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.jobsCreateJob) ) diff --git a/src/views/karvan_view.js b/src/views/karvan_view.js deleted file mode 100644 index 54ab789..0000000 --- a/src/views/karvan_view.js +++ /dev/null @@ -1,121 +0,0 @@ -"use strict"; -// Vista del módulo KARVAN (mensajes temporales + WebRTC). El chat va por relay servidor (RAM, -// autodestrucción) y, si conectan pares, por data-channel WebRTC (instantáneo). El