android: downloads on Wi-Fi only

One switch the desktop never needed, off by default: on mobile data every
Save the phone can start — a record, a track, a folder, a file, a
playlist's bulk, the now-playing Save — waits, with the reason on the bar;
streaming is never held back. The walk flips the switch, makes the
connection cellular and sees a download refused.
This commit is contained in:
KeelrLeek committed 2026-09-13 22:15:07 -05:00
1 parent 3c8989dfa6
commit 694a29eb0d
10 files changed
+55 -5

No files matched your search

+4 -1
View File
@@ -18,6 +18,7 @@
import { lib, albumById, albumsBy, tracksOf, loadLibrary, refreshPlaylists, refreshRecents } from "./lib.svelte.js";
import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics, pasteLyrics } from "./lyrics.svelte.js";
import { art, wantArt } from "./art.svelte.js";
import { downloadsAllowed } from "./network.svelte.js";
import { note } from "./notice.svelte.js";
import { fmtOf, albumFmt, fmtBytes, basename, dirname, longDur, mmss, titleFromName } from "./util.js";
@@ -185,6 +186,7 @@
pipeline — and the whole streamed record the queue holds */
const streamMates = (track, album) => player.queue.filter((e) => e.t?.stream && e.t.path && e.album?.title === album?.title && e.album?.artist === album?.artist);
async function saveStreams(entries) {
if (!downloadsAllowed()) return;
const srcs = entries.filter((e) => e.t?.stream && e.t.path).map((e) => ({ username: e.t.stream.username, path: e.t.path, size: e.t.stream.size }));
if (!srcs.length) return;
try {
@@ -200,6 +202,7 @@
if (es.length) playEntries(es, 0, g.folderName).catch(oops);
}
async function saveGroup(g) {
if (!downloadsAllowed()) return;
try {
for (const f of g.files) await api.downloadFile(g.username, f.path, f.size);
note(`Saving ${g.files.length} file${g.files.length === 1 ? "" : "s"} from ${g.username}`);
@@ -544,7 +547,7 @@
<div class="sheet-rows">
{@render row("play", "Play from here", "the rest of the folder follows", act(() => { const es = groupEntries(g, i); if (es.length) playEntries(es, 0, g.folderName).catch(oops); }))}
{#if f}
{@render row("download", "Save this file to library", "as it is, from " + g.username, act(() => api.downloadFile(g.username, f.path, f.size).then(() => note(`Saving “${titleFromName(f.name)}” from ${g.username}`)).catch(oops)))}
{@render row("download", "Save this file to library", "as it is, from " + g.username, act(() => { if (!downloadsAllowed()) return; api.downloadFile(g.username, f.path, f.size).then(() => note(`Saving “${titleFromName(f.name)}” from ${g.username}`)).catch(oops); }))}
{/if}
<div class="sheet-hair"></div>
{@render row("folder-open", "Browse their files", "everything " + g.username + " shares", act(() => push("browse", { username: g.username })))}
+2 -1
View File
@@ -47,6 +47,7 @@
import * as pls from "./playlists.svelte.js";
import { importer, bindImporter } from "./importer.svelte.js";
import { bindShare } from "./share.svelte.js";
import { downloadsAllowed, cellular } from "./network.svelte.js";
import * as sources from "../lib/sources.js";
import { greeting, drag } from "./util.js";
@@ -191,7 +192,7 @@
setTimeout(() => { api.libraryArtPrewarm([360]).catch(() => {}); }, 8000);
/* the probe handle — the desktop has __soulstream for the same reason:
tests/mobile/smoke.mjs drives the real app through it */
window.__ss = { nav, top, go, push, back, openSheet, closeSheet, openHalf, closeHalf, player, lib, art, slsk, net, resolver, streamSong, streamCatalogAlbum, sources, api, transfers, pls, notice, importer, next, prev, toggle, seekSecs, playQueueAt, patch };
window.__ss = { nav, top, go, push, back, openSheet, closeSheet, openHalf, closeHalf, player, lib, art, slsk, net, resolver, streamSong, streamCatalogAlbum, sources, api, transfers, pls, notice, importer, wifi: { downloadsAllowed, cellular }, next, prev, toggle, seekSecs, playQueueAt, patch };
return () => { if (unlisten) unlisten(); };
});
</script>
+22
View File
@@ -0,0 +1,22 @@
/* The phone's connection — for the one guard the desktop never needed:
downloads kept to Wi-Fi when the switch says so (You → Downloads).
Streaming is never refused: that is what a phone on the network is for.
navigator.connection is Chrome's; a WebView without it reports nothing,
and nothing is refused. */
import { settings } from "./prefs.svelte.js";
import { player } from "./player.svelte.js";
export const cellular = () => {
const c = typeof navigator !== "undefined" ? navigator.connection : null;
return !!c && c.type === "cellular";
};
export const NO_WIFI = "Downloads wait for Wi-Fi — the switch is under You → Downloads.";
/** true when a download may start now; otherwise the reason is on the bar */
export function downloadsAllowed() {
if (settings.wifiOnly && cellular()) {
player.error = NO_WIFI;
return false;
}
return true;
}
+2
View File
@@ -12,6 +12,7 @@ import { player, playEntries } from "./player.svelte.js";
import { streamSong } from "./resolve.svelte.js";
import { resolveSources, songCandidates, simplifyTitle, cachedStreamSourceFor, normText } from "../lib/sources.js";
import { slsk } from "./slsk.svelte.js";
import { downloadsAllowed } from "./network.svelte.js";
import { note } from "./notice.svelte.js";
export const pls = $state({
@@ -306,6 +307,7 @@ export function dismissBulk() { bulk.summary = null; }
export const bulkable = (items) => items.filter((i) => i.kind === "stream" && i.artist && i.title).length;
export async function runBulk(p, items, mode) {
if (bulk.active) return false;
if (mode === "download" && !downloadsAllowed()) return false;
const seen = new Set();
const want = items.filter((r) => {
if (r.kind !== "stream" || !r.artist || !r.title) return false;
+1 -1
View File
@@ -4,7 +4,7 @@
/* `glass` is the desktop's frosted chrome (a backdrop blur behind the bars).
Off by default on the phone: tests/mobile/perf.mjs measured it at half the
frames on a Home scroll and on the player's opening. */
export const settings = $state({ ambience: true, accent: true, motion: true, glass: false });
export const settings = $state({ ambience: true, accent: true, motion: true, glass: false, wifiOnly: false });
const KEY = "ss.mobile.prefs";
+3
View File
@@ -20,6 +20,7 @@ import { player, hooks, playEntries, playAlbum } from "./player.svelte.js";
import { lib, tracksOf } from "./lib.svelte.js";
import { slsk } from "./slsk.svelte.js";
import { yearOf } from "./catalog.svelte.js";
import { downloadsAllowed } from "./network.svelte.js";
export const resolver = $state({ active: false, label: "", error: "" });
@@ -189,6 +190,7 @@ export async function streamCatalogAlbum(album, at = 0) {
* art and logs beside the finished album and says soulseek:library-changed.
* Returns { username, files } or false. */
export async function saveCatalogAlbum(album) {
if (!downloadsAllowed()) return false;
if (resolver.active) return false;
if (!online()) { resolver.error = OFFLINE; player.error = OFFLINE; return false; }
const catalogTracks = album.id
@@ -218,6 +220,7 @@ export async function saveCatalogAlbum(album) {
/** One song into the library, by the download rules. */
export async function saveSong(artist, title) {
if (resolver.active) return false;
if (!downloadsAllowed()) return false;
if (!online()) { resolver.error = OFFLINE; player.error = OFFLINE; return false; }
const { responses, artistIncluded } = await resolveSources(
`${artist} — ${title}`,
+2 -1
View File
@@ -10,6 +10,7 @@
import { player, playEntries } from "../player.svelte.js";
import { openHalf } from "../nav.svelte.js";
import { note } from "../notice.svelte.js";
import { downloadsAllowed } from "../network.svelte.js";
import { mmss, fmtBytes, titleFromName, folderMeta } from "../util.js";
let { username } = $props();
@@ -76,7 +77,7 @@
}
async function save(d) {
const list = d.files.filter(audio);
if (!list.length) return;
if (!list.length || !downloadsAllowed()) return;
try {
for (const f of list) await api.downloadFile(username, f.path, f.size);
note(`Saving ${list.length} file${list.length === 1 ? "" : "s"} from ${username}`);
+5
View File
@@ -242,6 +242,11 @@
<div class="sheet-sec">Downloads</div>
<div class="group">
<button class="row" onclick={() => setSetting("wifiOnly", !settings.wifiOnly)}>
<span class="lead"><Icon name="globe" size={20} /></span>
<span class="b"><span class="t">Downloads on Wi-Fi only</span><span class="m">on mobile data, Save waits; streaming is never held back</span></span>
<span class="sw" class:on={settings.wifiOnly}><span></span></span>
</button>
<button class="row" onclick={layoutPrompt}>
<span class="lead"><Icon name="folder-open" size={20} /></span>
<span class="b"><span class="t">Album folder format</span><span class="m mono">{cfg?.library_layout || "{artist}\\{album}"}</span></span>