DataSaverToggle
A setting to keep lighter images and off autoplay, following the phone's save-data, with what it changes.
Demo
What it changes
- PicturesFull size
- VideoPlays by itself
- MapsLoaded at once
Nothing reported yet.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/data_saver_toggle.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/data_saver_toggle.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/data_saver_toggle.qwe
let data_saver_toggle_demo_full = "data:image/svg+xml," + url_encode("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 90'><rect width='160' height='90' fill='#4b6cb7'/><circle cx='120' cy='30' r='14' fill='#ffd76a'/></svg>")let data_saver_toggle_demo_lite = "data:image/svg+xml," + url_encode("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 9'><rect width='16' height='9' fill='#9aa8c4'/></svg>")component DataSaverToggleDemoLive() { state told = "Nothing reported yet." state more = 0 <div.demo-stack> <DataSaverToggle id="dsave-demo" on_change={(on, mode) => { told = `on_change: ${on} ${mode}.` }} changes={[{ label: "Pictures", off: "Full size", on: "Lighter versions" }, { label: "Video", off: "Plays by itself", on: "Waits for a press" }, { label: "Maps", off: "Loaded at once", on: "Loaded on a press" }]}/> <p.demo-note id="dsave-told">{told}</p> <div.demo-row> <img id="dsave-photo" src={data_saver_toggle_demo_full} srcset={`${data_saver_toggle_demo_full} 2x`} data-lite={data_saver_toggle_demo_lite} alt="The harbour at noon" width="160" height="90"/> {for i in range(more) => <img key={i} class="dsave-added" src={data_saver_toggle_demo_full} data-lite={data_saver_toggle_demo_lite} alt={`Added picture ${i}`} width="160" height="90"/>} <video id="dsave-clip" autoplay="true" muted="true" playsinline="true" width="160" height="90" aria-label="A clip"></video> </div> <div.demo-row><Button size="lg" variant="outline" id="dsave-more" on_click={() => { more += 1 }}>Add a picture</Button></div> </div>}view DataSaverToggleDemo() = <DataSaverToggleDemoLive/>Source
ui/data_saver_toggle.qwe
// DataSaverToggle: a setting for using less data: lighter pictures and no video playing by// itself. It can follow the phone's own data saver, or be on or off here, and it shows what// it changes, on this page as well as in general. It does the work itself on what is on the// page, and marks the page so a program's CSS and routes can do more.//// import ui/data_saver_toggle.qwe use DataSaverToggle//// route GET / = <body>// <DataSaverToggle saved={cookie("ui-data-saver")}/>// <img src="/photo-1600.jpg" data-lite="/photo-400.jpg" alt="The harbour"/>// <video src="/clip.mp4" autoplay muted playsinline></video>// </body>//// saved: the choice, "follow", "on" or "off", which it keeps in a cookie called// ui-data-saver, as cookies last in a phone app, where localStorage comes back empty each// launch. A route can read it, so the first paint is right too: data_saver_toggle_light(mode,// request.headers["save-data"] == "on") says whether to send the light version.// changes: what the setting changes, each { label, off, on }; the default lists pictures and// video. on_change(active, mode): inside one of your components, when either changes. legend:// the heading. id: names its radio buttons, so give each one its own. class.//// While on: an <img data-lite="small.jpg"> shows that picture and gives up its srcset, each// <video autoplay> is paused and loses autoplay, with preload="none", and `data-data-saver="on"`// is on <html>, for rules such as `[data-data-saver="on"] .hero { background-image: none }`.// What is added to the page later is treated too. Off puts it all back.//// What a phone tells: Chrome on Android gives navigator.connection.saveData, and the// Save-Data header. iOS tells nothing of Low Data Mode, to pages or to web views, so there it// says so, and the choice here is what counts.//// data_saver_toggle_active(mode, phone) and data_saver_toggle_status(mode, phone) say whether// it is on, and why. phone is true, false or null, for not known.import ui/theme.qwe use ui_clslet data_saver_toggle_css = css`.ui-dsave { container-type: inline-size; min-width: 0; color: var(--ui-fg); font: 15px/1.4 var(--ui-font); }.ui-dsave-in { display: grid; gap: 12px; }.ui-dsave-set { display: grid; gap: 10px; min-width: 0; margin: 0; padding: 0; border: 0; }.ui-dsave-set legend { padding: 0 0 8px; font-size: 16px; font-weight: 600; }.ui-dsave-opts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); overflow: hidden; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); }.ui-dsave-opt { position: relative; display: grid; place-items: center; min-height: 48px; padding: 4px 6px; border-left: 1px solid var(--ui-border); background: var(--ui-surface); text-align: center; font-size: 14px; font-weight: 500; line-height: 1.2; cursor: pointer; touch-action: manipulation; }.ui-dsave-opt:first-child { border-left: 0; }.ui-dsave-opt input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }.ui-dsave-opt:has(input:checked) { background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-dsave-opt:has(input:focus-visible) { box-shadow: inset var(--ui-focus); }.ui-dsave-status { margin: 0; }.ui-dsave-lasts { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-dsave-what { display: grid; gap: 6px; min-width: 0; align-content: start; }.ui-dsave-what h4 { margin: 0; font-size: 14px; color: var(--ui-muted); font-weight: 500; }.ui-dsave-list { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-dsave-item { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 12px; padding: 8px 10px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-dsave-item b { font-weight: 500; }.ui-dsave-value { color: var(--ui-muted); }.ui-dsave-in[data-active] .ui-dsave-value { color: var(--ui-success); }.ui-dsave-count { margin: 0; color: var(--ui-muted); font-size: 13px; }@container (min-width: 600px) { .ui-dsave-in { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 20px; align-items: start; }}`let data_saver_toggle_changes = [ { label: "Pictures", off: "Full size", on: "Lighter versions" }, { label: "Video", off: "Plays by itself", on: "Waits for a press" },]// A choice as it should be.fn data_saver_toggle_mode(saved) -> str = saved == "on" || saved == "off" ? saved : "follow"// The choice from a Cookie header's text.fn data_saver_toggle_read(cookies) { if type_of(cookies) != "str" { return null } let found = split(cookies, ";") | map((c) => trim(c)) | find((c) => starts_with(c, "ui-data-saver=")) found == null ? null : data_saver_toggle_mode(slice(found, len("ui-data-saver="), len(found)))}fn data_saver_toggle_active(mode: str, phone) -> bool = mode == "on" || (mode == "follow" && phone == true)// For a route, before any script: whether to send the light version.fn data_saver_toggle_light(mode, save_data: bool) -> bool = data_saver_toggle_active(data_saver_toggle_mode(mode), save_data)// Whether it is on, and why, in a sentence.fn data_saver_toggle_status(mode: str, phone) -> str { if mode == "on" { return "Data saver is on." } if mode == "off" { return "Data saver is off." } if phone == true { return "Data saver is on, because this device's is." } if phone == false { return "Data saver is off, because this device's is." } "Data saver is off. This device does not tell apps about its own, so choose here."}fn data_saver_toggle_count(pictures: int, videos: int) -> str { let a = pictures == 1 ? "1 picture lighter" : `${pictures} pictures lighter` let b = videos == 1 ? "1 video stopped" : `${videos} videos stopped` pictures + videos == 0 ? "Nothing on this page has changed." : `On this page: ${join([pictures > 0 ? a : null, videos > 0 ? b : null] | filter((x) => x != null), ", ")}.`}fn data_saver_toggle_lasts(kept) -> str = match kept { null => "" false => "Cookies are blocked here, so this lasts until the page closes." _ => "This choice stays on this device."}component DataSaverToggle(saved = null, changes: list = data_saver_toggle_changes, on_change = null, legend: str = "Data saver", id: str = "data-saver", class: str? = null) { style global data_saver_toggle_css state mode = data_saver_toggle_mode(saved) state phone = null state kept = null state pictures = 0 state videos = 0 state ready = false state seen = false // something was added to the page, to look at in a moment state watcher = null let active = data_saver_toggle_active(mode, phone) // Goes over the page: what should be light is, and what should not be is put back. let sweep = (on: bool) => { let d = js("document") let imgs = js("Array.from", d.querySelectorAll("img[data-lite]")) let moved = 0 for img in imgs { if on { if !img.hasAttribute("data-full") { img.setAttribute("data-full", img.getAttribute("src") ?? "") if img.hasAttribute("srcset") { img.setAttribute("data-full-srcset", img.getAttribute("srcset")) img.removeAttribute("srcset") } img.setAttribute("src", img.getAttribute("data-lite")) } moved += 1 } else if img.hasAttribute("data-full") { if img.hasAttribute("data-full-srcset") { img.setAttribute("srcset", img.getAttribute("data-full-srcset")) img.removeAttribute("data-full-srcset") } img.setAttribute("src", img.getAttribute("data-full")) img.removeAttribute("data-full") } } let clips = js("Array.from", d.querySelectorAll("video[autoplay], video[data-autoplay]")) let held = 0 for v in clips { if on { if v.hasAttribute("autoplay") { v.setAttribute("data-autoplay", "") v.setAttribute("data-preload", v.getAttribute("preload") ?? "") v.removeAttribute("autoplay") v.setAttribute("preload", "none") v.pause() } held += 1 } else if v.hasAttribute("data-autoplay") { v.removeAttribute("data-autoplay") let was = v.getAttribute("data-preload") v.removeAttribute("data-preload") if was == null || was == "" { v.removeAttribute("preload") } else { v.setAttribute("preload", was) } v.setAttribute("autoplay", "") // A browser may refuse, with no press to go on; the video then waits for one. let p = v.play() if p != null { p.then((_r = null) => { null }, (_e = null) => { null }) } } } pictures = moved videos = held } let settle = () => { let on = data_saver_toggle_active(mode, phone) js("document").documentElement.setAttribute("data-data-saver", on ? "on" : "off") sweep(on) // Feeds add pictures as they go, so while on, the page is watched. if on && watcher == null { let w = js_new("MutationObserver", (_list) => { seen = true }) w.observe(js("document").body, { childList: true, subtree: true }) watcher = w } else if !on && watcher != null { watcher.disconnect() watcher = null } } if seen { every(120, () => { seen = false if data_saver_toggle_active(mode, phone) { sweep(true) } }) } let choose = (next: str) => { mode = next let d = js("document") d.cookie = `ui-data-saver=${next}; path=/; max-age=31536000; samesite=lax` kept = data_saver_toggle_read(d.cookie) == next settle() on_change && on_change(data_saver_toggle_active(next, phone), next) } <div class={ui_cls(["ui-dsave", class])} id={id} @mount={(_el) => { let d = js("document") let conn = js("navigator").connection let told = d.cookie let before = data_saver_toggle_read(told) if before != null { mode = before } phone = conn != null && type_of(conn.saveData) == "bool" ? conn.saveData : null kept = true settle() ready = true let turned = (_e = null) => { let was = data_saver_toggle_active(mode, phone) phone = type_of(conn.saveData) == "bool" ? conn.saveData : null settle() if was != data_saver_toggle_active(mode, phone) { on_change && on_change(data_saver_toggle_active(mode, phone), mode) } } if conn != null && conn.addEventListener != null { conn.addEventListener("change", turned) } () => { if conn != null && conn.removeEventListener != null { conn.removeEventListener("change", turned) } if watcher != null { watcher.disconnect() } d.documentElement.removeAttribute("data-data-saver") } }}> <div.ui-dsave-in data-active={active ? "" : null}> <fieldset.ui-dsave-set> <legend>{legend}</legend> <div.ui-dsave-opts> <label.ui-dsave-opt><input type="radio" name={`${id}-mode`} value="follow" checked={mode == "follow"} @change={() => { choose("follow") }}/>Follow device</label> <label.ui-dsave-opt><input type="radio" name={`${id}-mode`} value="on" checked={mode == "on"} @change={() => { choose("on") }}/>On</label> <label.ui-dsave-opt><input type="radio" name={`${id}-mode`} value="off" checked={mode == "off"} @change={() => { choose("off") }}/>Off</label> </div> <p.ui-dsave-status role="status">{ready ? data_saver_toggle_status(mode, phone) : ""}</p> <p.ui-dsave-lasts>{data_saver_toggle_lasts(kept)}</p> </fieldset> <div.ui-dsave-what> <h4>What it changes</h4> <ul.ui-dsave-list> {for c in changes => <li.ui-dsave-item key={c.label}> <b>{c.label}</b> <span.ui-dsave-value>{active ? c.on : c.off}</span> </li>} </ul> {ready && <p.ui-dsave-count>{data_saver_toggle_count(pictures, videos)}</p>} </div> </div> </div>}Related
StorageMeterSpace used by the app from navigator.storage, by kind, and a clear with a confirm.3 views · 6 downloadsAssetLookupScan or type an asset tag to see its record, history and what to do, with near matches and a report for a tag nobody knows.3 views · 7 downloadsConflictSheetThis device's version against the server's, chosen field by field, explained in plain words.2 views · 6 downloadsDraftKeeperKeeps what was typed on this device through a reload and a crash, offers it back, and clears on send.2 views · 7 downloadsErrorScreenA full screen for failure with the reason, retry, and offline told apart from server trouble.3 views · 8 downloadsGroupedListiOS-style inset grouped rows with headers, footers and disclosure chevrons, full-row tap targets, and dark mode.2 views · 7 downloads
Thread
Thread 0 posts
On DataSaverToggle: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.