Add the mobile plugin as a bundled plugin#3128
Conversation
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).
|
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()): 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:
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. |
|
@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. |
…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.
|
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):
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:
Suggested fix:
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)
|
… 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
|
@xirvik I really appreciate your detailed feedback! 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.
|
One more small thing — cache-busting for mobile.html. The plugin fetches its markup at init with no cache-buster: 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. |
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.
|
I don't seem to to be able to load the torrent list. From my desktop, I can force the mobile view with Both attempts using Chrome (cleared cache and force reload on Desktop, incognito mode on mobile).
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 |
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. |
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=1is 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,geoipandtracklabelswhen 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,
eslintandeditorconfig-checkerpass 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:
fonts/, MIT, license included) since the core doesn't ship them.de/fr/hu/itlang files are actually translated; the rest carry English strings for the plugin's three UI keys. Translations welcome after merge.Test plan
?mobile=1on desktop