Skip to content

Add the mobile plugin as a bundled plugin#3128

Merged
xirvik merged 7 commits into
Novik:masterfrom
xombiemp:add-mobile-plugin
Jul 24, 2026
Merged

Add the mobile plugin as a bundled plugin#3128
xirvik merged 7 commits into
Novik:masterfrom
xombiemp:add-mobile-plugin

Conversation

@xombiemp

@xombiemp xombiemp commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR adds the mobile plugin (https://github.com/xombiemp/rutorrentMobile) as a bundled plugin. It's a touch-friendly interface that replaces the desktop UI when a phone or tablet is detected (or when ?mobile=1 is passed): torrent list with filtering and sorting, torrent details with General/Trackers/Files/Peers tabs, adding torrents, per-torrent actions, speed limits and server status.

The plugin has been maintained as a standalone repo since 2013; full history is there. It was recently reworked for current ruTorrent: it uses the core's Bootstrap 5 assets and category list, piggybacks on the core's existing polling loop instead of running its own, and integrates with _getdir, erasedata, seedingtime, ratio, throttle, datadir, diskspace, check_port, geoip and tracklabels when they're installed (all optional).

Ahead of this PR the code was audited for inclusion: remote strings (torrent/tracker/file/peer data) are escaped or rendered via DOM text nodes, eslint and editorconfig-checker pass with the repo configs, and stale copies of other plugins' internals were removed in favor of calling those plugins' own request stubs.

Notes for reviewers:

  • Works with either RPC transport (httprpc or a direct XML-RPC mount); httprpc is the recommended and most-tested setup.
  • Bootstrap Icons font files are bundled (fonts/, MIT, license included) since the core doesn't ship them.
  • Only the de/fr/hu/it lang files are actually translated; the rest carry English strings for the plugin's three UI keys. Translations welcome after merge.

Test plan

  • Open ruTorrent on a phone/tablet, or append ?mobile=1 on desktop
  • Torrent list renders; filter (status/label/tracker), sort, and check the counts
  • Torrent details: all four tabs load and live-refresh
  • Add a torrent by file and by URL; delete with data (erasedata installed)
  • Set priority, label, ratio group and throttle channel from the General tab
  • Settings page: speed limits, disk meter (diskspace), port status (check_port)
  • Desktop UI unaffected when the plugin doesn't activate

A touch-friendly mobile interface that replaces the desktop UI when a
phone or tablet is detected (or ?mobile=1 is passed): torrent list
with filtering and sorting, torrent details (general/trackers/files/
peers), adding torrents, per-torrent actions, speed limits and server
status. Built on the core's Bootstrap 5 assets and category list;
integrates with _getdir, erasedata, seedingtime, ratio, throttle,
datadir, diskspace, check_port, geoip and tracklabels when installed.

Developed at https://github.com/xombiemp/rutorrentMobile (see that
repo for full history).
@xirvik

xirvik commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

I've spent a bit of time testing this and it doesn't "just work".

Some notes:

Blocker: disableOthers() disables the active RPC backend on some installs → empty torrent list

plugin.disableOthers() disables every plugin not in keepEnabled, and that list hardcodes the RPC backend by name (rpc, httprpc). ruTorrent's RPC backend is pluggable, so on any install whose active backend is a different plugin, the mobile UI disables the live backend. The client then falls back to sending raw XML-RPC to an endpoint that only speaks the backend's own wire format — every request 500s. Because the row render runs inside the getalltrackers callback (whose error handler is a no-op), process() never runs and the list stays empty, with no visible error.

Instead of hardcoding backend names, skip plugins the server marks un-shutdownable — which is exactly how a backend flags itself (plugin.may_be_shutdowned: 0, exposed as !plugin.canShutdown()):

  $.each(thePlugins.list, function(i, v) {
    if ($.inArray(v.name, keepEnabled) == -1 && v.canShutdown()) {
      v.disable();
    }
  });

Keeps whatever the active backend is (and other core-critical plugins) enabled, no names hardcoded. Verified this fixes the empty list, at least on the non-httprpc backend I have.

Two console errors after takeover:

