Skip to main content

Interface

njMule has one interface, written in Vue 3 and TypeScript in the njmule-ui project. The desktop window, a Tauri shell, shows it in the system's web view and starts the engine beside it; the web interface (njmule-http) serves it to browsers behind its login page. The interface holds no engine logic: it calls the remote-control API and shows what it answers, the same way in both.

The interface in a browser: a sidebar with Transfers, Search, Shared Files, Messages, Network, Statistics and Options, and the list of downloads with their progress, speed, sources and status.The interface in a browser: a sidebar with Transfers, Search, Shared Files, Messages, Network, Statistics and Options, and the list of downloads with their progress, speed, sources and status.
The interface in a browser, on the demo engine's example data.

Stack and layout​

PartWhat it uses
FrameworkVue 3; in a browser the place shown follows the address after the # (#/transfers), so the page stays at whatever address a reverse proxy gives it
ComponentsReka UI for dialogs, menus, popovers and tooltips; TanStack Table and TanStack Virtual for the lists; components of its own for the rest
APIopenapi-typescript makes the API's types from openapi.yaml, openapi-fetch calls it, eventsource-parser reads its events
LanguageTypeScript in strict mode, checked by vue-tsc
BundlerVite, with relative paths everywhere (base: './')
TestsVitest on happy-dom
FormattingPrettier

The project holds the sources in src/, the translations in i18n/ (Translations) and the country flags in flags/, PNG files made by tools/FlagGen.java. The sources:

PathContent
main.ts, App.vue, places.tsthe start, the window's frame and the places of the sidebar
platform.ts, desktop.tswhat differs between the desktop window and a browser page; the desktop window's calls to its shell
api/client.ts, the calls to the remote-control API, and events.ts, its events
model/engine.tsthe engine's state the screens show, read through the API and kept current by its events
screens/one component per place: transfers, search, shared files, messages, network, statistics, options
dialogs/the dialogs: adding links, categories, the import from eMule, the first-run wizard, another computer's njMule, the update…
shell/the frame's parts: sidebar, top bar, status strip, command palette, notifications
ui/the shared components: the data table, icons, part bars, rate graphs, switches…
popups/the menu of the tray icon and MiniMule, small windows of the desktop shell
i18n.ts, language.tsthe languages and the texts
icons.ts, file-icons.tsthe line icons, and the icons of the kinds of files
role.tswhat a guest may open
theme.ts, styles/the theme chosen; themes.css, the themes' colours, and njmule.css, njMule's tokens and styles
__tests__/the Vitest tests

How Gradle builds it​

Gradle downloads the pinned Node.js, checks it against its SHA-256, and runs npm ci from package-lock.json without running any package's install scripts (Dependencies). Then:

TaskWhat it does
:njmule-ui:bundlegenerates the API's types from openapi.yaml into build/generated/api.d.ts, checks the types (vue-tsc) and bundles the interface with Vite into build/dist, with THIRD-PARTY.md, the licenses of every bundled package
:njmule-ui:translationsconverts the texts of i18n/ the interface uses into one JSON file per language, build/translations/i18n/<tag>.json
:njmule-ui:flagscopies the country flags of flags/
:njmule-ui:compressgzips the text files (JavaScript, CSS, SVG, JSON, Markdown) at the highest level into build/shipped, for the web interface; the page itself stays as it is, since the server fills it in
:njmule-ui:siteputs the bundle, the translations and the flags, uncompressed, in build/site, for the desktop shell
:njmule-ui:unitTestruns the Vitest tests (npm run test)
:njmule-ui:formatCheckchecks the Prettier formatting (npm run lint)

assemble runs compress and site, and check runs unitTest and formatCheck, so ./gradlew build covers them all. njmule-http takes the result of compress into its jar, under org/njmule/http/app; :njmule-desktop:shell embeds the result of site, which src-tauri/tauri.conf.json names as its frontendDist.

The web interface reads those files once and keeps them. It sends the gzipped files as they are to browsers that accept gzip and unpacks them for any client that does not. For each session it fills two meta tags into the page: njmule-csrf, the session's form token, and njmule-role, which is guest for a guest.

Formatting and types​

Format the sources with Prettier before you commit, from njmule-ui:

npm run format

Use the Node.js Gradle downloaded (njmule-ui/.gradle/nodejs/node-v…/, with npm inside) or your own Node.js 24 or newer. Prettier writes 120 columns, 4 spaces, single quotes and LF line endings. The TypeScript configuration is strict, with noUncheckedIndexedAccess and no unused locals or parameters: ./gradlew :njmule-ui:bundle reports what vue-tsc finds. npm run api makes the API's types alone, for your editor, after a change of openapi.yaml.

Talking to the API​

src/api/client.ts is the only door to the engine: a typed client of the API, whose paths and schemas come from openapi.yaml, so that a call the document does not describe fails the type check. How the interface proves itself depends on where it runs (platform.ts):

  • In a browser, every call goes to api/v1/…, relative to the page, with the session cookie (same-origin). Every change (any method but GET) carries the session's token from the njmule-csrf meta tag in the X-NjMule-CSRF header, which no page of another site can send. A 401 answer means the session ended: the page goes back to the login page. The page needs no API token: the web interface lets a logged-in session call the API whether or not the token API (--webui-api) is on.
  • In the desktop window, the shell starts the engine with a control endpoint: the engine answers on the loopback address, at a free port, only the token made for this run, which it prints for the shell (ControlEndpoint in njmule-http). The interface sends that token as a bearer token, and no cookie. A window on another computer's njMule uses that njMule's API token the same way.

An error answer's error field becomes an ApiError whose message is meant for the user.

