feat(explore): refuse 0.6 GB on someone's mobile data
Plan 016 B4. The catalog artifact is about 0.6 GB and the app fetched it with no awareness of the connection: on a desktop that is a minute of bandwidth, on a phone it can be a month's allowance. It is now skipped on a cellular connection unless `AllowMeteredCatalogDownload` is on, with the toggle in Settings' Search Index section, where the text explaining what the catalog is already lives. The file layout is dictated by the cgo rule rather than by taste. `explore` is imported by `cmd/indexbuild`, which builds with CGO_ENABLED=0 and must not link Wails, so `netpolicy.go` holds the policy and the JSON parsing -- tested on every platform -- and the single platform call is a closure injected from `app.go`, which already names `application` legitimately. Three rules in it are load-bearing. An unknown answer is not a metered one: only mobile answers at all, and treating silence as metered would have disabled the download for every desktop user in the world. Cellular is the only signal available, because the runtime reports `wifi|cellular|ethernet|none` and no metered flag -- so a metered Wi-Fi cannot be detected and is not refused, which is documented rather than implied. And the gate runs before the first status write, so declining is a no-op instead of a job in the indicator and an error tier to dismiss. Two corrections to the plan while implementing it: the portable API is `application.Mobile.NetworkJSON()`, not `application.Android`'s, which exists only under the `android` build tag; and the permission is read at the moment a download would start, so enabling it takes effect on the next attempt rather than the next launch.
This commit is contained in:
@@ -17,6 +17,8 @@ import {
|
||||
SetDefaultPage,
|
||||
GetQueueFallback,
|
||||
SetQueueFallback,
|
||||
GetAllowMeteredCatalogDownload,
|
||||
SetAllowMeteredCatalogDownload,
|
||||
} from '@go/config/config.js';
|
||||
import { GetIndexStatus } from '@go/explore/service.js';
|
||||
import { notificationStore } from '@store/notification-store';
|
||||
@@ -75,6 +77,9 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
// --- Now Playing state ---
|
||||
@state() private scrollMode = 'hover';
|
||||
|
||||
/** Whether the ~0.6 GB catalog may be fetched on mobile data. */
|
||||
@state() private allowMeteredCatalogDownload = false;
|
||||
|
||||
// --- Favorites state ---
|
||||
@state() private playlists: playlist.Summary[] = [];
|
||||
|
||||
@@ -888,17 +893,20 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
|
||||
private async loadLibraries(): Promise<void> {
|
||||
try {
|
||||
const [libs, mode, defaultPage, queueFallback] = await Promise.all([
|
||||
GetAllLibrariesWithTrackCounts(),
|
||||
GetScanConcurrency(),
|
||||
GetDefaultPage(),
|
||||
GetQueueFallback(),
|
||||
]);
|
||||
const [libs, mode, defaultPage, queueFallback, allowMetered] =
|
||||
await Promise.all([
|
||||
GetAllLibrariesWithTrackCounts(),
|
||||
GetScanConcurrency(),
|
||||
GetDefaultPage(),
|
||||
GetQueueFallback(),
|
||||
GetAllowMeteredCatalogDownload(),
|
||||
]);
|
||||
|
||||
this.libraries = libs ?? [];
|
||||
this.concurrencyMode = mode;
|
||||
this.defaultPage = defaultPage;
|
||||
this.queueFallback = queueFallback;
|
||||
this.allowMeteredCatalogDownload = allowMetered;
|
||||
|
||||
} catch (err) {
|
||||
console.error(
|
||||
@@ -1500,10 +1508,53 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
</div>`
|
||||
: html`<div class="index-loading">Loading status…</div>`}
|
||||
</div>
|
||||
|
||||
<config-field
|
||||
.schema=${{
|
||||
key: 'allowMeteredCatalogDownload',
|
||||
label: 'Download the catalog on mobile data',
|
||||
description:
|
||||
'The catalog is about 0.6 GB. It is skipped on a '
|
||||
+ 'cellular connection unless this is on; a '
|
||||
+ 'metered Wi-Fi network cannot be detected.',
|
||||
type: 'toggle' as const,
|
||||
}}
|
||||
.value=${this.allowMeteredCatalogDownload}
|
||||
@config-change=${this.handleAllowMeteredChange}
|
||||
></config-field>
|
||||
</config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The catalog download's one permission (plan 016 B4).
|
||||
*
|
||||
* It is in this section rather than General because it is about
|
||||
* *this* download and nothing else, and because the section already
|
||||
* explains what the catalog is — the toggle would be unreadable
|
||||
* beside "Default page".
|
||||
*/
|
||||
private handleAllowMeteredChange = (
|
||||
e: CustomEvent<ConfigFieldChangeEvent>,
|
||||
): void => {
|
||||
const allow = Boolean(e.detail.value);
|
||||
const previous = this.allowMeteredCatalogDownload;
|
||||
|
||||
this.allowMeteredCatalogDownload = allow;
|
||||
|
||||
void SetAllowMeteredCatalogDownload(allow).catch((err: unknown) => {
|
||||
console.error('failed to save metered download permission', err);
|
||||
// The visible state reverted, so this is the Transient case:
|
||||
// a small action the user can simply repeat.
|
||||
this.allowMeteredCatalogDownload = previous;
|
||||
notificationStore.transient({
|
||||
key: 'metered-catalog-setting',
|
||||
title: 'Setting not saved',
|
||||
text: describeError(err, 'That setting could not be saved.'),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private tierIcon(state: string): string {
|
||||
switch (state) {
|
||||
case 'complete':
|
||||
|
||||
Reference in New Issue
Block a user