fix theme / add history
This commit is contained in:
+3
-3
@@ -14,10 +14,10 @@ class KoolTabApp extends StatelessWidget {
|
||||
routerConfig: appRouter,
|
||||
// theme: ThemeData(
|
||||
// useMaterial3: false,
|
||||
// colorSchemeSeed: Colors.redAccent,
|
||||
// brightness: Brightness.dark,
|
||||
// colorSchemeSeed: Colors.blueAccent,
|
||||
// brightness: Brightness.light,
|
||||
// ),
|
||||
theme: neoBrutalDarkTheme
|
||||
theme: darkTheme
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../views/bar_screen_view.dart';
|
||||
import '../views/history_screen_view.dart';
|
||||
import '../views/product_form_view.dart';
|
||||
import '../views/product_list_view.dart';
|
||||
|
||||
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||||
|
||||
final appRouter = GoRouter(
|
||||
initialLocation: '/bar',
|
||||
observers: [
|
||||
routeObserver,
|
||||
],
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/bar',
|
||||
builder: (context, state) => const BarScreenView(),
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
path: '/products',
|
||||
builder: (context, state) => const ProductListView(),
|
||||
@@ -29,5 +37,9 @@ final appRouter = GoRouter(
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/history',
|
||||
builder: (context, state) => const HistoryScreenView(),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -69,18 +69,45 @@ class TabItems extends Table {
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@DataClassName('ClosedTabsRow')
|
||||
class ClosedTabs extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get originalTabId => text()();
|
||||
TextColumn get customerName => text()();
|
||||
DateTimeColumn get closedAt => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@DataClassName('ClosedTabItemRow')
|
||||
class ClosedTabItems extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get closedTabId => text()();
|
||||
TextColumn get productId => text()();
|
||||
TextColumn get productName => text()();
|
||||
IntColumn get quantity => integer()();
|
||||
IntColumn get unitPriceInCents => integer()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
Products,
|
||||
BarTabs,
|
||||
TabItems,
|
||||
|
||||
ClosedTabs,
|
||||
ClosedTabItems
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
int get schemaVersion => 4;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
@@ -97,6 +124,11 @@ class AppDatabase extends _$AppDatabase {
|
||||
if (from < 3) {
|
||||
await migrator.addColumn(products, products.imagePath);
|
||||
}
|
||||
|
||||
if (from < 4){
|
||||
await migrator.createTable(closedTabs);
|
||||
await migrator.createTable(closedTabItems);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:kooltab2/viewmodels/history_view_model.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'app/app.dart';
|
||||
@@ -13,6 +16,11 @@ import 'viewmodels/product_list_view_model.dart';
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
|
||||
await initializeDateFormatting('nl_BE', null);
|
||||
Intl.defaultLocale = 'nl_BE';
|
||||
|
||||
|
||||
await SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
@@ -45,6 +53,9 @@ Future<void> main() async {
|
||||
barTabService: context.read<BarTabService>(),
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider<HistoryViewModel>(
|
||||
create: (context) => HistoryViewModel(barTabService: context.read<BarTabService>()),
|
||||
)
|
||||
],
|
||||
child: const AppBootstrap(
|
||||
child: KoolTabApp(),
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'closed_tab_item.dart';
|
||||
|
||||
class ClosedTab {
|
||||
final String id;
|
||||
final String originalTabId;
|
||||
final String customerName;
|
||||
final DateTime closedAt;
|
||||
final List<ClosedTabItem> items;
|
||||
|
||||
ClosedTab({
|
||||
required this.id,
|
||||
required this.originalTabId,
|
||||
required this.customerName,
|
||||
required this.closedAt,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
int get itemCount => items.fold(0, (sum, item) => sum + item.quantity);
|
||||
|
||||
int get totalInCents =>
|
||||
items.fold(0, (sum, item) => sum + item.quantity * item.unitPriceInCents);
|
||||
|
||||
String get formattedTotal =>
|
||||
NumberFormat.simpleCurrency().format(totalInCents / 100);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
class ClosedTabItem {
|
||||
final String id;
|
||||
final String closedTabId;
|
||||
final String productId;
|
||||
final String productName;
|
||||
final int quantity;
|
||||
final int unitPriceInCents;
|
||||
|
||||
ClosedTabItem({
|
||||
required this.id,
|
||||
required this.closedTabId,
|
||||
required this.productId,
|
||||
required this.productName,
|
||||
required this.quantity,
|
||||
required this.unitPriceInCents,
|
||||
});
|
||||
|
||||
int get lineTotalInCents => quantity * unitPriceInCents;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../database/app_database.dart';
|
||||
import '../models/bar_tab.dart';
|
||||
import '../models/closed_tab.dart';
|
||||
import '../models/closed_tab_item.dart';
|
||||
import '../models/product.dart';
|
||||
import '../models/tab_item.dart';
|
||||
|
||||
@@ -25,7 +27,11 @@ abstract class BarTabService {
|
||||
required int quantity,
|
||||
});
|
||||
|
||||
/// Archives the tab's current items into history and clears them.
|
||||
/// The tab itself stays open under the same customer name.
|
||||
Future<void> closeTab(String tabId);
|
||||
|
||||
Future<List<ClosedTab>> getClosedTabs();
|
||||
}
|
||||
|
||||
class DriftBarTabService implements BarTabService {
|
||||
@@ -61,6 +67,17 @@ class DriftBarTabService implements BarTabService {
|
||||
);
|
||||
}
|
||||
|
||||
ClosedTabItem _mapClosedItemRow(ClosedTabItemRow row) {
|
||||
return ClosedTabItem(
|
||||
id: row.id,
|
||||
closedTabId: row.closedTabId,
|
||||
productId: row.productId,
|
||||
productName: row.productName,
|
||||
quantity: row.quantity,
|
||||
unitPriceInCents: row.unitPriceInCents,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<TabItem>> _getItemsForTab(String tabId) async {
|
||||
final query = database.select(database.tabItems)
|
||||
..where((item) => item.tabId.equals(tabId))
|
||||
@@ -201,14 +218,83 @@ class DriftBarTabService implements BarTabService {
|
||||
|
||||
@override
|
||||
Future<void> closeTab(String tabId) async {
|
||||
final updateQuery = database.update(database.barTabs)
|
||||
..where((tab) => tab.id.equals(tabId));
|
||||
await database.transaction(() async {
|
||||
final tabQuery = database.select(database.barTabs)
|
||||
..where((tab) => tab.id.equals(tabId));
|
||||
|
||||
await updateQuery.write(
|
||||
BarTabsCompanion(
|
||||
status: const Value('closed'),
|
||||
closedAt: Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
final tabRow = await tabQuery.getSingleOrNull();
|
||||
|
||||
if (tabRow == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final items = await _getItemsForTab(tabId);
|
||||
|
||||
if (items.isEmpty) {
|
||||
// Nothing to settle, leave the tab as-is.
|
||||
return;
|
||||
}
|
||||
|
||||
final closedTabId = _uuid.v4();
|
||||
|
||||
await database.into(database.closedTabs).insert(
|
||||
ClosedTabsCompanion.insert(
|
||||
id: closedTabId,
|
||||
originalTabId: tabId,
|
||||
customerName: tabRow.customerName,
|
||||
closedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
for (final item in items) {
|
||||
await database.into(database.closedTabItems).insert(
|
||||
ClosedTabItemsCompanion.insert(
|
||||
id: _uuid.v4(),
|
||||
closedTabId: closedTabId,
|
||||
productId: item.productId,
|
||||
productName: item.productName,
|
||||
quantity: item.quantity,
|
||||
unitPriceInCents: item.unitPriceInCents,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final deleteQuery = database.delete(database.tabItems)
|
||||
..where((item) => item.tabId.equals(tabId));
|
||||
|
||||
await deleteQuery.go();
|
||||
|
||||
// Note: tab status/customerName untouched on purpose — the tab
|
||||
// stays open so it keeps showing in the open tabs list.
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ClosedTab>> getClosedTabs() async {
|
||||
final query = database.select(database.closedTabs)
|
||||
..orderBy([
|
||||
(tab) => OrderingTerm.desc(tab.closedAt),
|
||||
]);
|
||||
|
||||
final closedTabRows = await query.get();
|
||||
|
||||
final closedTabs = <ClosedTab>[];
|
||||
|
||||
for (final row in closedTabRows) {
|
||||
final itemsQuery = database.select(database.closedTabItems)
|
||||
..where((item) => item.closedTabId.equals(row.id));
|
||||
|
||||
final itemRows = await itemsQuery.get();
|
||||
|
||||
closedTabs.add(ClosedTab(
|
||||
id: row.id,
|
||||
originalTabId: row.originalTabId,
|
||||
customerName: row.customerName,
|
||||
closedAt: row.closedAt,
|
||||
items: itemRows.map<ClosedTabItem>(_mapClosedItemRow).toList(),
|
||||
));
|
||||
}
|
||||
|
||||
return closedTabs;
|
||||
}
|
||||
}
|
||||
+516
-158
@@ -1,38 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared design tokens
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class AppRadii {
|
||||
static const double sm = 10;
|
||||
static const double md = 14;
|
||||
static const double lg = 20;
|
||||
static const double xl = 28;
|
||||
}
|
||||
|
||||
class AppSpacing {
|
||||
static const double xs = 4;
|
||||
static const double sm = 8;
|
||||
static const double md = 12;
|
||||
static const double lg = 16;
|
||||
static const double xl = 24;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dark theme — refined violet palette
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const _violet = Color(0xFF8B5CF6);
|
||||
const _violetSoft = Color(0xFFB39DFF);
|
||||
const _bg = Color(0xFF111014);
|
||||
const _surface = Color(0xFF1A1920);
|
||||
const _surfaceRaised = Color(0xFF221F29);
|
||||
const _outline = Color(0x1FFFFFFF); // ~12% white
|
||||
|
||||
final ThemeData darkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF7C4DFF),
|
||||
seedColor: _violet,
|
||||
brightness: Brightness.dark,
|
||||
surface: _surface,
|
||||
error: const Color(0xFFFF6B6B),
|
||||
),
|
||||
|
||||
scaffoldBackgroundColor: const Color(0xFF121212),
|
||||
canvasColor: const Color(0xFF121212),
|
||||
scaffoldBackgroundColor: _bg,
|
||||
canvasColor: _bg,
|
||||
|
||||
appBarTheme: const AppBarTheme(
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
centerTitle: false,
|
||||
backgroundColor: _bg,
|
||||
foregroundColor: Colors.white,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
titleTextStyle: TextStyle(
|
||||
titleTextStyle: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.3,
|
||||
color: Colors.white,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.white70, size: 22),
|
||||
actionsIconTheme: const IconThemeData(color: Colors.white70, size: 22),
|
||||
),
|
||||
|
||||
iconButtonTheme: IconButtonThemeData(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.white.withOpacity(0.05),
|
||||
foregroundColor: Colors.white70,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
),
|
||||
),
|
||||
|
||||
cardTheme: CardThemeData(
|
||||
color: const Color(0xFF1E1E1E),
|
||||
color: _surface,
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
side: BorderSide(
|
||||
color: Colors.white.withOpacity(0.06),
|
||||
borderRadius: BorderRadius.circular(AppRadii.lg),
|
||||
side: const BorderSide(color: _outline, width: 1),
|
||||
),
|
||||
),
|
||||
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
elevation: 0,
|
||||
backgroundColor: _violet,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: Colors.white.withOpacity(0.06),
|
||||
disabledForegroundColor: Colors.white24,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xl,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -40,175 +110,214 @@ final ThemeData darkTheme = ThemeData(
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
elevation: 0,
|
||||
backgroundColor: const Color(0xFF7C4DFF),
|
||||
backgroundColor: _violet,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 16,
|
||||
horizontal: AppSpacing.xl,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: BorderSide(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
),
|
||||
side: const BorderSide(color: _outline, width: 1.4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 16,
|
||||
horizontal: AppSpacing.xl,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.white70,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF242424),
|
||||
fillColor: _surfaceRaised,
|
||||
hintStyle: const TextStyle(color: Colors.white38),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 18,
|
||||
vertical: 16,
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.white.withOpacity(0.08),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
borderSide: const BorderSide(color: _outline),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF7C4DFF),
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
borderSide: const BorderSide(color: _violet, width: 2),
|
||||
),
|
||||
),
|
||||
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: _surfaceRaised,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.lg),
|
||||
),
|
||||
titleTextStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
contentTextStyle: const TextStyle(color: Colors.white70, height: 1.4),
|
||||
),
|
||||
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: Color(0xFF7C4DFF),
|
||||
backgroundColor: _violet,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
),
|
||||
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: const Color(0xFF1A1A1A),
|
||||
indicatorColor: const Color(0xFF7C4DFF).withOpacity(0.25),
|
||||
labelTextStyle: WidgetStatePropertyAll(
|
||||
TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
backgroundColor: _surface,
|
||||
indicatorColor: _violet.withOpacity(0.25),
|
||||
labelTextStyle: const WidgetStatePropertyAll(
|
||||
TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
|
||||
listTileTheme: ListTileThemeData(
|
||||
tileColor: const Color(0xFF1E1E1E),
|
||||
tileColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
iconColor: const Color(0xFFB39DFF),
|
||||
iconColor: _violetSoft,
|
||||
textColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.xs,
|
||||
),
|
||||
),
|
||||
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: const Color(0xFF2A2A2A),
|
||||
backgroundColor: _surfaceRaised,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
side: const BorderSide(color: _outline),
|
||||
),
|
||||
contentTextStyle: const TextStyle(color: Colors.white),
|
||||
),
|
||||
|
||||
dividerTheme: DividerThemeData(
|
||||
color: Colors.white.withOpacity(0.08),
|
||||
thickness: 1,
|
||||
dividerTheme: const DividerThemeData(color: _outline, thickness: 1, space: 1),
|
||||
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: _surfaceRaised,
|
||||
labelStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.sm),
|
||||
side: const BorderSide(color: _outline),
|
||||
),
|
||||
),
|
||||
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
displayLarge: TextStyle(color: Colors.white, fontWeight: FontWeight.w800),
|
||||
headlineMedium: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.3,
|
||||
),
|
||||
titleLarge: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.2,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 16,
|
||||
height: 1.5,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
color: Colors.white60,
|
||||
height: 1.4,
|
||||
),
|
||||
labelLarge: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
bodyLarge: TextStyle(color: Colors.white70, fontSize: 16, height: 1.5),
|
||||
bodyMedium: TextStyle(color: Colors.white60, height: 1.4),
|
||||
bodySmall: TextStyle(color: Colors.white38, height: 1.3),
|
||||
labelLarge: TextStyle(color: Colors.white, fontWeight: FontWeight.w700),
|
||||
),
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Neo-brutalist dark theme — punchy, high-contrast alternative
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const _nbBg = Color(0xFF0C0C0E);
|
||||
const _nbSurface = Color(0xFF1C1C20);
|
||||
const _nbYellow = Color(0xFFFFD60A);
|
||||
const _nbCyan = Color(0xFF00E5FF);
|
||||
const _nbPink = Color(0xFFFF3D81);
|
||||
|
||||
final ThemeData neoBrutalDarkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: Color(0xFFFFD60A), // Bright yellow
|
||||
secondary: Color(0xFF00E5FF), // Cyan
|
||||
surface: Color(0xFF1A1A1A),
|
||||
error: Color(0xFFFF5252),
|
||||
primary: _nbYellow,
|
||||
secondary: _nbCyan,
|
||||
surface: _nbSurface,
|
||||
error: _nbPink,
|
||||
),
|
||||
|
||||
scaffoldBackgroundColor: const Color(0xFF0E0E0E),
|
||||
canvasColor: const Color(0xFF0E0E0E),
|
||||
scaffoldBackgroundColor: _nbBg,
|
||||
canvasColor: _nbBg,
|
||||
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
letterSpacing: -1,
|
||||
letterSpacing: -1.5,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
titleLarge: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white70,
|
||||
),
|
||||
bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
|
||||
bodyMedium: TextStyle(fontSize: 14, color: Colors.white70),
|
||||
bodySmall: TextStyle(fontSize: 12, color: Colors.white54, fontWeight: FontWeight.w600),
|
||||
),
|
||||
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xFF0E0E0E),
|
||||
backgroundColor: _nbBg,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
@@ -220,108 +329,357 @@ final ThemeData neoBrutalDarkTheme = ThemeData(
|
||||
),
|
||||
|
||||
cardTheme: CardThemeData(
|
||||
color: const Color(0xFF242424),
|
||||
color: _nbSurface,
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: const BorderSide(color: Colors.white, width: 3),
|
||||
),
|
||||
),
|
||||
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: _nbYellow,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 18),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: const BorderSide(color: Colors.black, width: 3),
|
||||
),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16),
|
||||
),
|
||||
),
|
||||
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _nbYellow,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 18),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: const BorderSide(color: Colors.black, width: 3),
|
||||
),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16),
|
||||
),
|
||||
),
|
||||
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: const BorderSide(color: Colors.white, width: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 18),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w800),
|
||||
),
|
||||
),
|
||||
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: _nbSurface,
|
||||
hintStyle: const TextStyle(color: Colors.white38, fontWeight: FontWeight.w600),
|
||||
contentPadding: const EdgeInsets.all(18),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 3),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 3),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(color: _nbYellow, width: 4),
|
||||
),
|
||||
),
|
||||
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: _nbSurface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: const BorderSide(
|
||||
color: Colors.white,
|
||||
width: 3,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: const BorderSide(color: Colors.white, width: 3),
|
||||
),
|
||||
titleTextStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
contentTextStyle: const TextStyle(color: Colors.white70, fontWeight: FontWeight.w600),
|
||||
),
|
||||
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: _nbPink,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
side: BorderSide(color: Colors.white, width: 3),
|
||||
),
|
||||
),
|
||||
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: _nbSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: const BorderSide(color: Colors.white, width: 3),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
contentTextStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600),
|
||||
),
|
||||
|
||||
dividerTheme: const DividerThemeData(color: Colors.white, thickness: 3),
|
||||
|
||||
listTileTheme: ListTileThemeData(
|
||||
tileColor: _nbSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: const BorderSide(color: Colors.white, width: 3),
|
||||
),
|
||||
textColor: Colors.white,
|
||||
iconColor: _nbYellow,
|
||||
),
|
||||
);
|
||||
|
||||
const _violetL = Color(0xFF7C3AED);
|
||||
const _violetSoftL = Color(0xFF6D28D9);
|
||||
const _bgL = Color(0xFFFAFAFC);
|
||||
const _surfaceL = Color(0xFFFFFFFF);
|
||||
const _surfaceRaisedL = Color(0xFFF3F1F8);
|
||||
const _outlineL = Color(0x14000000); // ~8% black
|
||||
|
||||
final ThemeData lightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: _violetL,
|
||||
brightness: Brightness.light,
|
||||
surface: _surfaceL,
|
||||
error: const Color(0xFFE5484D),
|
||||
),
|
||||
|
||||
scaffoldBackgroundColor: _bgL,
|
||||
canvasColor: _bgL,
|
||||
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
backgroundColor: _bgL,
|
||||
foregroundColor: Colors.black87,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
titleTextStyle: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.3,
|
||||
color: Colors.black87,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black54, size: 22),
|
||||
actionsIconTheme: const IconThemeData(color: Colors.black54, size: 22),
|
||||
),
|
||||
|
||||
iconButtonTheme: IconButtonThemeData(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.black.withOpacity(0.04),
|
||||
foregroundColor: Colors.black54,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
),
|
||||
),
|
||||
|
||||
cardTheme: CardThemeData(
|
||||
color: _surfaceL,
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.lg),
|
||||
side: const BorderSide(color: _outlineL, width: 1),
|
||||
),
|
||||
),
|
||||
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
elevation: 0,
|
||||
backgroundColor: _violetL,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: Colors.black.withOpacity(0.06),
|
||||
disabledForegroundColor: Colors.black26,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xl,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFFD60A),
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
backgroundColor: _violetL,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 18,
|
||||
horizontal: AppSpacing.xl,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: const BorderSide(
|
||||
color: Colors.black,
|
||||
width: 3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 16,
|
||||
textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.black87,
|
||||
side: const BorderSide(color: _outlineL, width: 1.4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xl,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.black54,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF242424),
|
||||
contentPadding: const EdgeInsets.all(18),
|
||||
fillColor: _surfaceRaisedL,
|
||||
hintStyle: const TextStyle(color: Colors.black38),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.white,
|
||||
width: 3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.white,
|
||||
width: 3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
borderSide: const BorderSide(color: _outlineL),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFFFD60A),
|
||||
width: 4,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
borderSide: const BorderSide(color: _violetL, width: 2),
|
||||
),
|
||||
),
|
||||
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: Color(0xFFFF5252),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: _surfaceL,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
side: BorderSide(
|
||||
color: Colors.white,
|
||||
width: 3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadii.lg),
|
||||
),
|
||||
titleTextStyle: const TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
contentTextStyle: const TextStyle(color: Colors.black54, height: 1.4),
|
||||
),
|
||||
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: _violetL,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
),
|
||||
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: _surfaceL,
|
||||
indicatorColor: _violetL.withOpacity(0.15),
|
||||
labelTextStyle: const WidgetStatePropertyAll(
|
||||
TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
|
||||
listTileTheme: ListTileThemeData(
|
||||
tileColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
iconColor: _violetSoftL,
|
||||
textColor: Colors.black87,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.xs,
|
||||
),
|
||||
),
|
||||
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: const Color(0xFF242424),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: const BorderSide(
|
||||
color: Colors.white,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.black87,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: Colors.white,
|
||||
thickness: 3,
|
||||
),
|
||||
|
||||
listTileTheme: ListTileThemeData(
|
||||
tileColor: const Color(0xFF242424),
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: const BorderSide(
|
||||
color: Colors.white,
|
||||
width: 3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
),
|
||||
textColor: Colors.white,
|
||||
iconColor: const Color(0xFFFFD60A),
|
||||
contentTextStyle: const TextStyle(color: Colors.white),
|
||||
),
|
||||
|
||||
dividerTheme: const DividerThemeData(color: _outlineL, thickness: 1, space: 1),
|
||||
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: _surfaceRaisedL,
|
||||
labelStyle: const TextStyle(
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.sm),
|
||||
side: const BorderSide(color: _outlineL),
|
||||
),
|
||||
),
|
||||
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(color: Colors.black87, fontWeight: FontWeight.w800),
|
||||
headlineMedium: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.3,
|
||||
),
|
||||
titleLarge: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.2,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
bodyLarge: TextStyle(color: Colors.black54, fontSize: 16, height: 1.5),
|
||||
bodyMedium: TextStyle(color: Colors.black45, height: 1.4),
|
||||
bodySmall: TextStyle(color: Colors.black38, height: 1.3),
|
||||
labelLarge: TextStyle(color: Colors.black87, fontWeight: FontWeight.w700),
|
||||
),
|
||||
);
|
||||
@@ -114,17 +114,12 @@ class BarScreenViewModel extends ChangeNotifier {
|
||||
|
||||
await barTabService.closeTab(tab.id);
|
||||
|
||||
_selectedTabId = null;
|
||||
await _reloadTabs();
|
||||
}
|
||||
|
||||
Future<void> closeTab(String tabId) async {
|
||||
await barTabService.closeTab(tabId);
|
||||
|
||||
if (_selectedTabId == tabId) {
|
||||
_selectedTabId = null;
|
||||
}
|
||||
|
||||
await _reloadTabs();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/closed_tab.dart';
|
||||
import '../services/bar_tab_service.dart';
|
||||
|
||||
class HistoryViewModel extends ChangeNotifier {
|
||||
final BarTabService barTabService;
|
||||
|
||||
HistoryViewModel({
|
||||
required this.barTabService,
|
||||
});
|
||||
|
||||
List<ClosedTab> _closedTabs = [];
|
||||
bool _isLoading = false;
|
||||
bool _hasLoaded = false;
|
||||
String? _errorMessage;
|
||||
String _searchQuery = '';
|
||||
|
||||
List<ClosedTab> get closedTabs {
|
||||
if (_searchQuery.isEmpty) return _closedTabs;
|
||||
|
||||
return _closedTabs
|
||||
.where((tab) =>
|
||||
tab.customerName.toLowerCase().contains(_searchQuery.toLowerCase()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
bool get hasLoaded => _hasLoaded;
|
||||
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
Future<void> ensureLoaded() async {
|
||||
if (_hasLoaded || _isLoading) return;
|
||||
|
||||
await load();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_closedTabs = await barTabService.getClosedTabs();
|
||||
} catch (_) {
|
||||
_errorMessage = 'Could not load tab history.';
|
||||
} finally {
|
||||
_hasLoaded = true;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void search(String query) {
|
||||
_searchQuery = query;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
+502
-187
@@ -22,34 +22,65 @@ class BarScreenView extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Bar Tabs'),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Manage products',
|
||||
onPressed: () => context.go('/products'),
|
||||
icon: const Icon(Icons.inventory_2_outlined),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
IconButton(
|
||||
tooltip: 'Tab history',
|
||||
onPressed: () => context.go('/history'),
|
||||
icon: const Icon(Icons.history_rounded),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
IconButton(
|
||||
tooltip: 'Refresh',
|
||||
onPressed: viewModel.load,
|
||||
icon: const Icon(Icons.refresh),
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
if (viewModel.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||
);
|
||||
}
|
||||
|
||||
if (viewModel.errorMessage != null) {
|
||||
return Center(child: Text(viewModel.errorMessage!));
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline_rounded,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
viewModel.errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
flex: 2,
|
||||
child: _ProductGrid(
|
||||
products: productsViewModel.products,
|
||||
hasSelectedTab: viewModel.selectedTab != null,
|
||||
@@ -67,7 +98,7 @@ class BarScreenView extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
Container(width: 1, color: Theme.of(context).dividerColor),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: _TabPanel(
|
||||
@@ -101,12 +132,12 @@ class BarScreenView extends StatelessWidget {
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Customer / group name',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
Navigator.of(dialogContext).pop(value);
|
||||
},
|
||||
),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
@@ -141,16 +172,17 @@ class BarScreenView extends StatelessWidget {
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text('Close ${tab.customerName}ʼs tab?'),
|
||||
content: Text(
|
||||
'Current total: ${tab.formattedTotal}\n\n'
|
||||
'Payments are not handled yet, so this only marks the tab as closed.',
|
||||
),
|
||||
content: Text('Current total: ${tab.formattedTotal}\n\n'),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(dialogContext).colorScheme.error,
|
||||
),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Close tab'),
|
||||
),
|
||||
@@ -165,6 +197,10 @@ class BarScreenView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Product grid
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _ProductGrid extends StatelessWidget {
|
||||
final List<Product> products;
|
||||
final bool hasSelectedTab;
|
||||
@@ -178,15 +214,36 @@ class _ProductGrid extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
if (products.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.inventory_2_outlined, size: 48),
|
||||
const SizedBox(height: 12),
|
||||
const Text('No products yet.'),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: scheme.onSurface.withOpacity(0.05),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 40,
|
||||
color: scheme.onSurface.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No products yet',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Add your first product to start selling.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () => context.go('/products/new'),
|
||||
icon: const Icon(Icons.add),
|
||||
@@ -197,32 +254,79 @@ class _ProductGrid extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final columns = ((constraints.maxWidth / 170).floor())
|
||||
.clamp(3, 6)
|
||||
.toInt();
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: products.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 1,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('Products', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(width: 10),
|
||||
if (!hasSelectedTab)
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.onSurface.withOpacity(0.06),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline_rounded,
|
||||
size: 14,
|
||||
color: scheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Select a tab to add items',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final product = products[index];
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final columns = ((constraints.maxWidth / 170).floor())
|
||||
.clamp(3, 5)
|
||||
.toInt();
|
||||
|
||||
return _ProductTile(
|
||||
product: product,
|
||||
enabled: hasSelectedTab,
|
||||
onTap: () => onProductTap(product),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||
itemCount: products.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final product = products[index];
|
||||
|
||||
return _ProductTile(
|
||||
product: product,
|
||||
enabled: hasSelectedTab,
|
||||
onTap: () => onProductTap(product),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -250,41 +354,90 @@ class _ProductTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Opacity(
|
||||
opacity: enabled ? 1 : 0.45,
|
||||
child: _hasImage
|
||||
? Image.file(
|
||||
File(product.imagePath!),
|
||||
fit: BoxFit.scaleDown,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
)
|
||||
: const Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
Icon(Icons.image_not_supported_outlined, size: 42),
|
||||
Text("No Image found!"),
|
||||
],
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: scheme.onSurface.withOpacity(0.08), width: 1),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: Material(
|
||||
color: theme.cardTheme.color ?? scheme.surface,
|
||||
child: InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Opacity(
|
||||
opacity: enabled ? 1 : 0.4,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_hasImage
|
||||
? Image.file(
|
||||
File(product.imagePath!),
|
||||
fit: BoxFit.scaleDown,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
)
|
||||
: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
size: 34,
|
||||
color: scheme.onSurface.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'No image',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Scrim stays black regardless of theme — it's for legibility
|
||||
// of the (usually light/photographic) image beneath it, not
|
||||
// themed UI chrome.
|
||||
if (_hasImage)
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.35),
|
||||
],
|
||||
stops: const [0.6, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class _TabPanel extends StatefulWidget {
|
||||
final List<BarTab> tabs;
|
||||
final BarTab? selectedTab;
|
||||
final String? selectedTabId;
|
||||
final VoidCallback onNewTabPressed;
|
||||
final ValueChanged<String> onTabSelected;
|
||||
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
|
||||
final Future<void> Function(TabItem item, int quantity)
|
||||
onItemQuantityChanged;
|
||||
final VoidCallback onCloseTabPressed;
|
||||
final ValueChanged<String> onTabClosed;
|
||||
|
||||
@@ -315,79 +468,130 @@ class _TabPanelState extends State<_TabPanel> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
final filteredTabs = widget.tabs.where((tab) {
|
||||
return tab.customerName.toLowerCase().contains(
|
||||
_searchQuery.toLowerCase(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: widget.onNewTabPressed,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Open tab'),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchQuery = value;
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search name...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'Open tabs',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: _OpenTabsList(
|
||||
tabs: filteredTabs,
|
||||
selectedTabId: widget.selectedTabId,
|
||||
onTabSelected: widget.onTabSelected,
|
||||
onTabClosed: widget.onTabClosed,
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
Expanded(
|
||||
child: widget.selectedTab == null
|
||||
? const Center(child: Text('Select or open a tab.'))
|
||||
: _SelectedTabDetails(
|
||||
tab: widget.selectedTab!,
|
||||
onItemQuantityChanged: widget.onItemQuantityChanged,
|
||||
onCloseTabPressed: widget.onCloseTabPressed,
|
||||
return DecoratedBox(
|
||||
// A step darker/lighter than the main surface, whichever direction
|
||||
// the active theme goes — matches how it read against the dark
|
||||
// surface color originally.
|
||||
decoration: BoxDecoration(color: scheme.surfaceContainerLow),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchQuery = value;
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search by name…',
|
||||
prefixIcon: Icon(Icons.search_rounded, size: 20),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(width: 10),
|
||||
|
||||
IconButton.filled(
|
||||
onPressed: widget.onNewTabPressed,
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
tooltip: 'Open new tab',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'OPEN TABS',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.onSurface.withOpacity(0.06),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
'${filteredTabs.length}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
SizedBox(
|
||||
height: 190,
|
||||
child: _OpenTabsList(
|
||||
tabs: filteredTabs,
|
||||
selectedTabId: widget.selectedTabId,
|
||||
onTabSelected: widget.onTabSelected,
|
||||
onTabClosed: widget.onTabClosed,
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 28),
|
||||
|
||||
Expanded(
|
||||
child: widget.selectedTab == null
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.receipt_long_outlined,
|
||||
size: 36,
|
||||
color: scheme.onSurface.withOpacity(0.25),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Select or open a tab',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: _SelectedTabDetails(
|
||||
tab: widget.selectedTab!,
|
||||
onItemQuantityChanged: widget.onItemQuantityChanged,
|
||||
onCloseTabPressed: widget.onCloseTabPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _OpenTabsList extends StatelessWidget {
|
||||
final List<BarTab> tabs;
|
||||
final String? selectedTabId;
|
||||
@@ -404,22 +608,30 @@ class _OpenTabsList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (tabs.isEmpty) {
|
||||
return const Center(child: Text('No open tabs.'));
|
||||
return Center(
|
||||
child: Text(
|
||||
'No open tabs',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: tabs.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final tab = tabs[index];
|
||||
final selected = tab.id == selectedTabId;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final primary = scheme.primary;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Slidable(
|
||||
key: ValueKey(tab.id),
|
||||
endActionPane: ActionPane(
|
||||
motion: const DrawerMotion(),
|
||||
extentRatio: 0.65,
|
||||
extentRatio: 0.6,
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
@@ -433,28 +645,63 @@ class _OpenTabsList extends StatelessWidget {
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (_) => onTabClosed(tab.id),
|
||||
icon: Icons.close,
|
||||
icon: Icons.close_rounded,
|
||||
label: 'Close',
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: null,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
selected: selected,
|
||||
title: Text(
|
||||
tab.customerName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? primary.withOpacity(0.14)
|
||||
: scheme.onSurface.withOpacity(0.04),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? primary.withOpacity(0.6)
|
||||
: scheme.onSurface.withOpacity(0.06),
|
||||
width: selected ? 1.4 : 1,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: () => onTabSelected(tab.id),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
tab.customerName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${tab.itemCount} items - ${tab.formattedTotal}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: Text('${tab.itemCount} items • ${tab.formattedTotal}'),
|
||||
onTap: () => onTabSelected(tab.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -464,6 +711,9 @@ class _OpenTabsList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
class _SelectedTabDetails extends StatelessWidget {
|
||||
final BarTab tab;
|
||||
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
|
||||
@@ -477,52 +727,89 @@ class _SelectedTabDetails extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tab.customerName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Total: ${tab.formattedTotal}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: tab.items.isEmpty
|
||||
? const Center(child: Text('Tap products to add them.'))
|
||||
: ListView.separated(
|
||||
itemCount: tab.items.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final item = tab.items[index];
|
||||
|
||||
return _TabItemRow(
|
||||
item: item,
|
||||
onQuantityChanged: onItemQuantityChanged,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
tab.formattedTotal,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
tab.customerName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: onCloseTabPressed,
|
||||
child: const Text('Close'),
|
||||
),
|
||||
Divider()
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: tab.items.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_bar_outlined,
|
||||
size: 32,
|
||||
color: scheme.onSurface.withOpacity(0.25),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap products to add them',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
itemCount: tab.items.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final item = tab.items[index];
|
||||
|
||||
return _TabItemRow(
|
||||
item: item,
|
||||
onQuantityChanged: onItemQuantityChanged,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Total', style: Theme.of(context).textTheme.bodySmall),
|
||||
Text(
|
||||
tab.formattedTotal,
|
||||
style: Theme.of(context).textTheme.headlineMedium
|
||||
?.copyWith(fontSize: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: onCloseTabPressed,
|
||||
child: const Text('Close tab'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -536,8 +823,10 @@ class _TabItemRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -548,7 +837,9 @@ class _TabItemRow extends StatelessWidget {
|
||||
item.productName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${item.quantity} × ${item.formattedUnitPrice}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
@@ -556,23 +847,47 @@ class _TabItemRow extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () => onQuantityChanged(item, item.quantity - 1),
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
),
|
||||
Text('${item.quantity}'),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () => onQuantityChanged(item, item.quantity + 1),
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.onSurface.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () =>
|
||||
onQuantityChanged(item, item.quantity - 1),
|
||||
icon: const Icon(Icons.remove_rounded, size: 18),
|
||||
),
|
||||
SizedBox(
|
||||
width: 22,
|
||||
child: Text(
|
||||
'${item.quantity}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () =>
|
||||
onQuantityChanged(item, item.quantity + 1),
|
||||
icon: const Icon(Icons.add_rounded, size: 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: Text(item.formattedLineTotal, textAlign: TextAlign.end),
|
||||
child: Text(
|
||||
item.formattedLineTotal,
|
||||
textAlign: TextAlign.end,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../app/router.dart';
|
||||
import '../models/closed_tab.dart';
|
||||
import '../models/closed_tab_item.dart';
|
||||
import '../viewmodels/history_view_model.dart';
|
||||
|
||||
class HistoryScreenView extends StatefulWidget {
|
||||
const HistoryScreenView({super.key});
|
||||
|
||||
@override
|
||||
State<HistoryScreenView> createState() => _HistoryScreenViewState();
|
||||
}
|
||||
|
||||
class _HistoryScreenViewState extends State<HistoryScreenView> with RouteAware {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<HistoryViewModel>().ensureLoaded();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<HistoryViewModel>().load();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
// Called when you come back to this screen
|
||||
context.read<HistoryViewModel>().load();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = context.watch<HistoryViewModel>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
IconButton(onPressed: (){
|
||||
context.go('/bar');
|
||||
}, icon: const Icon(Icons.arrow_back)),
|
||||
const SizedBox(width: 5),
|
||||
const Text('Tab History'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Refresh',
|
||||
onPressed: viewModel.load,
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
if (viewModel.isLoading && !viewModel.hasLoaded) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||
);
|
||||
}
|
||||
|
||||
if (viewModel.errorMessage != null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline_rounded,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
viewModel.errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||||
child: TextField(
|
||||
onChanged: viewModel.search,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search by name…',
|
||||
prefixIcon: Icon(Icons.search_rounded, size: 20),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: viewModel.closedTabs.isEmpty
|
||||
? _EmptyState()
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||||
itemCount: viewModel.closedTabs.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
final closedTab = viewModel.closedTabs[index];
|
||||
|
||||
return _ClosedTabCard(closedTab: closedTab);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: scheme.onSurface.withOpacity(0.05),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.history_rounded,
|
||||
size: 40,
|
||||
color: scheme.onSurface.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No closed tabs yet',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Tabs you close will show up here.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClosedTabCard extends StatefulWidget {
|
||||
final ClosedTab closedTab;
|
||||
|
||||
const _ClosedTabCard({required this.closedTab});
|
||||
|
||||
@override
|
||||
State<_ClosedTabCard> createState() => _ClosedTabCardState();
|
||||
}
|
||||
|
||||
class _ClosedTabCardState extends State<_ClosedTabCard> {
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final closedTab = widget.closedTab;
|
||||
final dateFormat = DateFormat('MMM d, y · h:mm a');
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.onSurface.withOpacity(0.04),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: scheme.onSurface.withOpacity(0.06)),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => setState(() => _expanded = !_expanded),
|
||||
enableFeedback: true,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
closedTab.customerName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${dateFormat.format(closedTab.closedAt)} · ${closedTab.itemCount} items',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
closedTab.formattedTotal,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
_expanded
|
||||
? Icons.expand_less_rounded
|
||||
: Icons.expand_more_rounded,
|
||||
color: scheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
crossFadeState: _expanded
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 14),
|
||||
child: Column(
|
||||
children: [
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 8),
|
||||
...closedTab.items.map(
|
||||
(item) => _ClosedTabItemRow(item: item),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
secondChild: const SizedBox(width: double.infinity),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClosedTabItemRow extends StatelessWidget {
|
||||
final ClosedTabItem item;
|
||||
|
||||
const _ClosedTabItemRow({required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final unitPrice =
|
||||
NumberFormat.simpleCurrency().format(item.unitPriceInCents / 100);
|
||||
final lineTotal =
|
||||
NumberFormat.simpleCurrency().format(item.lineTotalInCents / 100);
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.productName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: scheme.onSurface),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${item.quantity} × $unitPrice',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: Text(
|
||||
lineTotal,
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(fontWeight: FontWeight.w700, color: scheme.onSurface),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+232
@@ -1,6 +1,22 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "100.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -25,6 +41,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.15.1"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.12.6"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -33,6 +97,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: "5909d2c6b66817222779e1eedc19e0e28b76d1df7bd9856a4792ccb9881df358"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -89,6 +177,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.9"
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -97,6 +193,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.34.0"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "0994276f63a394b7434ed7deaeffd2ddc855a5eafccf5ed00e5e341905a6f62b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.34.3"
|
||||
drift_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -224,6 +328,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "17.3.0"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -240,6 +352,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -312,6 +432,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.3"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -328,6 +464,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.12.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -504,6 +648,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -520,6 +672,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -528,11 +696,35 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.3"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -565,6 +757,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0+eol"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "772bb2f6f5bce0631a60f26b57d6e8882e1105c22345b05c163ee6ee5d7ba32d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.45.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -581,6 +781,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -637,6 +845,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -645,6 +861,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -43,6 +43,8 @@ dependencies:
|
||||
image_picker: ^1.2.3
|
||||
path: ^1.9.1
|
||||
flutter_slidable: ^4.0.3
|
||||
intl: ^0.20.3
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -54,6 +56,8 @@ dev_dependencies:
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^6.0.0
|
||||
drift_dev: ^2.34.2+1
|
||||
build_runner: ^2.15.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
Reference in New Issue
Block a user