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:
1 parent
3c8989dfa6
commit
694a29eb0d
10 files changed
+55
-5
No files matched your search
+5
-1
@@ -215,7 +215,11 @@ record the queue holds. A moment after boot the phone warms the library's thumbn
|
||||
desktop's way (`library_art_prewarm` at the cards' edge, one decode at a
|
||||
time behind the tiles' gate). The network results carry the hub's filters (a free slot only; FLAC or
|
||||
MP3 320+), and the lyrics sheet has the desktop's paste door (plain text,
|
||||
or LRC with timestamps, kept for the track). The nav wears two badges the desktop's chrome has: transfers in flight on
|
||||
or LRC with timestamps, kept for the track). One switch the desktop never needed: You → Downloads → "Downloads on
|
||||
Wi-Fi only" (`network.svelte.js`, off by default) holds every Save — a
|
||||
record, a track, a folder, a file, a playlist's bulk, the now-playing
|
||||
Save — while the phone is on mobile data (`navigator.connection.type`);
|
||||
streaming is never held back. The nav wears two badges the desktop's chrome has: transfers in flight on
|
||||
You (the title bar's download ring) and unread chats on Community.
|
||||
The Library's count line carries the collection's hours and the sort
|
||||
offers By format (lossless by depth and rate first, then by bitrate); the
|
||||
|
||||
@@ -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 })))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -117,6 +117,7 @@ const PROBE = `(function () {
|
||||
jump: document.querySelectorAll('.screen .rail .release').length,
|
||||
countline: (document.querySelector('.countline .t-sub') || {}).textContent || '',
|
||||
navBadges: [].map.call(document.querySelectorAll('.nav .nb'), function (b) { return b.textContent.trim(); }).join('|'),
|
||||
wifiOnly: (function () { var r = [].slice.call(document.querySelectorAll('.screen .row')).find(function (x) { return /Wi-Fi only/.test(x.textContent); }); return r ? (r.querySelector('.sw.on') ? 'on' : 'off') : ''; })(),
|
||||
firstBadge: (document.querySelector('.grid2 .release .ed-badge') || {}).textContent || '',
|
||||
plthumbs: document.querySelectorAll('.pl-rows .thumb img').length,
|
||||
imp: document.querySelectorAll('.imp-rows .trk').length,
|
||||
@@ -383,6 +384,11 @@ const MOCK_STEPS = [
|
||||
{ do: `__click('.screen .row', 'Word-by-word')`, wait: 400 },
|
||||
{ do: `__click('.prefs[data-mode="albumart"] .chip', '1000 px')`, name: "you-artedge", wait: 500 },
|
||||
{ do: `__click('.prefs[data-mode="albumart"] .chip', '1500 px')`, wait: 400 },
|
||||
/* downloads on Wi-Fi only: the switch on, the connection made cellular, a download refused; then off */
|
||||
{ do: `__click('.screen .row', 'Wi-Fi only')`, name: "wifi-on", wait: 400 },
|
||||
{ do: `(function () { try { Object.defineProperty(navigator, 'connection', { value: { type: 'cellular' }, configurable: true }); } catch (e) {} return __ss.wifi.downloadsAllowed(); })()`, name: "wifi-refused", wait: 400 },
|
||||
{ do: `__click('.toast:not(.notice)', null)`, wait: 300 },
|
||||
{ do: `__click('.screen .row', 'Wi-Fi only')`, name: "wifi-off", wait: 400 },
|
||||
{ do: `__click('.screen .row', 'Clear play history')`, name: "history-confirm", wait: 600 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Clear')`, name: "history-cleared", wait: 800 },
|
||||
/* recent searches on the empty page: the ones this walk used */
|
||||
@@ -413,6 +419,9 @@ const MOCK_EXPECT = {
|
||||
"you-slots": (i) => i.slots === "3",
|
||||
"you-words": (i) => i.wordfill === "on",
|
||||
"you-artedge": (i) => i.artEdge === "1000 px",
|
||||
"wifi-on": (i) => i.wifiOnly === "on",
|
||||
"wifi-refused": (i) => /Wi-Fi/.test(i.toast),
|
||||
"wifi-off": (i) => i.wifiOnly === "off" && !i.toast,
|
||||
"history-confirm": (i) => i.half.includes("up") && /Clear the play history/.test(i.halfText),
|
||||
"history-cleared": (i) => !i.half.includes("up") && /history cleared/i.test(i.notice),
|
||||
"search-recent": (i) => i.title.startsWith("Search") && i.recents >= 1 && i.jump >= 1,
|
||||
|
||||
Reference in new issue
Block a user