Polish frontend: design tokens, theming, AppBar titles, empty/loading states

Frontend usability and aesthetics pass across web and mobile:

- Add shared layout tokens (theme/dimens.dart: Insets + Breakpoints) and
  replace magic-number spacing and per-file breakpoint consts.
- Route both themes through buildAppTheme (theme/theme_builder.dart): explicit
  useMaterial3 and a shared branded textTheme (Barlow Condensed for
  display/headline/title styles); logo now reads its style from the theme.
- Move screen titles into the shared AppBar (route-derived) and drop the
  redundant in-body headers; upgrade Settings buttons to M3 FilledButton.
- Add reusable EmptyState and skeleton loaders; Devices empty state links to
  scanner status, notifications get filter-aware messages.
- Add a first-run welcome intro and Save-disabled helper text in Settings.
- Make device Filter/Sort adaptive: bottom sheet on phones, dialog on wide.
- Collapse the device-list filter chips and Sort/Filter buttons into one row;
  rename the home Scanners card title and align its style.

Tests: add EmptyState/skeleton widget tests and Settings first-run cases;
update tests for the FilledButton swap and relocated titles. 89 passing,
dart analyze clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-04 08:39:37 -04:00
co-authored by Claude Opus 4.8
parent 5f6f5ad679
commit 9367c359d7
20 changed files with 661 additions and 274 deletions
+22 -16
View File
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../theme/dimens.dart';
// The version is read at runtime from the bundled package metadata
// (frontend/pubspec.yaml), so it never needs to be hand-edited here.
const _releaseDate = 'May 28, 2026';
@@ -18,18 +20,16 @@ class About extends StatelessWidget {
final textTheme = Theme.of(context).textTheme;
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
padding: const EdgeInsets.all(Insets.xxl),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('About OOTT', style: textTheme.headlineSmall),
const SizedBox(height: 16),
Text(
'Easy to setup and use network device discovery and alert system. '
'Notifies you when new or unknown devices join your local area network.',
style: textTheme.bodyLarge,
),
const SizedBox(height: 8),
const SizedBox(height: Insets.sm),
FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
@@ -45,7 +45,7 @@ class About extends StatelessWidget {
);
},
),
const SizedBox(height: 24),
const SizedBox(height: Insets.xxl),
_SurfaceContainer(
colorScheme: colorScheme,
child: Column(
@@ -60,17 +60,17 @@ class About extends StatelessWidget {
Divider(
height: 1,
color: colorScheme.outlineVariant,
indent: 16,
endIndent: 16,
indent: Insets.lg,
endIndent: Insets.lg,
),
_LicenseRow(colorScheme: colorScheme, textTheme: textTheme),
],
),
),
const SizedBox(height: 16),
const SizedBox(height: Insets.lg),
_SurfaceContainer(
colorScheme: colorScheme,
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(Insets.lg),
child: _NoticesSection(textTheme: textTheme),
),
],
@@ -132,11 +132,14 @@ class _LinkRow extends StatelessWidget {
return InkWell(
onTap: _open,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
padding: const EdgeInsets.symmetric(
vertical: Insets.md,
horizontal: Insets.lg,
),
child: Row(
children: [
Icon(icon, size: 18, color: colorScheme.primary),
const SizedBox(width: 12),
const SizedBox(width: Insets.md),
Text(
label,
style: textTheme.bodyMedium?.copyWith(
@@ -168,12 +171,15 @@ class _LicenseRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
padding: const EdgeInsets.symmetric(
vertical: Insets.md,
horizontal: Insets.lg,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.gavel, size: 18, color: colorScheme.primary),
const SizedBox(width: 12),
const SizedBox(width: Insets.md),
Flexible(
child: Wrap(
children: [
@@ -214,7 +220,7 @@ class _NoticesSection extends StatelessWidget {
'OOTT, Copyright (C) 2024-2026 Ricardo Zuasti',
style: textTheme.bodyMedium,
),
const SizedBox(height: 8),
const SizedBox(height: Insets.sm),
Text(
'This product includes software developed by third parties and distributed '
'under the Apache License, Version 2.0.',
@@ -222,7 +228,7 @@ class _NoticesSection extends StatelessWidget {
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
const SizedBox(height: Insets.md),
..._thirdPartyComponents.map(
(c) => _ThirdPartyEntry(component: c, textTheme: textTheme),
),
@@ -241,7 +247,7 @@ class _ThirdPartyEntry extends StatelessWidget {
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.only(bottom: Insets.sm),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [