chore: release v1.4.0
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m7s

- Decentralized Room Registry pattern
- Each room registers itself with central registry
- Dynamic navigation tabs and settings dropdown
- New Media Room and Parlor feature folders
- Permission-based room filtering support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-01-05 15:46:44 +01:00
co-authored by Claude Opus 4.5
parent 40ba6a869d
commit c3f6d27a52
15 changed files with 581 additions and 134 deletions
+6 -17
View File
@@ -4,9 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:tatlock_ui/core/auth/auth_provider.dart';
import 'package:tatlock_ui/core/config/app_config.dart';
import 'package:tatlock_ui/features/control_room/router.dart';
import 'package:tatlock_ui/features/security/router.dart';
import 'package:tatlock_ui/routing/app_router.dart';
import 'package:tatlock_ui/routing/room_registry.dart';
import 'package:tatlock_ui/shared/layouts/widgets/top_header_bar.dart';
/// Main application scaffold with top header navigation.
@@ -217,22 +215,13 @@ class _AppScaffoldState extends ConsumerState<AppScaffold> {
int _selectedIndex(BuildContext context) {
final location = GoRouterState.of(context).matchedLocation;
if (location.startsWith(ControlRoomRoutes.base)) return 1;
if (location.startsWith(SecurityRoutes.base)) return 2;
if (location.startsWith(AppRoutes.parlor)) return 3;
// Settings is no longer in main nav (accessed via Profile dropdown)
return 0; // Front Hall
return roomRegistry.indexOfRoute(location);
}
void _onNavSelected(BuildContext context, int index) {
final route = switch (index) {
0 => AppRoutes.frontHall,
1 => ControlRoomRoutes.containers,
2 => SecurityRoutes.users,
3 => AppRoutes.parlor,
_ => AppRoutes.frontHall,
};
context.go(route);
final rooms = roomRegistry.all;
if (index >= 0 && index < rooms.length) {
context.go(rooms[index].defaultRoute);
}
}
}
+6 -35
View File
@@ -2,6 +2,7 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:tatlock_ui/core/semantics/semantic_ids.dart';
import 'package:tatlock_ui/routing/room_registry.dart';
import 'profile_dropdown.dart';
@@ -24,28 +25,8 @@ class TopHeaderBar extends StatelessWidget {
static const double _logoCircleRadius = (_logoSize + _logoMargin * 2) / 2; // 64px
static const double _bulgeFraction = 0.20; // 20% of circle below header line
static const _rooms = [
_RoomDestination(
icon: Icons.door_front_door_outlined,
selectedIcon: Icons.door_front_door,
label: 'Front Hall',
),
_RoomDestination(
icon: Icons.dns_outlined,
selectedIcon: Icons.dns,
label: 'Control Room',
),
_RoomDestination(
icon: Icons.security_outlined,
selectedIcon: Icons.security,
label: 'Security',
),
_RoomDestination(
icon: Icons.lightbulb_outline,
selectedIcon: Icons.lightbulb,
label: 'Parlor',
),
];
/// Get rooms from the registry.
List<RoomDefinition> get _rooms => roomRegistry.all;
@override
Widget build(BuildContext context) {
@@ -121,9 +102,10 @@ class TopHeaderBar extends StatelessWidget {
List<Widget> _buildRoomTabs(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final rooms = _rooms;
return List.generate(_rooms.length, (index) {
final room = _rooms[index];
return List.generate(rooms.length, (index) {
final room = rooms[index];
final isSelected = index == selectedIndex;
return Padding(
@@ -257,14 +239,3 @@ class _HeaderPainter extends CustomPainter {
}
}
class _RoomDestination {
const _RoomDestination({
required this.icon,
required this.selectedIcon,
required this.label,
});
final IconData icon;
final IconData selectedIcon;
final String label;
}