Both come from the same root: disable() only sets plugin.enabled = false — it doesn't undo the hooks a plugin installed on theWebUI / the category list at load, nor cancel its setTimeout/setInterval loops. Since the desktop engine keeps polling under the mobile UI, those dangling hooks keep firing:

  1. TypeError: ... panelLabelAttribs.prss is undefined — the RSS plugin's theWebUI.addRSSItems + self-rescheduling loadRSS keep calling categoryList.refresh('prss') on a panel that's no longer maintained. (You already neutralize theWebUI.loadTorrents / filterByLabel for exactly this reason — the RSS entry points need the same.)
  2. Invalid dimensions for plot, width = 0, height = 0 (jquery.flot.js) — the traffic-graph plugin's Flot plot keeps a window-resize handler bound; once the mobile UI hides the desktop layout the placeholder is 0×0 and the resize throws. plot.shutdown() on takeover removes it.

I saw a few more things during testing but let's start with that.

I'm quite hesitant about adding a new plugin to core distribution because that typically means I will be on the hook for keeping it working with each release, so it needs needs to be (or seem to be) perfect before considering it.

@xombiemp

Copy link
Copy Markdown
Collaborator Author

@xirvik Great feedback! I'll take a look at those items. And honestly, I'm ok keeping this plugin separate, but as is already evident, I think the plugin would really benefit with some other experienced eyes looking at it.

xombiemp added 2 commits July 21, 2026 08:24
…ic hooks

- Keep any plugin the server marks un-shutdownable enabled instead of
  hardcoding backend names, so pluggable RPC backends survive takeover
- Stub theWebUI.loadRSS/addRSSItems so the rss plugin's polling loop
  stops refreshing the discarded desktop category panel
- Shut down the trafic plugin's Flot plot to unbind its window-resize
  handler, which throws once the desktop layout is gone
An audit of all bundled plugins for the same failure class found more
leftovers that disable() doesn't undo:

- cpuload creates its Flot plot and window-resize handler from an
  unguarded retry loop, throwing "Invalid dimensions for plot" after
  the desktop layout is gone (the likely source of the reported error;
  the trafic graph can't exist while disabled since onLangLoaded is
  skipped), and re-fetches the CPU load on every list poll
- diskspace keeps its self-rescheduling free-space poll running,
  duplicating the mobile UI's own diskspace request
- rss's config wrap starts getrsssettings plus two perpetual timers
- history's config wrap spins a 1s wait loop that never terminates
  while disabled; _task's fires a pointless tasklist request
- trafic keeps polling getratios every updateInterval minutes

Entry points reachable via theWebUI.config wraps are stubbed in
disableOthers (before config runs); the cpuload/diskspace status-bar
widgets are dismantled after the takeover, once the removed desktop
stylesheets keep their retry loops from re-creating anything.
@xirvik

xirvik commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Tested a bit more. The history.go(-1) is really unreliable - really needs to be replaced.

filter/sort/delete/save-path controls can reload the page or do nothing

All the in-app back/cancel/OK controls navigate with history.go(-1):

  • #navBack (top back arrow)
  • #filterDone (filter OK)
  • #sortCancel, #dataDirCancel, #deleteCancel

history.go(-1) walks the browser's global history, which has no relationship to the app's own page state. It lands on whatever the previous history entry happens to be — a page visited before the UI loaded, or (once the UI has pushed its own hash entries) an unpredictable one. In practice, tapping filter OK doesn't return to the torrent list: it steps back in browser history — reloading a previous URL (full page reload) or doing nothing if there's no prior entry.

Two things compound it:

  1. The controls call history.go(-1) instead of the app's own navigation.
  2. They're bare elements (default type="submit"), so any path where the handler doesn't navigate away first can submit and reload.

Suggested fix:

  • Give these buttons type="button" and call the app's own navigation directly — e.g. mobile.showList() for filter OK, mobile.showPage(...) for the others — instead of history.go(-1).
  • If hardware/browser back support is desired, drive it from a single hashchange/popstate handler that maps the current hash to the right showPage(), rather than a blind go(-1) from each control.

Repro: open the filter, select a status, tap OK — it doesn't return to the list.

