Skip to content

Comments

UI redesign#211

Open
DennisMoschina wants to merge 15 commits intomainfrom
ui-redesign
Open

UI redesign#211
DennisMoschina wants to merge 15 commits intomainfrom
ui-redesign

Conversation

@DennisMoschina
Copy link
Collaborator

  • redesigned the entire UI of the app
  • improved usability
  • added Settings page

@github-actions
Copy link
Contributor

github-actions bot commented Feb 20, 2026

Visit the preview URL for this PR (updated for commit 9b73f99):

https://open-earable-web--pr211-ui-redesign-bgq4vyjd.web.app

(expires Fri, 20 Mar 2026 15:30:26 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: c7397c11177c71d8d81172cea9365829823fb41c

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Redesigns the app UI to a more modern Material 3 look-and-feel, improves usability across device/app flows, and adds Settings navigation/pages.

Changes:

  • Reworked core screens (apps, devices, FOTA) into card-based layouts with new pills/badges/toasts/banners.
  • Added new reusable UI components (theme, toasts, prompts, status pills, wearable icons) plus improved routing.
  • Updated device/sensor plumbing (shared sensor streams, caching/status resolution, auto-connect preferences).

Reviewed changes

Copilot reviewed 60 out of 94 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
open_wearable/lib/widgets/global_app_banner_overlay.dart Switches global banner layout to stacked/dismissible vertical banners.
open_wearable/lib/widgets/fota/stepper_view/firmware_select.dart Redesigns firmware selection UI into a tappable card with status chip.
open_wearable/lib/widgets/fota/logger_screen/logger_screen.dart Migrates logger screen to platform scaffold and improves loading/error layout.
open_wearable/lib/widgets/fota/fota_verification_banner.dart Reworks FOTA verification banner to a styled stacked banner with deduping and deadlines.
open_wearable/lib/widgets/fota/firmware_update.dart Rebuilds firmware update flow UI, adds guarded back navigation and toast warnings.
open_wearable/lib/widgets/devices/wearable_icon.dart Adds reusable wearable icon widget with stereo-side resolution.
open_wearable/lib/widgets/devices/stereo_position_badge.dart Adds stereo position badge widget.
open_wearable/lib/widgets/devices/device_status_pills.dart Introduces reusable device metadata “pills” (battery/FW/HW/side).
open_wearable/lib/widgets/devices/device_detail/stereo_pos_label.dart Replaces bespoke stereo label with new badge component.
open_wearable/lib/widgets/devices/device_detail/status_led_widget.dart Redesigns status LED controls UI and adds “disable LED” behavior.
open_wearable/lib/widgets/devices/device_detail/rgb_control.dart Restyles RGB control button to outlined icon button.
open_wearable/lib/widgets/devices/device_detail/microphone_selection_widget.dart Redesigns microphone selection UI with loading/apply states and error handling.
open_wearable/lib/widgets/devices/battery_state.dart Replaces simple battery UI with cached/primed/live-updating badge component.
open_wearable/lib/widgets/common/no_devices_prompt.dart Adds reusable “no devices connected” prompt widget.
open_wearable/lib/widgets/common/app_section_card.dart Adds a reusable titled section card widget.
open_wearable/lib/widgets/app_toast.dart Introduces toast abstraction backed by AppBannerController / SnackBar fallback.
open_wearable/lib/widgets/app_banner.dart Restyles AppBanner and adds icon/foreground customization.
open_wearable/lib/view_models/wearables_provider.dart Improves wearable naming, stereo grouping preferences, and time sync messaging.
open_wearable/lib/view_models/sensor_recorder_provider.dart Switches recorder to shared sensor streams to avoid multiple subscriptions.
open_wearable/lib/view_models/sensor_data_provider.dart Adds shared stream usage + “silent sensor” handling to keep time window moving.
open_wearable/lib/view_models/sensor_configuration_storage.dart Improves config file listing robustness and adds scoped-key utilities.
open_wearable/lib/view_models/sensor_configuration_provider.dart Adds pending-change tracking and richer restore result semantics.
open_wearable/lib/view_models/app_banner_controller.dart Extends banner controller API with hide-by-key.
open_wearable/lib/theme/app_theme.dart Adds a new Material 3 theme with consistent shapes/typography/colors.
open_wearable/lib/router.dart Adds settings/recordings routes, improves home tab parsing, and unsupported FOTA redirect UX.
open_wearable/lib/models/wearable_status_cache.dart Adds cache for stable wearable metadata (position/FW/HW/support).
open_wearable/lib/models/wearable_display_group.dart Adds grouping/ordering logic for stereo wearables, with firmware compatibility gating.
open_wearable/lib/models/sensor_streams.dart Adds broadcast stream sharing for sensors.
open_wearable/lib/models/device_name_formatter.dart Adds display name normalization (e.g., BCL → OpenRing).
open_wearable/lib/models/bluetooth_auto_connector.dart Reworks auto-connect flow to use preferences + avoids duplicate connections.
open_wearable/lib/models/auto_connect_preferences.dart Adds SharedPreferences-backed auto-connect enablement & remembered names.
open_wearable/lib/models/app_shutdown_settings.dart Adds persisted app shutdown/live graph settings via SharedPreferences.
open_wearable/lib/models/app_launch_session.dart Adds session counter to track app-launch flows.
open_wearable/lib/models/app_background_execution_bridge.dart Adds method-channel bridge for iOS background execution during shutdown.
open_wearable/lib/apps/widgets/select_earable_view.dart Redesigns wearable selection UI, adds compatibility filtering.
open_wearable/lib/apps/widgets/apps_page.dart Redesigns apps page UI and gates apps by connected compatible devices.
open_wearable/lib/apps/widgets/app_tile.dart Rebuilds app tile UI with supported-device chips and disabled states.
open_wearable/lib/apps/widgets/app_compatibility.dart Adds helper functions for app/device compatibility checks.
open_wearable/lib/apps/posture_tracker/view/settings_view.dart Redesigns posture tracker settings UI with validation and calibration card.
open_wearable/lib/apps/posture_tracker/view/posture_roll_view.dart Restyles roll visualization and plugs in new ArcPainter styling.
open_wearable/lib/apps/posture_tracker/view/arc_painter.dart Refactors ArcPainter to parameterize colors/strokes and improve repaint logic.
open_wearable/lib/apps/posture_tracker/model/earable_attitude_tracker.dart Updates sensor config application to avoid marking pending options.
open_wearable/lib/apps/heart_tracker/widgets/rowling_chart.dart Improves rolling chart normalization/axis rendering and adds empty state.
open_wearable/lib/apps/heart_tracker/model/open_ring_classic_heart_processor.dart Adds OpenRing classic HR/HRV processing pipeline and signal quality gating.
open_wearable/lib/apps/heart_tracker/model/msptd_fast_v2_detector.dart Adds a Dart adaptation of MSPTD Fast v2 peak detector.
open_wearable/lib/apps/heart_tracker/model/band_pass_filter.dart Hardens band-pass filter parameter handling and adds initialization behavior.
open_wearable/ios/Runner/AppDelegate.swift Adds method channels for system settings and background execution window on iOS.
open_wearable/android/app/src/main/kotlin/com/example/open_wearable/MainActivity.kt Adds method channel to open Bluetooth settings on Android.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if (hasBanners)
Positioned(
top: MediaQuery.of(context).padding.top,
top: 0,
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues here: (1) key: banner.key ?? UniqueKey() creates a new UniqueKey() on every rebuild for banners without keys, which can break Dismissible state (animations/reset) and make identity unstable. Prefer requiring non-null keys for banners, generating a stable ValueKey from an ID, or storing a stable generated key alongside the banner in the controller. (2) Replacing the horizontal SingleChildScrollView with a plain Column inside a Positioned overlay can cause vertical overflow when multiple banners stack; wrap the list in a scrollable (e.g., SingleChildScrollView vertical) or constrain height (e.g., ConstrainedBox/ListView with shrinkWrap).

Copilot uses AI. Check for mistakes.
Comment on lines +38 to 55
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 6),
...banners.map(
(banner) => Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 8),
child: Dismissible(
key: banner.key ?? UniqueKey(),
direction: DismissDirection.up,
onDismissed: (_) => controller.hideBanner(banner),
child: banner,
),
],
),
),
),
],
),
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues here: (1) key: banner.key ?? UniqueKey() creates a new UniqueKey() on every rebuild for banners without keys, which can break Dismissible state (animations/reset) and make identity unstable. Prefer requiring non-null keys for banners, generating a stable ValueKey from an ID, or storing a stable generated key alongside the banner in the controller. (2) Replacing the horizontal SingleChildScrollView with a plain Column inside a Positioned overlay can cause vertical overflow when multiple banners stack; wrap the list in a scrollable (e.g., SingleChildScrollView vertical) or constrain height (e.g., ConstrainedBox/ListView with shrinkWrap).

