Add dive-count sort and favorites to the Add Buddy picker - #10
alpheios-one wants to merge 177 commits into
Conversation
Adds a decompression status filter to the Advanced Search page, so dives can be narrowed to deco/no-deco without relying on the existing "Technical" dive-type filter. Deco status has no stored column; it's derived from the recorded profile signal (dive_profiles.deco_type / ceiling and dive_profile_events), mirroring the classification already used by the Decompression Obligation statistic, so no schema migration is needed. Resolves submersion-app#642. Co-authored-by: alpheios-one <275321969+alpheios-one@users.noreply.github.com>
Buddies in the "Add buddy" sheet can now be sorted by number of shared dives (descending by default) instead of just alphabetically, and can be marked as favorites with a star toggle that pins them to the top of the list regardless of sort. Adds buddies.is_favorite (schema v161) with the usual onUpgrade/beforeOpen migration pair. Addresses submersion-app#638. Co-authored-by: alpheios-one <275321969+alpheios-one@users.noreply.github.com>
|
📦 Build artifacts for this PR · commit
Artifacts expire in 7 days. Downloading requires being signed in to GitHub. macOS needs two extractions: unzip the downloaded artifact, then unzip the Updated automatically on each push. |
|
Behebe die Fehler Nach Abschluss der Änderungen zwingend selbst prüfen: flutter pub get Falls ein Befehl aus Berechtigungsgründen nicht ausgeführt werden kann, |
|
Claude finished @alpheios-one's task in 18m 11s —— View job Working on it
|
|
Bitte in einer durchgehenden Session ohne Unterbruch abschliessen:
Falls ein Befehl aus Berechtigungsgründen nicht ausgeführt werden kann, dies explizit im Kommentar melden statt es stillschweigend auszulassen. |
Fixes the two formatting violations flagged by CI's dart format check; whitespace/line-wrap only, no behavior change. Co-authored-by: alpheios-one <275321969+alpheios-one@users.noreply.github.com>
|
Claude finished @alpheios-one's task in 12m 46s —— View job Working on it
|
Adds a DiveFilterState.weekdays axis (DateTime.weekday numbering) that ANDs
with startDate/endDate when both are set, mirroring every other filter axis.
Implemented across the in-memory apply(), the dive-list SQL builder, and the
statistics SQL builder via strftime('%w', ...) on the wall-clock-as-UTC
dive_date_time column. A new WeekdayFilterSelector widget renders locale-aware
weekday chips ordered by the diver's locale week start (Monday- or
Sunday-first) via MaterialLocalizations.firstDayOfWeekIndex, wired into both
the dive list's filter sheet and the Advanced Search page.
Resolves submersion-app#1234.
Co-authored-by: alpheios-one <275321969+alpheios-one@users.noreply.github.com>
The per-cell O2 mV toggle added for issue submersion-app#810 had no persisted default, unlike every other dive profile chart metric, so it always started off each session. Adds defaultShowO2CellMv (schema v161) following the same pattern as the other default-visible metrics, and surfaces it in the Gas Analysis Metrics group of Settings > Appearance > Dives. Fixes submersion-app#1235
Addresses PR review feedback: the summary subtitle hardcoded the total as 19 (18 before this PR), but DefaultVisibleMetricsPage renders 22 SwitchListTiles -- the list backing the count was missing showDecoStopsOnProfile and the total was never derived from it. Both now come from the same list so they cannot drift apart again.
The Media Library was the last selectable surface still entered by long-press. Its selection state was a bare Set<String> in mediaSelectionProvider, where "selection mode is active" meant "the set is non-empty" -- a model that cannot represent mode-on-with-nothing-checked, so an explicit Select control was not merely missing, it was unrepresentable. MediaLibraryView now owns a SelectionController, wrapped in SelectableListScope and pruned to the visible ids after every frame. mediaSelectionProvider is deleted rather than left as a second owner: two owners is the failure mode where the grid's exit and the controller's re-activation cancel out and strand the bar at "0 selected". MediaSelectionBar keeps all of its media-specific logic, including the dive-linked / site-linked id filtering that stops a bulk unlink latching retainInLibrary on rows that never carried the link, and renders it through the shared SelectionAppBar(shell: pane). The library therefore inherits Select All, Deselect All, Escape and Cmd/Ctrl-A to exit and select, Android back leaving the mode instead of popping the route, and Delete behind the overflow divider. It had none of these. The long-press callbacks are deleted, not unwired, per PR submersion-app#1021: re-adding the gesture means re-adding plumbing. Tiles gain isSelectionMode so unchecked thumbnails dim, which is what makes an empty-but-active mode visible, and the grouped list's dive headers go inert on the mode rather than on "something is checked" -- the two differ for exactly the first tap after Select. Toolbar note: the control row is all fixed widths, so a fourth button is spent budget. Three default-density icons plus the view-mode selector overflow by 16px at 320dp; the icons are compact for that reason and a fifth control will not fit. Behavior change worth calling out: unchecking the last item no longer drops the bar. A deliberate entry is the user's to end, so the bar stays at "0 selected" until it is closed. That is the app-wide rule the other 23 surfaces already follow. Tested: MediaLibraryView now runs verifySelectionContract, the app-wide contract it had never been held to. Suite 20,053 passed / 19 skipped / zero failures; analyze clean; dart format clean.
PR submersion-app#1247 landed on main while this branch was open and rewrote the same file, media_selection_bar.dart, with different semantics: after submersion-app#1244 every media row must hold a dive or a site link, so unlinking a singly linked row in the library IS deleting it. The three destructive affordances (Unlink, Unlink from site, Delete) collapsed into one confirmed Unlink, and unlink_metadata_warning_dialog was deleted along with the per-link id filtering that fed it. This branch's version of that file was built on the three-action model, so taking either side wholesale would have been wrong in a way that compiles. Resolved as main's semantics wearing this branch's chrome: main's _unlinkSelected, its folded-in metadata warning and its l10n keys, expressed through the shared SelectionAppBar. Two judgement calls in the resolution: - onDelete is null and Unlink is an ordinary BulkAction. Passing onDelete would render the baseline entry as "Delete", which submersion-app#1247 deliberately stopped calling it: the rows go, the source files do not. - maxInlineActions is 2 rather than the default 3, so Share and Move to dive hold the inline slots and Unlink is reached by open-then-choose. That keeps the shared bar's rule that a destructive action is never one tap away, which onDelete: null would otherwise have given up. media_selection_test.dart took main's version wholesale, with this branch's entry change re-applied on top: Select control instead of long-press, action keys instead of button labels, and the destructive action reached through the overflow.
Opens the next beta train after promoting v1.7.5.6772; the App Store closes a version train on release, so betas cannot continue on the promoted marketing version.
Promote run 32779335838 published 1.7.5 and submitted it on macOS, but the iOS submit lane logged "1.7.4 is already in review; skipping submission of 1.7.5" and exited green. App Store Connect showed 1.7.4 as Rejected, not in review. Both readings were right. Rejection closes the review but not the review SUBMISSION: it stays open in UNRESOLVED_ISSUES, which is one of the three states fastlane's get_in_progress_review_submission matches. The guard's version-agnostic first check read that as "Apple is holding a build" and short-circuited, so ALREADY_SUBMITTED_STATES, which deliberately omits every rejected state because "they all need a fresh submission, which is exactly what this lane is for", never got to run. The two halves of the guard disagreed about rejection and the half that never sees a version string won. Relaxing that check alone is not enough. deliver runs the identical lookup in create_review_submission and calls user_error! on any hit, but only after it has renamed the editable version and uploaded metadata, so the lane would have traded a clean skip for a red run and a half-written version. The rejected submission has to be retired first. So: read the submission state, treat only an explicit UNRESOLVED_ISSUES as non-blocking, cancel that dead submission, and poll until App Store Connect stops reporting it before going on to the editable-version check. WAITING_FOR_ REVIEW and IN_REVIEW still block exactly as before; that protection is what run 31903095751 bought and it is untouched. Unknown fails closed. Only a literal UNRESOLVED_ISSUES lowers the guard or triggers a cancel, so an unreadable state, an empty one, or a state Apple adds later all keep the old blocking behaviour. A cancel that does not take effect blocks too, rather than falling through into deliver's error. Note what this changes operationally: a rejection used to force a human to look before anything was resubmitted. The promote job now resubmits on its own, which matches Play and the appcast, and puts the "did we actually fix the rejection?" judgement entirely on the release owner. The guard test grows fakes for submission state and cancellation, five decision cases and four wrapper cases. Reverting just the new condition reproduces run 32779335838's message verbatim, on both platforms.
…budget Review round on PR submersion-app#1251. Schedule the pruning callback only while selection mode is active. The dive media section schedules it unconditionally and this branch copied that, but the two surfaces are not comparable: pruneTo builds a lookup over the whole visible set, and the library pages through thousands of rows where a dive holds a handful. The guard cannot skip a prune that mattered, because state.entries comes from a watched provider (so every change to it runs this build) and entering the mode starts from an empty checked set. Selection changes never reach this build at all; the ValueListenableBuilder owns those. Proven still live by inverting the guard and watching the contract test's prune step fail 3 against an expected 1. Correct the toolbar budget comment, which said "a fourth control does not fit" while grid mode was already showing four controls that do. It meant a fourth ICON BUTTON. Measured the real figures rather than restating the estimate: the row requires exactly 312dp (fits at 312, overflows by 4 at 308), so the slack at 320dp is 8dp, compact density reclaims exactly 8dp per button (3 default-density buttons need 336), and a fourth compact button would overflow by 32dp.
…-1.7.6-123 chore: bump version to 1.7.6+123
…-default-visibility Add persisted default for CCR O2 cell visibility
…ary-selection-pattern feat(media): bring Media Library selection onto the shared pattern
…ed-review-submission-guard fix(release): resubmit after a rejection instead of skipping
`UpdateChannelConfig.isAutoUpdateEnabled` returned false on Android on the grounds that Android is a store-only platform, but Submersion is not on Play. Sideloaded APK installs therefore had no banner, no check button and no version comparison, so a diver stayed on whatever build they first installed. That is how the launch failure fixed in 1.7.5.6772 could not reach the phone reported in submersion-app#1256: the fix shipped, but nothing on the device could see it. The guard conflated two questions: can this platform self-install a binary, and does a store already deliver updates here. iOS answers yes to the second and keeps its guard. Android answers no to both, exactly like Linux, and now follows the compile-time UPDATE_CHANNEL like every other platform. No build change is needed: build-all.yml already builds the APK with UPDATE_CHANNEL=github and the Play bundle with UPDATE_CHANNEL=playstore, so the switch-over when Play lands is a build flag rather than another code change. This is also what the original design called for; the table in docs/plans/2026-02-14-auto-update-design.md has always listed the Android APK as GitHub-updated. Nothing else was needed to make it work. The Android branch of the GitHub updater was already written and unreachable, every release already publishes a matching Android.apk asset, and GithubUpdateService installs nothing: the banner's Download action opens the APK URL externally, which is the same flow a diver used to install the app in the first place. So there is no self-install machinery and no REQUEST_INSTALL_PACKAGES permission here. The beta channel comes along with it, deliberately. beta.yml publishes the Android APK to beta-builds alongside the desktop artifacts and those releases are not marked prerelease, so /releases/latest resolves them. The dead "Join the Beta" tile is fixed by the same split. It renders only when auto-update is off, so the sideloaded APK no longer offers the Play opt-in page for an app that is not listed on Play. Its platform mapping moves out of the settings page into betaEnrollUrlFor(), next to the constants, so that condition is covered by a test rather than living inside a private getter on a 3,000-line page. Both new rules are pure functions taking the platform as an argument, because Platform.isIOS is a host fact under flutter test and a getter reading it can only ever exercise one branch.
The Detailed site list rendered depths in meters regardless of the diver's
unit setting, while the Table view rendered them correctly.
SiteListTile._depthString was a getter on the widget class rather than the
State, so it had no access to ref and hardcoded the "m" suffix with no
conversion. The Table view was already correct because it formats through
SiteFieldDescriptor.formatValue, which receives a UnitFormatter.
Moving the getter into the State fixes both the Detailed view and the site
search delegate, which reuses the same tile.
The active depth-filter chip on the same screen had the same defect, and
fixing it surfaced a second, silent bug: the filter sheet's depth inputs
were suffixed "m" and fed straight into a comparison against the
meter-valued site depths. An imperial diver typing 100 was filtering at
100 meters, not feet. The bounds now stay in meters internally, matching
the values they are compared against, and convert only at the input and
display edges.
The three depthRangeXxx ARB strings carried a hardcoded unit token, which
is now dropped in every locale so the symbol comes from the formatter.
Localized lead-ins ("Up to", "Bis zu", "Fino a") are preserved. Arabic and
Hebrew previously rendered their own meter abbreviations in these chips and
now use m/ft, matching every other depth display in the app.
Site Features previously rendered directly under the map, above Basic Info, which pushed the site's identity and core stats below the fold. It now sits after the Depth and Altitude sections and before Tide. The section had been nested inside the map's site.hasCoordinates spread and shared that guard. Moving it out gives it its own hasCoordinates check, which it still needs because its add action opens the fullscreen scape and that has nothing to render without a location.
Release builds already carry this permission. The manifest merger unions it in from a library manifest, and apkanalyzer confirms it on the shipped v1.7.5.6772 APK, so this is not a behaviour change and the update check was never going to fail for want of it. Declaring it explicitly removes a dependency nobody chose: the only pub package in the tree whose manifest supplies INTERNET is google_sign_in_android, which is transitive via google_sign_in. An app whose core features are cloud sync, media upload and update checks should not inherit its networking permission from a sign-in plugin it might one day drop. Raised in review on submersion-app#1261.
…22-2125 # Conflicts: # lib/features/dive_log/data/repositories/dive_repository_impl.dart # lib/features/dive_log/domain/models/dive_filter_state.dart # lib/features/statistics/data/dive_filter_sql.dart # test/features/dive_log/domain/models/dive_filter_state_test.dart # test/features/statistics/data/dive_filter_sql_test.dart
main took v161 for diver_settings.default_show_o2_cell_mv (issue submersion-app#1235) while this branch was open, so buddies.is_favorite moves to the next free rung. v165, v166 and v167 are claimed by PRs submersion-app#1290, submersion-app#1300 and submersion-app#1276.
…ssue-386-sac-lmin-tank-volume fix(sac): make L/min SAC reachable on dive-computer downloads (submersion-app#386)
Both collapsible section headers laid out `trailing` inflexibly, so RenderFlex gave it its full natural width before the Expanded title got anything. On a full-width card there was always room to spare and nobody noticed. Halve the card and there is not: the Tide header's cycle range measured 384px against a 436px row, the title collapsed to zero width and rendered "Tide" as a column of single letters, and the row still overflowed. Wrap the trailing widget in Flexible so it can only claim half the free space, and in an Align so it stays flush right whenever it fits inside that cap. A full-width card lays out byte-for-byte as before; only a card too narrow for its own header behaves differently, and there the title survives. No test of its own: the half-width case this guards only exists once cards are paired side by side, which the following commit adds along with the tests that exercise it.
Details and Environment already sit side by side on a wide pane. Do the same for two more pairs that read as one thought: the surface fixes next to the tide they were taken on, and the cylinders next to the lead that offsets them. The existing rule could not express either pair. It required the two halves to be immediately adjacent in the diver's configured section order, and neither pair was: Water Conditions sat between Tide and Surface GPS, and Buoyancy between Weights and Cylinders. Buoyancy is the harder case, because it renders exactly when the dive has cylinders -- the condition for a Cylinders card to exist at all. Under adjacency the pair was unreachable, not merely unlucky. So pairing now looks ahead. A pair forms whenever both halves are visible and both have content, wherever they sit; the row renders at the slot of whichever half comes first and the section between them drops below. Left and right come from the pair table rather than the configured order, so a diver whose saved order predates a pair still gets Surface GPS and Cylinders on the left instead of a mirrored layout. When either half has nothing to show, both render full-width in their own slots exactly as before. The lookahead replaces adjacency for all four pairs rather than only the new ones. Two pairing rules in one loop is a trap for whoever adds the fifth pair, and the case adjacency protected -- a diver who deliberately parked a section between Details and Environment to break the pair -- is indistinguishable from a diver who simply never touched the old default order. The pairs themselves move into a const table so a fifth is a one-line addition rather than another branch in a 5,477-line page, and the default order is reshuffled to list each pair's halves together. Existing saved orders are untouched and pair anyway, which is the whole point of the lookahead. Splitting the tide card out of its section wrapper gives the pair a bare card to place and a null to gate on from one code path, so the section and the pair cannot disagree about whether there is tide data to show.
…dows (submersion-app#1304) Switching the sync backend forces a full base publish, and on Windows every attempt died with: PathNotFoundException: Cannot copy file to '...\submersion/sync_base_publish/Temp/ssv1_base_<dev>_1.<uuid>.json' (OS Error: Das System kann den angegebenen Pfad nicht finden, errno = 3) Two halves, neither visible on POSIX: 1. Sync assembles paths by interpolating a literal '/', so on Windows the export path mixes separators: C:\Users\x\AppData\Local\Temp/ssv1_base_<dev>_1.<uuid>.json 2. ChangesetWriter._recordResumable took the filename with base.path.split(Platform.pathSeparator).last, which on Windows splits on '\' only. The last segment was therefore 'Temp/ssv1_base_....json', and the move target became <publishDir>/Temp/<name>: a subdirectory nothing ever creates. The rename failed and the copy fallback threw uncaught. Both reconstructed strings match the reported error character for character. Extract basePublishTargetPath, which uses p.basename and p.join. Under the Windows style basename treats BOTH separators as separators, so either path shape resolves. Also switch the two path origins (exportBaseToTempFile and resolveBasePublishDir) to p.join so the mixed shape stops being produced. On macOS and Linux Platform.pathSeparator is '/', so the malformed path splits correctly by luck and the existing ChangesetWriter resume integration tests pass either way. The regression tests therefore pin p.Style.windows explicitly via the helper's injectable p.Context, which is the only reason that seam exists. No migration is needed: the copy threw before creating anything at the target, and the orphaned export sits in the OS temp dir that deleteLeftoverBaseTempFiles already sweeps.
Review catch. The leading spacer was decided from `pair.left`, a fixed property of the pair, rather than from the half whose slot the row actually lands in. Those are the same section only while the diver leaves the left half ordered first. Order Environment above Details and the row renders in Environment's slot but inherits Details' exemption from the 24px gap, butting against whatever precedes it. Key the decision off the loop's `id` instead. Details keeps its exemption because it is the section that emits no gap of its own, which is a fact about that section's slot, not about which side of a pair it sits on. The test pumps the same dive under both orderings and asserts the row sits 24px lower when Environment leads. It needs an explicit teardown between the two pumps: layering a second ProviderScope over a live one keeps the existing SettingsNotifier, so the second section order never reaches the page and both measurements come back identical -- which reads exactly like the bug being absent.
…Search Codecov reported 77.33% patch coverage on this PR. The 17 cold lines were all callback bodies and collection-if subtrees that no test drove: the weekday chips' onChanged, the conditional "Clear weekdays" affordance and its onPressed, the date-section auto-expansion for a weekdays-only filter, and the Monday-first branch of the locale week-start conversion (en_US is Sunday-first, so the existing selector tests could only ever reach the other branch). - weekday_filter_selector_test.dart: a German-locale case that asserts the chip row starts on Monday, guarded by an assertion on firstDayOfWeekIndex so the test cannot pass for the wrong reason. - dive_filter_sheet_weekday_test.dart: seeded selection renders and survives Apply, tapping a chip adds its weekday, Clear weekdays empties the axis. - dive_search_page_weekday_test.dart: a weekdays-only filter auto-expands the date section, chip + Search writes the weekday back, Clear weekdays drops the axis, and Clear All wipes a seeded selection. Patch coverage is now 100% (75/75 instrumented added lines). No production code changed.
At desktop width the GPS log page hosts the overview map beside its track list via MapListScaffold; rows select on the map and the info card opens the track. A summary strip (tracks, recorded time, dives covered) tops both layouts, the empty state explains the feature, and an empty basemap replaces the blank map pane. Shared list tile, stat tile, date filter action and overview map widgets keep the logger page and the track map page from drifting apart.
Adds gpsLogger_summary_tracks, gpsLogger_summary_recordedTime and gpsLogger_summary_divesCovered to all locales and regenerates the localization classes.
Espanol, Francais and Portugues were stored with their diacritics stripped, while Deutsch, Magyar and the Chinese, Arabic and Hebrew entries were not. The list is now shown in the place name language picker as well as the app language page, so the misspellings are twice as visible.
…claude.yml Addresses the three Copilot review findings on PR submersion-app#1237. Sort toggle direction: text fields invert direction throughout this codebase, so SortDirection.descending is what renders A to Z (buddySortProvider on the standalone buddy list already defaults to name + descending for that reason). The picker's new toggle asked for ascending, which landed on the inverted branch and rendered Z to A. Three widget tests now pin the rendered order for the default sort, the toggled sort, and the toggle back. setFavorite phantom sync records: the update wrote unconditionally and then marked the record pending even when no row matched, leaving a sync record pointing at a buddy that does not exist. Drift's write() returns the affected row count, so the method now returns early on zero. toggleFavorite already guarded this with its read-before-write; both paths now have a regression test asserting sync_records stays empty for an unknown id. Removed .github/workflows/claude.yml: an issue_comment-triggered job with contents: write and pull-requests: write, gated only on the comment body containing "@claude", lets any commenter drive privileged automation with the base repo's secrets.
…ssue-1304-windows-base-publish-path fix(sync): keep base publishes off a phantom Temp subdirectory on Windows (submersion-app#1304)
GpsTrackOverviewMap returned nothing until at least one track had two decoded fixes, so on a cold cache the map pane was blank for as long as the decode and simplify took. The basemap now mounts at a world view straight away and frames the tracks when their geometry lands, from onMapReady if it arrived before the map was ready and from the signature path otherwise.
The Decompression Obligation bar sets `value` to the deco share, so it paints orange from the leading edge and leaves the no-deco remainder in green. The legend below it listed green "No Deco" first and orange "Deco" last, putting each word at the opposite end from the segment it named. Swap the two labels so the orange one leads with the orange fill. Row and LinearProgressIndicator are both direction-aware, so the pairing holds in RTL locales. The bar's Semantics label was already correct and is unchanged. Adds a widget test that derives the invariant rather than hardcoding a side: it reads valueColor off the rendered indicator and asserts the legend label carrying that color sits at the leading edge.
The deco "Any" chip borrowed diveSites_filter_difficulty_any, which is translated in the context of a difficulty scale (Arabic renders it "any level"). Add a dedicated diveLog_search_filter_any so the Advanced Search tri-state chips have a home of their own, translated in all ten non-English locales with the values already shipping for the app's other generic filter "Any", and regenerate app_localizations_*.dart.
…ked views The deco axis was evaluated in memory by DiveFilterState.apply(), but getAllDives deliberately skips profile hydration for list views, so dive.profile is always empty on that path and deco-stop events never reach the entity at all. _matchesDecoFilter therefore computed neither deco nor no-deco and returned false for both polarities: turning the filter on emptied the dive table view, the dive activity map and the heat map. The existing tests missed it by hand-building Dive objects with a populated profile. Route the axis through SQL everywhere instead: * DiveRepository.getDiveIdsWithDecoSignal resolves the matching ids with the same decoSignalCondition the paginated list and Statistics use, so all three classify dives identically, decoStopStart events included. * decoFilteredDiveIdsProvider (keyed on the wanted polarity, so a Yes/No flip cannot reuse the other polarity's cached ids) wraps it, and filteredDivesProvider intersects. It is only built while the filter is active, keeping the dive_profiles scan off the default list load. * Drop _matchesDecoFilter and document decoOnly as the one axis apply() deliberately does not evaluate. Tests: deco_filter_providers_test.dart drives filteredDivesProvider through the real DB for both polarities, a polarity flip and a deco+date combination with an event-only dive in the fixture; the repository test covers all five signal shapes plus diver scoping and pins the premise that getAllDives leaves profiles unhydrated.
…ives libdivecomputer fires DC_SAMPLE_PRESSURE once per air-integrated transmitter, so one profile sample can carry a reading for several tanks. The wrapper accumulated a single pressure/tank pair per sample, so the last transmitter overwrote every earlier one. On a CCR dive with an O2 and a diluent transmitter the O2 tank kept only the readings taken while the diluent was out of comms, which on the reported dive was none at all, and the chart drew it as a flat "(est.)" line between start and end pressure. Record each reading against its own tank in a per-sample array and carry it through to tank_pressure_profiles. pressure/tank are unchanged: they feed the single dive_profiles.pressure column, which has no tank index. The download and re-parse paths now share one grouping helper so they cannot drift apart. Verified against the reporter's raw records: the tank that stored 0 of its 2142 readings now stores all of them, matching their Shearwater Cloud export exactly. Fixes submersion-app#1223
…ps-log-page-redesign feat(gps-log): map-first desktop layout, summary strip, and empty state
…ssue-1187-site-geocoding feat(sites): town, body of water and a synced place name language from coordinates
…ive-detail-more-pairs Pair Surface GPS with Tide and Cylinders with Weights on wide dive-detail panes
…eco-legend-labels-swapped fix(statistics): pair the Deco / No Deco legend labels with their bar segments
…-5-20260822-2125 Feature: Advanced Search - Deco yes / no submersion-app#642
…-12-20260823-2019 Add weekday filter to Advanced Search, combinable with date range
…ssue-1223-transmitter-pressure fix(dive-computer): keep every transmitter's pressure on multi-tank dives
# Conflicts: # lib/core/database/database.dart

Summary
Addresses submersion-app/submersion issue 638.
Test plan
Note - this PR was authored in an environment without flutter or dart CLI access, so the checks above could not be run locally. Please verify via CI.