Repository navigation
Sozo 3.2.1: Telegram-style desktop corner player, no endless loading, downloads and reconnect fixes - #46
Open
professorDeveloper wants to merge 37 commits into
Open
Sozo 3.2.1: Telegram-style desktop corner player, no endless loading, downloads and reconnect fixes#46professorDeveloper wants to merge 37 commits into
professorDeveloper wants to merge 37 commits into
Conversation
Each new episode folder started a prune without waiting. Two running side by side picked the same oldest folders; the second delete threw, and the catch around the whole loop gave up on the rest, leaving 25 of 24. That was the flaky preview_grid_test that turned master's CI red. Prunes are chained now, and each delete is guarded on its own.
On macOS, Windows and Linux the player can now dock into a small floating box in a corner of the app window. It keeps playing while the viewer moves around Home, Search, a detail page or anything else, because it lives in a layer above the Navigator (PlayerDockHost, in MaterialApp.router's builder). Nothing is handed over or restarted. /player now builds PlayerRoute, which gives PlayerPage a GlobalKey. Docking pops the route, and the same State moves into the host in the same frame. Expanding pushes /player again, and the route takes the key back. History ticks, auto-advance, subtitles, party sync and retries carry on as before, and closing the corner player disposes the page normally. - The corner box can be dragged, and on release it snaps to the nearest corner, taking the fling into account. Its inner edges resize it, between 240 px and 60% of the window. The corner and width are saved. - Hover controls: close, back to the full player, the title, previous and next episode, play/pause, 10-second steps when there is room, the time, and a timeline that seeks on click or drag. - I or the PiP button docks the player. Shift+I or a long press keeps the old whole-window always-on-top mini, which is still the only way to watch over other apps. - While docked, the keyboard stays with the page underneath. Clicking the corner player takes the keyboard. Esc or a click elsewhere gives it back without closing the video. I expands. - Opening another title closes the docked one first. Signing out, switching profile and onboarding close it too. - Fix: in the always-on-top mini window, regaining focus cleared _isPip and drew the full control bar into the 426-px window. Mobile and TV are unchanged. PlayerDock.supported is desktop-only, and the phone PiP paths are untouched.
'The source sent a web page instead of the video' failed whole episodes on vidapi and anikoto. Their CDNs serve real MPEG-TS segments as page-N.html / seg-N.html with Content-Type text/html; the player never reads that header and plays them, but both download engines refused them by it. - Kotlin and Dart engines read the first bytes and refuse only markup (<!doctype html, <html, <head, …); TS, fMP4, MKV, QuickTime and encrypted segments pass. A real web page fails once, without retries. - classify() no longer calls a network error 'web page' because a segment URL ends in .html. - A direct download is probed at tap time and refused there when it is a web page; a probe with no answer never blocks. - Mirrors under a WebView directive are sniffed instead of being taken as direct, and a locally proxied stream offers its upstream URL.
- libmpv reconnects a dropped HTTP/HLS stream itself, and a drop is only fatal once playback actually stops; then the player retries at the same position a few times before saying anything. 'Connection reset by peer' after two minutes no longer ends the episode. - Every server gets a start deadline (longer for torrents and WebView sniffing). Past it the player moves to the next server with a short notice, and when none plays it says so instead of 'Video loading…' forever. ExoPlayer's initialize() is bounded too. - Library error sentences are mapped to localized messages. - A labelled or remembered quality is pinned on ExoPlayer instead of ABR starting at the lowest rung, and Auto no longer claims the size it started at. - media_kit: the picture is forced to redraw once the texture exists, so a stream no longer stays black until the first seek.
MovieBox overrides only the paged search, and our host called the plain one, which CloudStream leaves as NotImplementedError. The host now falls back to the paged search (and the other way round).
… and the corner player; Continue Watching keeps up while docked
The tap-time probe failed the whole episode when the default mirror served a web page, even with a working mirror the choice sheet would have offered. Now only when every mirror does; otherwise the pages are dropped from the choice. The film page shows the web-page message instead of "play it first", and a stream played through the media-segment relay offers its upstream url for download rather than the 127.0.0.1 address that dies with the player.
…t once An end of file is judged before playhead movement: the cache carries the playhead past the drop, so a file the connection ended early always looked recovered and stopped silently mid-episode. The last tcp error is kept for that judgement. A live stream is no longer held through the 25 s grace once playing, and an end of a stream without length after a drop is fatal, so it reconnects. Torrents opened from torrent search get the torrent start budget.
Restoring, closing or opening another title while the left /player route was still animating out rebuilt the page there too: the same GlobalKey twice, or the page pulled into a dying route. Signing out and switching profile now close the corner player first, so its last history save lands before the account's rows are wiped instead of after.
Two exported chat transcripts and a Gradle problems report were committed by mistake, along with banner and screenshot images nothing links to any more. The transcripts stay on disk, now ignored; horizontal-banner.png stays because the v1.0-v3.0 release notes still show it.
professorDeveloper
force-pushed
the
release/3.2.1
branch
from
October 26, 2026 13:09
909dbc5 to
9078b64
Compare
Leaving Sozo while a video plays now shrinks the player into the mini window, which floats like Chrome's and Telegram's picture-in-picture: on every Space, beside other apps' full-screen windows, above floating panels, with no title bar, and with hover controls even while another app is in front. Coming back from the mini puts a docked player back in its dock.
mpv reports "Error decoding audio." for a single bad packet and carries on; the player treated it as fatal and showed an error over a playing video.
The glass capsule's tab Row follows the ambient direction, so in Arabic it laid the tabs out right to left while the package's tap, drag and indicator maths kept counting from the left edge. Every tap opened the mirror-image tab: Home went to Profile, Search to Downloads. Build the bar left to right in every locale and, in RTL, hand it the tabs reversed with the index and the callback mirrored. Home stays on the right in Arabic. liquid_glass_widgets fixed it the same way upstream after 0.18; upgrading across that major is not a patch-release change.
Brings the drawn glass from satashkent-staff, the same liquid_glass_renderer pin (0.2.0-dev.4) and the rules it was tuned under: - GlassGroup/GlassShape/GlassSurface: refraction where Impeller runs it, a blur under a thicker tint where it cannot, an opaque fill under Reduce Transparency. One recipe per kind, dark only, since Sozo has one theme. - Held to its tint while its route moves, while the keyboard moves, and while one of the phone's own views stands under it, where the shader would draw a dark block. - GlassSense (iOS, soplay/glass) reports Reduce Transparency; GlassPower (Android, soplay/glass_power) reports an Android Go phone, which draws the blur instead of the refraction. - MediaGlass: clear glass for controls over video, with the player's 0.45 black laid in it so a white glyph keeps 3:1 over a white frame. iPhone only; every other platform keeps the black plate it has. Nothing draws it yet.
The top bar, the centre transport and the bottom row now stand on MediaGlass instead of a black disc: the same black, laid in clear glass that bends the frame at its rim. Each row is one MediaGlassLayer, so the whole overlay reads the picture three times, not once per button. Android, TV and desktop draw exactly what they drew before.
- The lock pill stands on MediaGlass with the deeper 0.6 dimming its words need. - Locking and unlocking give a firm tap: the controls vanish with the lock, so nothing on screen confirms it. A double-tap seek ticks, since the ripple lands on the far side of the thumb. Phones only. - Locking was the same five-line lambda in two places; now _lock(). - A stall shows CupertinoActivityIndicator on iOS, as AVKit does over a stalled video; Material's ring everywhere else.
A download started from the episode list or the detail page went out with only the headers the source declared - usually just a Referer - so the Android service asked under Dalvik's agent and the in-process engine under dart:io's. Hosts that answer a bare client with a page failed those episodes as "a web page instead of the video" while the same episode played. The player's header rule now lives in mediaRequestHeaders and the transfer and the tap-time probe both use it.
A CloudStream series hands every season over in one list, numbered within each season. The app dropped the season and told episodes apart by number, so Lupin's seasons sorted into each other (Chapter 1, Chapter 6, Chapter 1) and S2E1 shared an id with S1E1. Episodes now keep their season; a run whose seasons share numbers is numbered through in season order (season 1 keeps its numbers), the list shows one season at a time behind season chips, rows and the player show S2 E1, and switching source matches season and episode.
Asked for in support. The pick is remembered on the device. Also drops an unneeded import from the glass test.
The tab asked only AniList and took its first search hit, so a film or a series either had no relations or showed an anime that shared a word with its name. A title that is not anime now gets the rest of its film series and TMDB's recommendations (through the backend proxy), and the AniList path takes a hit only when its title really matches.
The sheet listed every source that had the title and left the walk to the viewer: pick one, and when it had the show but not the episode, pick the next. Auto does the walk - corrections first, then the matches it trusts by score, each source as it answers - and opens the first one that plays.
… its playlist An HLS download is index.m3u8 and its segments, and the exporter copied the one file it was given: an 80 KB Baththa.m3u8 in Downloads/Sozo and the 655 MB film still in the app. The segments are now joined into one .ts, or .mp4 for fragmented MP4, with AES-128 decrypted. A separate audio playlist or SAMPLE-AES is refused with a reason rather than saved broken.
Only the paged mode could zoom. The list stops scrolling while two fingers are down, or a pinch whose fingers move up or down reads as a scroll.
…wering is a failure The card read "00:49 +1:25"; it now reads "00:49 / 02:07". Timeouts and refused opens were not counted, so a dead host left the card on a skeleton for good and the warmer walked two full passes over it. They count now, and the warmer stops after 6 in a row. The byte count of the scrub cache no longer drifts when a frame moves to the grid. A progressive file gets the same 15 s open and 6 s frame as HLS: a slow host's index took longer than 5 s, and a timeout discards what was read. The grid is not filled when a VTT arrives during the wait, and a download warms the full grid on mobile data. VTT cues may leave the hours out, the parser is testable, a sheet shows the skeleton while it loads, and the first two sheets are precached.
Whole streams never showed a picture, while the player was playing them. - Too little time. A first frame is a playlist, a segment and a decode, and 5 s was never enough for a 2.3 MB segment on 2 Mbit/s. Each try was abandoned and the next paid for a segment again. Now 18 s native, 20 s in Dart. - A lifted finger threw away the frame being fetched, so on a slow host no drag ever showed one. It now lands in the cache and the next drag finds it. - The retriever was released on every lift and read the file's index again each drag. It is kept for a minute, and an open that took long still counts. - A retriever that refuses a stream, hangs or only gives nulls now hands over to Media3's extractor, which also reads HLS that a source mislabelled. - A failed proxy registration or playlist read was cached for 20 minutes. - The grid gave up for good on a host that was down. It rests and tries again, and waits for the duration when the engine does not know it yet. - A sprite sheet that never answers falls back to the decoder. - The proxy now recognises a playlist served as text by its first bytes; its absolute segment addresses used to go out with no headers. Not checked on a device: the Kotlin changes are compiled, not run.
The flag means a probe refused the source. Providers do not probe, so it is usually absent, and reading absent as false left SourceLadder with nothing: the first mirror played, its failure ended on the error screen with the others untried, and the remembered quality was ignored. Real logs show one url retried until the error, on primesrc and on Coflix. CloudStream, Aniyomi, Mangayomi and Jellyfin always sent true, so only they ever walked.
Diagnostics were taken once, when the ticket opened, so "it still does not play" reached support with a log from before the attempt, or none. Only on a ticket that opened with diagnostics: the switch on the new-ticket screen stays the viewer's choice.
Past the first slice the track stayed with the rest of the file still in the source language, and no message.
The Hide button held the long no-connection sentence, and the message held "Hide sources that are down", in every language. Also a literal \u201c on screen in the TV link message.
vi.json is machine-written and not yet read by a native speaker. Flutter and the system fonts already cover Vietnamese, so nothing else is needed for it to show. Subtitle labels named Vietnamese, vie or Vietsub now match. A subtitle in the viewer's language beats the source's default track, and one-tap translate uses a human subtitle in the wanted language rather than machine-translating English.
…ect hard subs KKPhim splices a 30 s gambling ad into every title. A source can now name such segments, and the local proxy drops them from the playlist. Checked through the real proxy against a live title: 6073.0 s becomes 6042.8 s, and it plays across the join. VSMOV stores MPEG-TS behind real PNGs. The proxy finds the stream behind the picture and its padding, including when the player opens with Range: bytes=0-, as both engines do. Checked against a live title: FFmpeg reads 1080p H.264 and decodes it; without the proxy it reads a 0x0 PNG. __sozoCaps tells the served script this build can do it. A source can say which language is burned into the picture, so auto-translate does not draw a second line over it. Subtitle search says what the viewer reads, so the server can look further for it.
A Vietnamese capital carries two marks and reaches about 1.03 em above the baseline. Text cut off with an ellipsis is clipped to its line box, so at a height of 1.08 a long title lost them. Rendered with real Roboto: the Ẳ of a long banner title showed as Ă, which is another word, and at 1.25 it is whole. Only Vietnamese gets the extra room; other languages keep the design.
…#47) * Anime4K: Windows takes ';' between shader files, not ':' mpv's path lists are separated by ':' on Unix and by ';' on Windows, where a ':' is the drive letter's own. Joined with ':' there, a three-file chain arrived as one impossible filename — Cannot open file 'C:\…\Anime4K_Clamp_Highlights.glsl:C:\…_x2_VL.glsl': Invalid argument — and mpv took video output down with it, so the episode was a black screen behind a playback error. Windows only, and only for the viewers who had turned Anime4K on; found in a support log from one of them (ticket 6ae742f9), who reported it as "no image at all, just the retry error". The separator now lives in one named function next to the presets, where the filenames it joins are, and the tests cover both platforms and the single-shader case. * Local HLS proxy: answer a range request the way a player expects The proxy stands between the player and a CDN that only answers signed, transformed requests, and it was loose about the half of HTTP that media players lean on hardest: a HEAD was served as a GET, a ranged request could come back 200 where the caller asked for 206, Content-Range and Accept-Ranges were sometimes absent, and a rewritten playlist went out with no Content-Length. libmpv and ExoPlayer tolerate all of it. AVFoundation — which is what plays every stream on iOS, since resolvePlayerEngine() has no libmpv to offer there — is entitled not to. Twelve tests drive the proxy the way a player does: HEAD on a playlist and on a segment, "Range: bytes=0-", "Range: bytes=0-1", no range at all, the same segment twice, and an upstream that answers a ranged request with a whole file. Six of them failed before this change. What this is NOT: the cause of uzmovi not playing on iOS. The integration test added here plays the live uzmovi stream through the real resolve, the real proxy and the real native player on an iOS target and prints what the player says. It reaches readyToPlay with the correct 1:29:19 duration and plays past two seconds — with this change AND with the code as it shipped in 3.2.1. So these are a protocol-correctness debt paid off, and the iOS report still needs a log from the device that saw it. * Sources: stop calling a source down on somebody else's evidence Two witnesses report on a source and they do not have equal standing. The server sweeps every extension's site from a datacenter; this phone runs the source's own code over a residential address. For a Sozo cloud source those are the same question — the catalogue reaches the app through the backend either way. For a CloudStream, Aniyomi, Manga, Mangayomi or Jellyfin source they are not, and a datacenter's result says nothing about the phone's leg. In the live health report there were 930 extension verdicts, 229 of them "dead", and 96 of those were a TLS handshake, a timeout or an unreachable address — the three symptoms a site shows a datacenter and not a phone. Ninety-six sources wore a red Down badge that nothing had established on the device they actually run on. Those badges now wait for a check from here, which the row's long-press offers; a parked or non-resolving domain is a fact about the domain and still stands wherever it was found. Three more things the same screen was getting wrong: - An empty catalogue counted as health. A 200 with an empty list is the commonest shape of a blocked site — kisskh answers the backend exactly that way today — so zero items now reads as a problem. - "Hide down sources · 7" took four rows away: the count was tallied over every row of the tab while the hiding ran over the rows left after the scope and language filters, and the source in use is never hidden. One function now answers both from the same list. - The quick-switch sheet — the one place a reader goes to get OUT of a broken source — still drew badges and counted from the raw store, so it kept steering people away from sources that work here. It asks the same policy as the Sources screen now.
…ce gave up on (#48) * Local HLS proxy: never serve a playlist from a slice, never a wrapper raw Three gaps an adversarial review found in the range handling, each closed with a test that fails against the previous code. A playlist served at a path that does not end in .m3u8 — /playlist, master.txt — still had the client's Range forwarded, so a range-honouring host returned a 206 slice, the sniffer recognised #EXTM3U inside it, and the proxy rewrote and served that slice while declaring it to be the whole file. A player was handed a manifest cut off mid-segment-list and told its length was ten bytes. Once the sniffer knows it is holding a manifest, a partial body is dropped and the file fetched again with Range stripped, so the rewrite always works on the complete text. An image-wrapped segment answered a Range with a 200, the whole body and `Accept-Ranges: none` — the exact protocol error the comments here say AVFoundation abandons a stream over. The unwrapped payload now goes out through the same buffered path the rewritten playlists use: a real Content-Length, a correct 206 with Content-Range, and a 416 past the end. The Range is also kept off the upstream for image paths, because a host that honoured a two-byte probe would have returned two bytes of PNG signature and the transport-stream offset could never have been found. A short upstream used to kill a committed response mid-flight: a range of up to 24 MB — everything a player actually asks for — is collected before any header goes out, so a host that sends fewer bytes than it declared becomes a clean 502 instead of a connection that dies with a length it cannot reach. * A title a source gave up on says so, in the reader's language The backend has been telling us for a while and nobody was listening. A title the source has moved into its own app comes back from /contents/episodes with playable false, a reason code and an empty episode list, and a failed resolve carries the same code. The app threw the body away — `if (code == 422) return Failure(Exception('Video URL aniqlanmadi'))` — so whatever had happened, the reader got one technical sentence in Uzbek and a Play button that could not work. Now the three cases the backend distinguishes reach the reader as three different sentences, in their own language: the source has moved this title into its own app, the page has no player at all, or this is a serial and its episodes are elsewhere. No Play affordance on a title the source called unplayable, and the obvious next step — the same title from another source — is offered in its place. Three smaller things in the same screens: An empty catalogue drew no badge. kisskh answers the backend 200 with an empty list, which is the commonest shape of a blocked site, so the row appeared among the problems with nothing to say why. It has a badge of its own now, for a verdict this phone reached itself rather than a sweep's. A phone whose clock jumped forward lost its entire check table. Pruning on every write turned a temporary clock error into permanent data loss, where before those rows were merely hidden and came back when the clock was corrected. runsOnDevice read as id-or-key agnostic and was not: a `csp:` health key missed the CloudStream prefix and came back as a Sozo cloud source, which is the one answer that lets a datacenter's verdict stand over a phone's.
…and tests as a visible check (#49) * Local HLS proxy: never serve a playlist from a slice, never a wrapper raw Three gaps an adversarial review found in the range handling, each closed with a test that fails against the previous code. A playlist served at a path that does not end in .m3u8 — /playlist, master.txt — still had the client's Range forwarded, so a range-honouring host returned a 206 slice, the sniffer recognised #EXTM3U inside it, and the proxy rewrote and served that slice while declaring it to be the whole file. A player was handed a manifest cut off mid-segment-list and told its length was ten bytes. Once the sniffer knows it is holding a manifest, a partial body is dropped and the file fetched again with Range stripped, so the rewrite always works on the complete text. An image-wrapped segment answered a Range with a 200, the whole body and `Accept-Ranges: none` — the exact protocol error the comments here say AVFoundation abandons a stream over. The unwrapped payload now goes out through the same buffered path the rewritten playlists use: a real Content-Length, a correct 206 with Content-Range, and a 416 past the end. The Range is also kept off the upstream for image paths, because a host that honoured a two-byte probe would have returned two bytes of PNG signature and the transport-stream offset could never have been found. A short upstream used to kill a committed response mid-flight: a range of up to 24 MB — everything a player actually asks for — is collected before any header goes out, so a host that sends fewer bytes than it declared becomes a clean 502 instead of a connection that dies with a length it cannot reach. * A title a source gave up on says so, in the reader's language The backend has been telling us for a while and nobody was listening. A title the source has moved into its own app comes back from /contents/episodes with playable false, a reason code and an empty episode list, and a failed resolve carries the same code. The app threw the body away — `if (code == 422) return Failure(Exception('Video URL aniqlanmadi'))` — so whatever had happened, the reader got one technical sentence in Uzbek and a Play button that could not work. Now the three cases the backend distinguishes reach the reader as three different sentences, in their own language: the source has moved this title into its own app, the page has no player at all, or this is a serial and its episodes are elsewhere. No Play affordance on a title the source called unplayable, and the obvious next step — the same title from another source — is offered in its place. Three smaller things in the same screens: An empty catalogue drew no badge. kisskh answers the backend 200 with an empty list, which is the commonest shape of a blocked site, so the row appeared among the problems with nothing to say why. It has a badge of its own now, for a verdict this phone reached itself rather than a sweep's. A phone whose clock jumped forward lost its entire check table. Pruning on every write turned a temporary clock error into permanent data loss, where before those rows were merely hidden and came back when the clock was corrected. runsOnDevice read as id-or-key agnostic and was not: a `csp:` health key missed the CloudStream prefix and came back as a Sozo cloud source, which is the one answer that lets a datacenter's verdict stand over a phone's. * Android glass: decide on what the phone measures, not what it was sold as GlassPower asked ActivityManager.isLowRamDevice and nothing else — the flag a maker sets when the phone ships as an Android Go device. It was wrong in both directions: a 2019 mid-range phone with 6 GB and a pre-Vulkan driver carries no flag, drew the refraction and dropped frames; a current 4 GB phone whose maker sets it lost the glass for nothing. The channel now reports what can actually be read — total memory, API level, Vulkan 1.1 in the driver, the GL ES major, the panel's refresh rate, Battery Saver, the core count — and the decision is a pure function in Dart with the order of refusal and the reason for each written down beside it. A missing or malformed field keeps its generous default, so a half answer never takes the glass away. Battery Saver is the one reading that changes while the app runs, so it is pushed over the channel as it turns and re-asked on resume instead of being read once at launch and believed all evening. Behind the gate there is now a measurement: GlassStepDown watches each frame's whole span against the panel's own budget while a refraction is on screen, and drops to the blur for the rest of the run when a sustained run of frames misses. No flag a phone carries says whether its GPU can bend the screen sixty times a second; the frames it draws do. A warm-up, a sliding window and slack keep a shader compile or a decode from deciding, the verdict latches so glass does not come and go as pages scroll, and it never arms in a debug build, where the JIT and the assertions miss frames on the fastest phone made. Two drawing faults found on the way, both proven failing-first: BackdropGroup was never placed anywhere, so the blur path's BackdropGroup.of returned null every frame and every shape took its own full read of the screen behind its clip — three reads for a bar with a pill and a lens, where the same glass refracting takes one. A GlassGroup now provides one. The blur path dropped the recipe's saturation, which is the number that makes a poster still read as a poster through the bar. The phones that fall back to the blur were getting a grey version of a different design rather than a plainer version of the same one. The frame cost is reasoned, not measured: there is no Android device in this session, so what this removes from the frame is stated and what it is worth in milliseconds is not. * iOS: the system's own glass, available but not yet wired in The app approximates in a shader what iOS now draws itself. This adds the real thing — a UIKit platform view over UIVisualEffectView with the glass effect the running OS provides, a capability probe over the existing soplay/glass channel so Dart chooses from what the phone reported rather than from a version string, and a fall back to the system blur material on an OS without the glass. Deliberately NOT connected to GlassGroup. Nothing in the app renders it yet, so this commit cannot change a pixel of what ships; the integration point is a few lines in glass_surface.dart and a new GlassMode, and that file belongs to work in progress elsewhere. Wiring it is a decision to take with the tab bar in front of us, not a side effect of adding the capability. What a platform view costs here is written into the widget's documentation, because it decides where this is the right answer: it composites outside the Flutter layer tree, so it cannot be freely rotated, clipped or animated the way the shader can, it does not appear in Flutter goldens, and it brings its own hit-test and ordering rules. The conclusion recorded there is that the shader stays right for the moving pieces — the lens that follows a finger — and the system glass belongs on the still surfaces. Two things are open, and neither is hidden: - On an iOS 26+ phone Dart can still choose systemMaterial through the branch for a style the phone did not report, and the Swift side does not honour that choice. It needs a decision about which side owns the fallback. - Nobody has looked at a pixel. The corner is a layer cornerRadius with clipsToBounds, which masks the glass rather than shaping it, and that is the most likely way this looks wrong on screen once it is used. The integration test runs on a simulator and asserts the native side is really there: it was first written so that it passed with the view registration commented out, which is the easiest way for a test like this to be worthless, and it now fails in that state. * CI: make the tests a check of their own flutter test was the last step of the analyze job, so a pull request's check list said "analyze" and a reader could not tell whether the suite had run — and because an analyzer error ends the job early, a red "analyze" meant either the tests failed or they never started, one signal for two different situations. It misled me today on #47. The trade is in the file: a separate job pays for its own checkout and its own pub get, about a minute, in exchange for a check whose name is the claim it makes. * Glass gate test: use a recipe that is actually committed The blur-lift cases reached for GlassRecipe.androidBar, which exists only in the tab-bar work that has not landed yet. With that file in the working tree the suite passed here and the analyzer failed in CI, which is the worst version of this mistake: green locally, red where it counts, and for a reason that has nothing to do with what the test is about. The test needs a recipe that asks for a colour lift, and the regular glass is one (saturation 1.3). It says the same thing and compiles against what is in the branch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sozo 3.2.1
Fixes for every real problem reported in support since 3.2.0, plus the Telegram-style desktop corner player.
Desktop: corner player (Telegram-style PiP)
P) docks the running player into a small floating window in the corner of the app. You can drag it and resize it, and it stays on screen while you move between pages (home, search, details, settings).isDesktopPlatform, and Android native PiP works as before.Player: no endless "Video loading", recovers from dropped connections
initialize()is bounded and can no longer hang forever.Downloads: judge a web page by its bytes, not its Content-Type
text/html. Both the Kotlin foreground service and the Dart transfer engine now sniff the first 512 bytes and refuse only real markup.DownloadBuildFailure.webPage.CloudStream
PluginHostfalls back to paged search for plugins that only implement it (MovieBox threwNotImplementedError).Fixes from a pre-PR review
CI
preview_grid_testthat turned master red.Server-side (already deployed in sozo-backend, listed for context)
fayllar.comis signed./hindi/<slug>/to/<slug>/.m1.multimovies.courses) and new site theme; the iqsmartgames video chain behind it is unchanged.Support tickets covered
Tests
flutter analyze: cleanflutter test: 2387/2387 passedPluginSearchTest(6) passweb_page_sniff,transfer_engine,download_media_probe,download_request_builder,download_failure,download_choices_proxy,player_dock_geometry,player_dock_host,player_dock_source, plus the player readiness/retry testsWebPageSniffTestNotes for release
bump: patch(→ 3.2.1), then set the in-app updater in admin → AppVersions.