model/engine.ts reads the engine at fixed paces (the status and the downloads every second, the servers every ten seconds, and so on), and each screen reads what it shows while it is open. The engine's events keep the state current between two readings: api/events.ts follows api/v1/events as server-sent events, read with fetch so that the desktop window's token can go in a header, and reconnects by itself; after a break the screens read everything again.

In the desktop window​

platform.ts tells where the interface runs: in the desktop shell's web view, or in a browser. In the desktop window, desktop.ts reaches the shell through Tauri's invoke for what a page cannot do: folder and file choosers, eD2K links the system hands over, the tray icon's menu and MiniMule, notifications and sounds, the system's icons of the kinds of files, the clipboard and its watch, the options only the shell applies (start minimised, hide in the notification area, ask before quitting, watch the clipboard: desktop-options.ts), opening files and folders and starting the preview player (files.ts, only while the engine runs on the same computer), and windows on another computer's njMule. In a browser the same calls do nothing or answer that they cannot, so a screen asks the platform before it offers them: a browser types the paths and shows the shell's options disabled. The tray's menu and MiniMule are small windows of the shell showing one view of the interface, chosen by ?view= in their address (popups/).

Texts and languages​

The interface's texts are the keys of its translations, t('IDS_…') for the texts taken from eMule and t('nj.…') for njMule's own (Translations).

  • The translations task scans the .ts and .vue files for literal keys in quotes and keeps only those in the JSON files. A key built at run time (t('nj.mode.' + mode)) is not found: write every key in full somewhere in the sources, as settings.ts does in its tables.
  • An unknown key shows itself in brackets, [nj.some.key], so that it is noticed.
  • The texts keep Java's placeholders (%s, %d, %.1f, %1$s, %%), which format() in i18n.ts fills in.
  • The language is the one chosen in the options, or the best match of the system's or the browser's languages among LANGUAGES in i18n.ts, with English as the fallback of every text. Arabic, Persian and Hebrew set the page right to left.

Icons​

src/icons.ts holds njMule's line icons as SVG path data on a 24 by 24 grid, drawn with a round 1.75 stroke; an icon marked filled is painted instead, and one marked directional is mirrored in right-to-left languages. Use them through the Icon component (<Icon name="FILE" />). This site draws the same file with a component of its own, NjIcon.

Colours​

The only colour literals of the interface are in src/styles/: themes.css holds the colours of each theme as --color-* custom properties, converted once from the AtlantaFX 3.0.0 themes (MIT), one rule per theme, and njmule.css holds njMule's --nj-* tokens, with a light and a dark value each and the system's colours under forced colours. theme.ts applies the theme chosen, Nord following the system's light or dark by default. Components use the --color-* and --nj-* variables only; the rules are in Code style.

The guest role​

The web interface can have a second password, for guests who may only look (Guest access). A guest's page has njmule-role set to guest, which guest in role.ts reads:

  • the server enforces the role whatever the page does: a guest's API calls may only read the transfers, the searches made, the servers, Kad and the statistics, and follow the events of the engine's start and stop, servers, searches, downloads and disk space (GUEST_READS and GUEST_EVENTS in RemoteApi, njmule-core); any other call answers 403;
  • the interface never opens the first-start wizard for a guest, disables the wizard and the import from eMule, and shows nothing of updates. GUEST_PLACES and allowed() in role.ts name the places a guest may open (the transfers, the searches, the network without its log, the statistics), but the sidebar does not use them yet: a guest's page shows every place and control, and the server refuses what they would change.
The interface as a guest sees it: the sidebar with every place, and the list of downloads with its Add links button, as on the owner's page.The interface as a guest sees it: the sidebar with every place, and the list of downloads with its Add links button, as on the owner's page.
A guest's page: the server refuses any change it asks for.

When you add a screen or a control, decide what a guest sees, and keep the server's list in RemoteApi in step with role.ts. A few calls answer only the desktop window on njMule's own computer (LOCAL_ONLY in RemoteApi): the web interface's options, the API token, and whether njMule starts at login and opens the eD2K links; a browser shows those options disabled.

Working on the interface​

On example data. The demo engine that makes this site's pictures serves the interface built by Gradle through a web interface of its own, with no network and nothing of your profile:

./gradlew :njmule-http:webCaptureArgs
java @njmule-http/build/web-capture/java.args

The first command builds everything the interface needs and writes the arguments file; run the second with a Java 25 java (the one njmule-http/build/web-capture/launcher.json names, for example). It prints one line of JSON with the address, the password and the guest password, then serves until its standard input closes: press Ctrl+D (on Windows Ctrl+Z then Enter). Run both commands again after each change.

With Vite's development server. npm run dev in njmule-ui serves the sources as they are and reloads the page as you save. It needs an engine that answers the API with a token, and the translations and flags the build makes. Start the demo with the desktop window's control endpoint instead of the web interface:

./gradlew :njmule-http:webCaptureArgs :njmule-ui:translations :njmule-ui:flags
java -Dnjmule.capture.control=true @njmule-http/build/web-capture/java.args

It prints the endpoint's address and token, {"url":"http://127.0.0.1:<port>/api/v1","token":"…"}. Then, in njmule-ui, point Vite at that port and give the page the token:

NJMULE_API=http://127.0.0.1:<port> VITE_NJMULE_TOKEN=<token> npm run dev

Vite passes the page's /api calls to the engine. A Gradle task that runs npm ci empties node_modules: stop the development server first.

With a real engine. Start the headless engine with a password file and a profile of its own, then open http://localhost:4711/:

./gradlew :njmule-app:run --args="--config-dir /home/you/njmule-dev --webui-password-file /home/you/njmule-dev/password.txt"

This engine connects to the eD2K and Kad networks like any njMule. To see the interface in the desktop window, build the shell (Building).