Here's a check-list of stuff to test, I recommend you do it twice (in the same session)

  1. Add torrent (highest-risk, untested)
  • Add by .torrent file upload → verify it appears in the list (this exercises the hidden upload iframe + noty result parsing).
  • Add by URL / magnet (paste into the URL field).
  • Add with options: start-stopped, fast-resume, don't-add-name, randomize-hash.
  • Label dropdown: pick an existing label; pick "new label" and type one.
  • Set a base directory on add.
  1. Torrent details (tap a torrent → tabs)
  • General tab — fields populate.
  • Files tab — file tree loads; change a file's priority (High/Normal/Low/Don't download); watch percentages update.
  • Trackers tab — tracker list loads.
  • Peers tab — peer list loads; try Add peer (IP), Ban, Kick, Snub/Unsnub a peer.
  1. Per-torrent actions
  • Start / Stop / Pause a torrent.
  • Change priority (torrent-level).
  • Change label on an existing torrent.
  • Save path / data dir — open it, and try the "..." directory browser (_getdir).
  • Delete — open confirm dialog; test delete with data checkbox (erasedata) and delete without data. (Use a throwaway torrent.)
  1. Settings (global)
  • Global download / upload speed limits (throttle) — set and confirm they stick.
  • Accent color change.
  • Server info panel — rTorrent/ruTorrent version, IPv4/IPv6 ports, open status, free disk space (diskspace).
  1. Integration plugins (kept enabled — exercise each)
  • Ratio groups (ratio plugin) — if a ratio-group selector shows on a torrent.
  • Seeding time display (seedingtime).
  • GeoIP flags on peers (geoip).
  1. Navigation / edge cases
  • Hardware Back button across pages (list → details → back, filter → back).
  • Deep-ish flows: details → Files → back → another torrent.
  • Leave it running a few minutes on the list (confirms the poll loop stays clean — this is where a stale disabled-plugin timer would eventually throw).
  • Rotate the phone (portrait/landscape) on the list and on a details tab (resize path — this is where the flot/cpuload fixes matter).

… testing-round fixes

Navigation (per review): every back/cancel/OK control used
history.go(-1), which walks the browser's global history and can land
outside the app or full-page-reload a stale URL. Controls now call the
app's own navigation: a goBack() that knows each page's parent, and a
navbar home button (house icon, replacing the back arrow) that always
returns to the torrent list. Hardware/browser back still works through
the existing hashchange listener. All bare buttons got type="button".

Behavior fixes found while testing the round:
- Restore the list scroll position after the hash update (assigning an
  empty fragment scrolls to top), and reset it when the sort or filter
  actually changes, since the old position is meaningless then
- Make the filter page transactional: taps still apply live, OK commits,
  and leaving any other way rolls the changes back; show the matched
  count / total size on the filter page itself
- Neutralize sticky :hover on touch devices, which left tapped buttons
  looking permanently pressed (Bootstrap's outline-button hover is a
  filled state)
- Return to the list after a successful torrent add (errors keep the
  form for correction)
- Scope toast dismiss timers to their own element - a previous toast's
  timers could hide the current one early - and position toasts below
  the top edge so iOS doesn't extend their color into the status bar
- Route noty() notifications into mobile toasts, and toast deleted
  torrents (detected from the list diff; the desktop gets this from the
  history plugin, which is disabled on mobile)
- Truncate file percentages like the desktop does (theConverter.round
  floors) instead of rounding
- Shrink the filter tab's tap target to its content, so taps on the
  empty tab-bar space no longer open the filter page
- Show the full content path (base_path) as Save As on the General tab,
  like the desktop details; the move form still prefills the directory
- Live-refresh the settings page speed-limit selects when the limits
  change elsewhere (e.g. from the desktop UI)
- Treat OK without changes on the save-path page as cancel; the datadir
  worker stops and closes the torrent even when there is nothing to move
@xombiemp

Copy link
Copy Markdown
Collaborator Author

@xirvik I really appreciate your detailed feedback!
I've added another commit which addresses your feedback on the navigation. history.go(-1) is not used at all anymore.
I also went through all your checklists. Some things were found and fixed as part of the testing. I even found a httprpc bug that I submitted another PR for.

Let me know what's next!

After a successful add, the list scrolls to the new torrent's row once
it appears (magnet and URL adds can take a few polls to materialize;
the intent expires after 30s) and fades an accent-colored highlight on
it. Detection rides the existing per-cycle row diff, picking the
topmost added row in display order when several arrive at once, and is
skipped when the user has navigated away or the active filter hides
the row. The highlight is an inset box-shadow on the cell: it renders
above the table striping, and row-level classes are reset by every
row update.
@xirvik xirvik closed this Jul 24, 2026
@xirvik xirvik reopened this Jul 24, 2026
@xirvik

xirvik commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

One more small thing — cache-busting for mobile.html.