Copilot uses AI. Check for mistakes.
Comment on lines +25 to 39
final removed = activeBanners.remove(banner);
if (!removed) {
return;
}
notifyListeners();
}

void hideBannerByKey(Key key) {
final before = activeBanners.length;
activeBanners.removeWhere((b) => b.key == key);
if (activeBanners.length == before) {
return;
}
notifyListeners();
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hideBanner changed semantics from 'remove by key' to 'remove by object identity'. This is a breaking behavioral change for any callers that construct a new AppBanner with the same key and call hideBanner(...) (previously it would work, now it will silently do nothing). Consider restoring key-based removal in hideBanner (or at least falling back to hideBannerByKey(banner.key!) when remove(banner) fails and banner.key != null).

Copilot uses AI. Check for mistakes.
Comment on lines 49 to 58
void didUpdateWidget(covariant BatteryStateView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.device != widget.device ||
oldWidget.liveUpdates != widget.liveUpdates) {
_isDisconnected = false;
_attachDisconnectListener();
_attachCapabilityListener();
_resolveBatteryStreams();
}
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addDisconnectListener is being called whenever liveUpdates changes as well as when the device changes, which can register multiple disconnect listeners for the same device over the widget lifetime. Also, the listener updates state unconditionally for the current widget even if the widget has since switched to a different device. Suggestion: only attach a disconnect listener when the device instance changes (not when liveUpdates changes), and inside the callback guard against stale updates (e.g., check _deviceKey == keyAtListenerRegistration before calling setState).

Copilot uses AI. Check for mistakes.
Comment on lines 62 to 75
void _attachDisconnectListener() {
final keyAtListenerRegistration = _deviceKey;
widget.device.addDisconnectListener(() {
if (!mounted) {
return;
}
setState(() {
_isDisconnected = true;
_primeAttemptsByDeviceId.remove(keyAtListenerRegistration);
_batteryPercentageStream = null;
_powerStatusStream = null;
});
});
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addDisconnectListener is being called whenever liveUpdates changes as well as when the device changes, which can register multiple disconnect listeners for the same device over the widget lifetime. Also, the listener updates state unconditionally for the current widget even if the widget has since switched to a different device. Suggestion: only attach a disconnect listener when the device instance changes (not when liveUpdates changes), and inside the callback guard against stale updates (e.g., check _deviceKey == keyAtListenerRegistration before calling setState).

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +190
Future<String> _wearableNameWithSide(Wearable wearable) async {
final displayName = formatWearableDisplayName(wearable.name);

if (!wearable.hasCapability<StereoDevice>()) {
return displayName;
}

try {
final position =
await wearable.requireCapability<StereoDevice>().position;
return switch (position) {
DevicePosition.left => '$displayName (Left)',
DevicePosition.right => '$displayName (Right)',
_ => displayName,
};
} catch (_) {
return displayName;
}
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_wearableNameWithSide awaits StereoDevice.position without any timeout. If that future stalls (e.g., capability present but not responsive), it can block _syncTimeAndEmit and delay/skip time synchronization and its event emission. Consider adding a bounded timeout (and falling back to displayName) so time sync isn't gated on side resolution.

Copilot uses AI. Check for mistakes.
final matchingValue = config.values
.where((value) => value.key == selectedKey)
.cast<SensorConfigurationValue?>()
.firstOrNull;
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

firstOrNull is not a dart:core API in many Dart/Flutter toolchains and typically requires an extension import (commonly package:collection/collection.dart). As written, this is likely a compile error. Either add the required import (if the repo already depends on it) or avoid firstOrNull by using firstWhere(..., orElse: () => null) with a nullable cast.

Suggested change
.firstOrNull;
.firstWhere(
(_) => true,
orElse: () => null,
);

Copilot uses AI. Check for mistakes.
Comment on lines +229 to +235
final selectedWearable = compatibleWearables
.where((wearable) => wearable.deviceId == selectedId)
.firstOrNull;

if (selectedWearable == null) {
return;
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as elsewhere: firstOrNull likely requires an extension import (e.g., package:collection/collection.dart) and may not compile without it. Add the appropriate import or rewrite to a firstWhere(..., orElse: () => null) pattern.

Copilot uses AI. Check for mistakes.
}
}

void _stopScanning() {
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_stopScanning() only cancels the local subscription but does not stop the underlying scan in WearableManager. This can leave scanning running (battery drain) and can also cause startScan() to fail later if a scan is already active. Prefer invoking the appropriate WearableManager API to stop scanning (or restoring the previous stop mechanism) in addition to cancelling the stream subscription.

Suggested change
void _stopScanning() {
void _stopScanning() {
// Ensure the underlying scan in WearableManager is also stopped.
// This prevents background scanning (battery drain) and allows future
// calls to startScan() to succeed if they require a fresh scan.
wearableManager.stopScan();

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 65 out of 100 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants