import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/views/helpers/constants/design_constants.dart'; import 'package:miler/views/helpers/constants/miler_type.dart'; /// ───────────────────────────────────────────────────────────────────────── /// OFFLINE, WITHOUT TAKING THE JOB AWAY /// /// ── What this replaces ── /// /// Losing signal used to lay an opaque full-screen page over the entire app: /// `Stack(children: [baseChild, const OfflinePage()])`. Not a warning — a /// replacement. /// /// A rider standing at a doorstep in a basement, a lift, a stairwell or a rural /// lane — which is where Indian first-mile delivery actually happens — lost the /// address, the customer's name, their phone number, the parcel count and the /// OTP field. He was holding a parcel and could not see who it was for. The app /// then told him it was "retrying automatically every 5 seconds", which is not /// something he can act on while a customer waits. /// /// ── Why a strip is the right shape ── /// /// Everything the rider needs at the door was already fetched. Connectivity is /// needed to *report* the stop, not to work it. So the right shape is a line of /// text, not a screen. /// /// ── What it must not say ── /// /// It said "Changes will sync when you're back online". **There is no mutation /// queue in this app** — no outbox, no replay, no retry loop. A status the /// rider sets with no signal is attempted once and fails, and nothing ever /// sends it again. The banner was promising a mechanism that does not exist, /// to the one person who would be blamed when the hub had no record of the /// stop. /// /// The copy now says what is actually true: he can keep working the stop, and /// the *status* needs signal. Building the queue is a real piece of work and a /// separate one (see the release report); until it exists this line must not /// imply it. /// /// The full-screen page survives for the one case where it is truthful: a cold /// start with no cached session, where there genuinely is nothing to show. /// /// ── Sizing ── /// /// Deliberately thin, and it does **not** overlay the content — [OfflineScope] /// gives it its own row above the app, so it can never cover a field the rider /// is typing into. The one thing worse than a blocker is a floating bar sitting /// on top of the OTP box. /// ───────────────────────────────────────────────────────────────────────── class OfflineBanner extends StatelessWidget { const OfflineBanner({super.key}); @override Widget build(BuildContext context) { return Semantics( liveRegion: true, label: 'Offline. Status updates need a connection. Stop details are still available.', excludeSemantics: true, child: Material( color: ColorConstants.warning, child: SafeArea( bottom: false, child: Padding( padding: EdgeInsets.fromLTRB(16.w, 7.h, 16.w, 7.h), child: Row( children: [ Icon( LucideIcons.cloudOff, size: 15.sp, color: ColorConstants.onAccent, ), SizedBox(width: 9.w), Expanded( child: Text( // States what is true and what he can still do. It must // not promise a sync — nothing in this app replays a failed // mutation. See the class note. 'Offline · Status updates need a connection', maxLines: 2, overflow: TextOverflow.ellipsis, style: MilerType.micro.on(ColorConstants.onAccent).semibold, ), ), ], ), ), ), ), ); } } /// Puts [OfflineBanner] above the app rather than over it. /// /// A `Column` rather than a `Stack` on purpose: the strip takes its own height /// out of the layout, so nothing underneath is ever covered. The app simply /// starts a few points lower while the connection is down. class OfflineScope extends StatelessWidget { final Widget child; final bool offline; const OfflineScope({super.key, required this.child, required this.offline}); @override Widget build(BuildContext context) { return Column( children: [ AnimatedSize( duration: DesignConstants.motionState, curve: Curves.easeOutCubic, alignment: Alignment.bottomCenter, child: offline ? const OfflineBanner() : const SizedBox(width: double.infinity), ), Expanded(child: child), ], ); } }