The plugin fetches its markup at init with no cache-buster:

  $.ajax({
    type: 'GET',
    url: this.path + 'mobile.html',
    processData: false,

The other plugin assets (init.js, mobile.css) go through ruTorrent's plugin loader with its ?v= version token, so they refresh automatically on a new release. mobile.html bypasses that path, so after an update the browser keeps serving the cached copy until the user hard-reloads — even though the new init.js loads normally.

That's a problem specifically because the two are coupled: if a release changes the markup (new element IDs, a new page/section), the updated init.js ends up running against stale HTML that's missing those elements, which can break the UI until the user manually clears the cache.

Could you cache-bust that fetch? Either the simple cache: false (jQuery appends a timestamp), or append the same ?v= version token the core assets use, so it still caches between releases.

xombiemp added 2 commits July 24, 2026 08:43
A search icon right of the sort icon toggles a search bar fixed below
the tab bar; typing filters the list live with the same semantics as
the desktop quick search (case-insensitive substring on the name, * as
a wildcard, 220ms debounce; see TextSearch in js/panel.js). The search
narrows whatever the status/label/tracker filters matched, so the
filter chip and the filter page totals reflect it, and the chip shows
the active term in quotes. Closing the bar (its X button or the search
icon) clears the search so the list can't stay invisibly narrowed.

iOS details: the bar is tucked 1px under the measured tab-bar height
so fractional heights can't leave a see-through gap, and the keyboard
is dismissed when a scroll gesture starts, since position:fixed
elements drift off-screen while it's open (touchmove, not scroll:
Safari's scroll-into-view on focus would close it as it opens).
The markup was fetched without a cache-buster, so after an update the
browser could keep serving a stale cached mobile.html while the new
init.js loads through the plugin loader's ?v= token - new code running
against old markup with missing element ids. Use the core's
cacheBust() for the fetch, so the markup stays cached between releases
and refreshes together with the code on a version bump.
@xirvik
xirvik merged commit 37daf0c into Novik:master Jul 24, 2026
5 checks passed
@Abzie

Abzie commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

I don't seem to to be able to load the torrent list.

From my desktop, I can force the mobile view with ?mobile=1 and I can browse the settings/add torrent pages.
I get a similar experience on my mobile, iPhone 15, iOS 27 Beta 4.

Both attempts using Chrome (cleared cache and force reload on Desktop, incognito mode on mobile).

integrates with _getdir, erasedata, seedingtime, ratio, throttle, datadir, diskspace, check_port, geoip and tracklabels when they're installed (all optional).

The only plugin I am missing is check_port (disabled via plugins.ini), enabling it does not resolve the missing torrents list.

I think this needs some more time in the oven to test connectivity via local sockets.

plugins.ini
;; Plugins' permissions.
;; If flag is not found in plugin section, corresponding flag from "default" section is used.
;; If flag is not found in "default" section, it is assumed to be "yes".
;;
;; For setting individual plugin permissions you must write something like that:
;;
;; [ratio]
;; enabled = yes                        ;; also may be "user-defined", in this case user can control plugin's state from UI
;; canChangeToolbar = yes
;; canChangeMenu = yes
;; canChangeOptions = no
;; canChangeTabs = yes
;; canChangeColumns = yes
;; canChangeStatusBar = yes
;; canChangeCategory = yes
;; canBeShutdowned = yes

[default]
enabled = user-defined
canChangeToolbar = yes
canChangeMenu = yes
canChangeOptions = yes
canChangeTabs = yes
canChangeColumns = yes
canChangeStatusBar = yes
canChangeCategory = yes
canBeShutdowned = yes

[spectrogram]
enabled = no

[_cloudflare]
enabled = no

[xmpp]
enabled = no

[feeds]
enabled = no

[rss]
enabled = no

[extsearch]
enabled = no

[mediainfo]
enabled = no

[rssurlrewrite]
enabled = no

[screenshots]
enabled = no

[ipad]
enabled = no

[bulk_magnet]
enabled = no

[scheduler]
enabled = no

[rutracker_check]
enabled = no

[retrackers]
enabled = no

[httprpc]
enabled = no

[loginmgr]
enabled = no

[lookat]
enabled = no

[check_port]
enabled = yes

[dump]
enabled = no

[unpack]
enabled = no

[mobile]
enabled = yes

@xirvik

xirvik commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

I think this needs some more time in the oven to test connectivity via local sockets.
plugins.ini

This was merged so now it should be an issue, not a comment on the PR - I saw this by chance.

Try #3133

Maybe it will fix the issue (unusure), but please comments on PRs before they are merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants