fix theme / add history
This commit is contained in:
+3
-3
@@ -14,10 +14,10 @@ class KoolTabApp extends StatelessWidget {
|
|||||||
routerConfig: appRouter,
|
routerConfig: appRouter,
|
||||||
// theme: ThemeData(
|
// theme: ThemeData(
|
||||||
// useMaterial3: false,
|
// useMaterial3: false,
|
||||||
// colorSchemeSeed: Colors.redAccent,
|
// colorSchemeSeed: Colors.blueAccent,
|
||||||
// brightness: Brightness.dark,
|
// brightness: Brightness.light,
|
||||||
// ),
|
// ),
|
||||||
theme: neoBrutalDarkTheme
|
theme: darkTheme
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../views/bar_screen_view.dart';
|
import '../views/bar_screen_view.dart';
|
||||||
|
import '../views/history_screen_view.dart';
|
||||||
import '../views/product_form_view.dart';
|
import '../views/product_form_view.dart';
|
||||||
import '../views/product_list_view.dart';
|
import '../views/product_list_view.dart';
|
||||||
|
|
||||||
|
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||||||
|
|
||||||
final appRouter = GoRouter(
|
final appRouter = GoRouter(
|
||||||
initialLocation: '/bar',
|
initialLocation: '/bar',
|
||||||
|
observers: [
|
||||||
|
routeObserver,
|
||||||
|
],
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/bar',
|
path: '/bar',
|
||||||
builder: (context, state) => const BarScreenView(),
|
builder: (context, state) => const BarScreenView(),
|
||||||
),
|
),
|
||||||
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/products',
|
path: '/products',
|
||||||
builder: (context, state) => const ProductListView(),
|
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};
|
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(
|
@DriftDatabase(
|
||||||
tables: [
|
tables: [
|
||||||
Products,
|
Products,
|
||||||
BarTabs,
|
BarTabs,
|
||||||
TabItems,
|
TabItems,
|
||||||
|
|
||||||
|
ClosedTabs,
|
||||||
|
ClosedTabItems
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 3;
|
int get schemaVersion => 4;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
@@ -97,6 +124,11 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
if (from < 3) {
|
if (from < 3) {
|
||||||
await migrator.addColumn(products, products.imagePath);
|
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/material.dart';
|
||||||
import 'package:flutter/services.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 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'app/app.dart';
|
import 'app/app.dart';
|
||||||
@@ -13,6 +16,11 @@ import 'viewmodels/product_list_view_model.dart';
|
|||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
|
||||||
|
await initializeDateFormatting('nl_BE', null);
|
||||||
|
Intl.defaultLocale = 'nl_BE';
|
||||||
|
|
||||||
|
|
||||||
await SystemChrome.setPreferredOrientations([
|
await SystemChrome.setPreferredOrientations([
|
||||||
DeviceOrientation.landscapeLeft,
|
DeviceOrientation.landscapeLeft,
|
||||||
DeviceOrientation.landscapeRight,
|
DeviceOrientation.landscapeRight,
|
||||||
@@ -45,6 +53,9 @@ Future<void> main() async {
|
|||||||
barTabService: context.read<BarTabService>(),
|
barTabService: context.read<BarTabService>(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ChangeNotifierProvider<HistoryViewModel>(
|
||||||
|
create: (context) => HistoryViewModel(barTabService: context.read<BarTabService>()),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
child: const AppBootstrap(
|
child: const AppBootstrap(
|
||||||
child: KoolTabApp(),
|
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 '../database/app_database.dart';
|
||||||
import '../models/bar_tab.dart';
|
import '../models/bar_tab.dart';
|
||||||
|
import '../models/closed_tab.dart';
|
||||||
|
import '../models/closed_tab_item.dart';
|
||||||
import '../models/product.dart';
|
import '../models/product.dart';
|
||||||
import '../models/tab_item.dart';
|
import '../models/tab_item.dart';
|
||||||
|
|
||||||
@@ -25,7 +27,11 @@ abstract class BarTabService {
|
|||||||
required int quantity,
|
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<void> closeTab(String tabId);
|
||||||
|
|
||||||
|
Future<List<ClosedTab>> getClosedTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
class DriftBarTabService implements BarTabService {
|
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 {
|
Future<List<TabItem>> _getItemsForTab(String tabId) async {
|
||||||
final query = database.select(database.tabItems)
|
final query = database.select(database.tabItems)
|
||||||
..where((item) => item.tabId.equals(tabId))
|
..where((item) => item.tabId.equals(tabId))
|
||||||
@@ -201,14 +218,83 @@ class DriftBarTabService implements BarTabService {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> closeTab(String tabId) async {
|
Future<void> closeTab(String tabId) async {
|
||||||
final updateQuery = database.update(database.barTabs)
|
await database.transaction(() async {
|
||||||
..where((tab) => tab.id.equals(tabId));
|
final tabQuery = database.select(database.barTabs)
|
||||||
|
..where((tab) => tab.id.equals(tabId));
|
||||||
|
|
||||||
await updateQuery.write(
|
final tabRow = await tabQuery.getSingleOrNull();
|
||||||
BarTabsCompanion(
|
|
||||||
status: const Value('closed'),
|
if (tabRow == null) {
|
||||||
closedAt: Value(DateTime.now()),
|
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';
|
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(
|
final ThemeData darkTheme = ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
|
splashFactory: InkRipple.splashFactory,
|
||||||
|
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: const Color(0xFF7C4DFF),
|
seedColor: _violet,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
|
surface: _surface,
|
||||||
|
error: const Color(0xFFFF6B6B),
|
||||||
),
|
),
|
||||||
|
|
||||||
scaffoldBackgroundColor: const Color(0xFF121212),
|
scaffoldBackgroundColor: _bg,
|
||||||
canvasColor: const Color(0xFF121212),
|
canvasColor: _bg,
|
||||||
|
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: true,
|
centerTitle: false,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: _bg,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
titleTextStyle: TextStyle(
|
titleTextStyle: const TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: -0.3,
|
||||||
color: Colors.white,
|
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(
|
cardTheme: CardThemeData(
|
||||||
color: const Color(0xFF1E1E1E),
|
color: _surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.md,
|
||||||
|
vertical: AppSpacing.sm,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(18),
|
borderRadius: BorderRadius.circular(AppRadii.lg),
|
||||||
side: BorderSide(
|
side: const BorderSide(color: _outline, width: 1),
|
||||||
color: Colors.white.withOpacity(0.06),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
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(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
backgroundColor: const Color(0xFF7C4DFF),
|
backgroundColor: _violet,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 24,
|
horizontal: AppSpacing.xl,
|
||||||
vertical: 16,
|
vertical: AppSpacing.lg,
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
),
|
|
||||||
textStyle: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
|
textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
side: BorderSide(
|
side: const BorderSide(color: _outline, width: 1.4),
|
||||||
color: Colors.white.withOpacity(0.15),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 24,
|
horizontal: AppSpacing.xl,
|
||||||
vertical: 16,
|
vertical: AppSpacing.lg,
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
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(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFF242424),
|
fillColor: _surfaceRaised,
|
||||||
|
hintStyle: const TextStyle(color: Colors.white38),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 18,
|
horizontal: AppSpacing.lg,
|
||||||
vertical: 16,
|
vertical: AppSpacing.lg,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
borderSide: BorderSide(
|
borderSide: const BorderSide(color: _outline),
|
||||||
color: Colors.white.withOpacity(0.08),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
borderSide: const BorderSide(
|
borderSide: const BorderSide(color: _violet, width: 2),
|
||||||
color: Color(0xFF7C4DFF),
|
|
||||||
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(
|
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||||
backgroundColor: Color(0xFF7C4DFF),
|
backgroundColor: _violet,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
),
|
),
|
||||||
|
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
backgroundColor: const Color(0xFF1A1A1A),
|
backgroundColor: _surface,
|
||||||
indicatorColor: const Color(0xFF7C4DFF).withOpacity(0.25),
|
indicatorColor: _violet.withOpacity(0.25),
|
||||||
labelTextStyle: WidgetStatePropertyAll(
|
labelTextStyle: const WidgetStatePropertyAll(
|
||||||
TextStyle(
|
TextStyle(fontWeight: FontWeight.w600),
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
listTileTheme: ListTileThemeData(
|
listTileTheme: ListTileThemeData(
|
||||||
tileColor: const Color(0xFF1E1E1E),
|
tileColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
),
|
),
|
||||||
iconColor: const Color(0xFFB39DFF),
|
iconColor: _violetSoft,
|
||||||
textColor: Colors.white,
|
textColor: Colors.white,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.md,
|
||||||
|
vertical: AppSpacing.xs,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
snackBarTheme: SnackBarThemeData(
|
snackBarTheme: SnackBarThemeData(
|
||||||
backgroundColor: const Color(0xFF2A2A2A),
|
backgroundColor: _surfaceRaised,
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
|
elevation: 4,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
|
side: const BorderSide(color: _outline),
|
||||||
),
|
),
|
||||||
|
contentTextStyle: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
|
|
||||||
dividerTheme: DividerThemeData(
|
dividerTheme: const DividerThemeData(color: _outline, thickness: 1, space: 1),
|
||||||
color: Colors.white.withOpacity(0.08),
|
|
||||||
thickness: 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(
|
textTheme: const TextTheme(
|
||||||
displayLarge: TextStyle(
|
displayLarge: TextStyle(color: Colors.white, fontWeight: FontWeight.w800),
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
headlineMedium: TextStyle(
|
headlineMedium: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: -0.3,
|
||||||
),
|
),
|
||||||
titleLarge: TextStyle(
|
titleLarge: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
),
|
||||||
|
titleMedium: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
bodyLarge: TextStyle(
|
bodyLarge: TextStyle(color: Colors.white70, fontSize: 16, height: 1.5),
|
||||||
color: Colors.white70,
|
bodyMedium: TextStyle(color: Colors.white60, height: 1.4),
|
||||||
fontSize: 16,
|
bodySmall: TextStyle(color: Colors.white38, height: 1.3),
|
||||||
height: 1.5,
|
labelLarge: TextStyle(color: Colors.white, fontWeight: FontWeight.w700),
|
||||||
),
|
|
||||||
bodyMedium: TextStyle(
|
|
||||||
color: Colors.white60,
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
labelLarge: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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(
|
final ThemeData neoBrutalDarkTheme = ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
|
|
||||||
colorScheme: const ColorScheme.dark(
|
colorScheme: const ColorScheme.dark(
|
||||||
primary: Color(0xFFFFD60A), // Bright yellow
|
primary: _nbYellow,
|
||||||
secondary: Color(0xFF00E5FF), // Cyan
|
secondary: _nbCyan,
|
||||||
surface: Color(0xFF1A1A1A),
|
surface: _nbSurface,
|
||||||
error: Color(0xFFFF5252),
|
error: _nbPink,
|
||||||
),
|
),
|
||||||
|
|
||||||
scaffoldBackgroundColor: const Color(0xFF0E0E0E),
|
scaffoldBackgroundColor: _nbBg,
|
||||||
canvasColor: const Color(0xFF0E0E0E),
|
canvasColor: _nbBg,
|
||||||
|
|
||||||
textTheme: const TextTheme(
|
textTheme: const TextTheme(
|
||||||
displayLarge: TextStyle(
|
displayLarge: TextStyle(
|
||||||
fontSize: 48,
|
fontSize: 48,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
letterSpacing: -1,
|
letterSpacing: -1.5,
|
||||||
),
|
),
|
||||||
headlineMedium: TextStyle(
|
headlineMedium: TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
letterSpacing: -0.5,
|
||||||
),
|
),
|
||||||
titleLarge: TextStyle(
|
titleLarge: TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
titleMedium: TextStyle(
|
||||||
|
fontSize: 17,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
bodyLarge: TextStyle(
|
bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
|
||||||
fontSize: 16,
|
bodyMedium: TextStyle(fontSize: 14, color: Colors.white70),
|
||||||
fontWeight: FontWeight.w600,
|
bodySmall: TextStyle(fontSize: 12, color: Colors.white54, fontWeight: FontWeight.w600),
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
bodyMedium: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.white70,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: Color(0xFF0E0E0E),
|
backgroundColor: _nbBg,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
@@ -220,108 +329,357 @@ final ThemeData neoBrutalDarkTheme = ThemeData(
|
|||||||
),
|
),
|
||||||
|
|
||||||
cardTheme: CardThemeData(
|
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,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(10),
|
||||||
side: const BorderSide(
|
side: const BorderSide(color: Colors.white, width: 3),
|
||||||
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(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFFFFD60A),
|
|
||||||
foregroundColor: Colors.black,
|
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowColor: Colors.transparent,
|
backgroundColor: _violetL,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 24,
|
horizontal: AppSpacing.xl,
|
||||||
vertical: 18,
|
vertical: AppSpacing.lg,
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
side: const BorderSide(
|
|
||||||
color: Colors.black,
|
|
||||||
width: 3,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
textStyle: const TextStyle(
|
textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
|
||||||
fontWeight: FontWeight.w900,
|
),
|
||||||
fontSize: 16,
|
),
|
||||||
|
|
||||||
|
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(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFF242424),
|
fillColor: _surfaceRaisedL,
|
||||||
contentPadding: const EdgeInsets.all(18),
|
hintStyle: const TextStyle(color: Colors.black38),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.lg,
|
||||||
|
vertical: AppSpacing.lg,
|
||||||
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide.none,
|
||||||
color: Colors.white,
|
|
||||||
width: 3,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
borderSide: const BorderSide(
|
borderSide: const BorderSide(color: _outlineL),
|
||||||
color: Colors.white,
|
|
||||||
width: 3,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
borderSide: const BorderSide(
|
borderSide: const BorderSide(color: _violetL, width: 2),
|
||||||
color: Color(0xFFFFD60A),
|
|
||||||
width: 4,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
dialogTheme: DialogThemeData(
|
||||||
backgroundColor: Color(0xFFFF5252),
|
backgroundColor: _surfaceL,
|
||||||
foregroundColor: Colors.white,
|
surfaceTintColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 8,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.circular(AppRadii.lg),
|
||||||
side: BorderSide(
|
),
|
||||||
color: Colors.white,
|
titleTextStyle: const TextStyle(
|
||||||
width: 3,
|
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(
|
snackBarTheme: SnackBarThemeData(
|
||||||
backgroundColor: const Color(0xFF242424),
|
backgroundColor: Colors.black87,
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
side: const BorderSide(
|
|
||||||
color: Colors.white,
|
|
||||||
width: 3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
elevation: 4,
|
||||||
|
|
||||||
dividerTheme: const DividerThemeData(
|
|
||||||
color: Colors.white,
|
|
||||||
thickness: 3,
|
|
||||||
),
|
|
||||||
|
|
||||||
listTileTheme: ListTileThemeData(
|
|
||||||
tileColor: const Color(0xFF242424),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||||
side: const BorderSide(
|
|
||||||
color: Colors.white,
|
|
||||||
width: 3,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
textColor: Colors.white,
|
contentTextStyle: const TextStyle(color: Colors.white),
|
||||||
iconColor: const Color(0xFFFFD60A),
|
),
|
||||||
|
|
||||||
|
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);
|
await barTabService.closeTab(tab.id);
|
||||||
|
|
||||||
_selectedTabId = null;
|
|
||||||
await _reloadTabs();
|
await _reloadTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeTab(String tabId) async {
|
Future<void> closeTab(String tabId) async {
|
||||||
await barTabService.closeTab(tabId);
|
await barTabService.closeTab(tabId);
|
||||||
|
|
||||||
if (_selectedTabId == tabId) {
|
|
||||||
_selectedTabId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _reloadTabs();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+501
-186
@@ -22,34 +22,65 @@ class BarScreenView extends StatelessWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
title: const Text('Bar Tabs'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Manage products',
|
tooltip: 'Manage products',
|
||||||
onPressed: () => context.go('/products'),
|
onPressed: () => context.go('/products'),
|
||||||
icon: const Icon(Icons.inventory_2_outlined),
|
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(
|
IconButton(
|
||||||
tooltip: 'Refresh',
|
tooltip: 'Refresh',
|
||||||
onPressed: viewModel.load,
|
onPressed: viewModel.load,
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh_rounded),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
body: Builder(
|
body: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
if (viewModel.isLoading) {
|
if (viewModel.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.errorMessage != null) {
|
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(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 2,
|
||||||
child: _ProductGrid(
|
child: _ProductGrid(
|
||||||
products: productsViewModel.products,
|
products: productsViewModel.products,
|
||||||
hasSelectedTab: viewModel.selectedTab != null,
|
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(
|
Expanded(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
child: _TabPanel(
|
child: _TabPanel(
|
||||||
@@ -101,12 +132,12 @@ class BarScreenView extends StatelessWidget {
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Customer / group name',
|
labelText: 'Customer / group name',
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
),
|
||||||
onSubmitted: (value) {
|
onSubmitted: (value) {
|
||||||
Navigator.of(dialogContext).pop(value);
|
Navigator.of(dialogContext).pop(value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
@@ -141,16 +172,17 @@ class BarScreenView extends StatelessWidget {
|
|||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Close ${tab.customerName}ʼs tab?'),
|
title: Text('Close ${tab.customerName}ʼs tab?'),
|
||||||
content: Text(
|
content: Text('Current total: ${tab.formattedTotal}\n\n'),
|
||||||
'Current total: ${tab.formattedTotal}\n\n'
|
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
'Payments are not handled yet, so this only marks the tab as closed.',
|
|
||||||
),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(dialogContext).colorScheme.error,
|
||||||
|
),
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||||
child: const Text('Close tab'),
|
child: const Text('Close tab'),
|
||||||
),
|
),
|
||||||
@@ -165,6 +197,10 @@ class BarScreenView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Product grid
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class _ProductGrid extends StatelessWidget {
|
class _ProductGrid extends StatelessWidget {
|
||||||
final List<Product> products;
|
final List<Product> products;
|
||||||
final bool hasSelectedTab;
|
final bool hasSelectedTab;
|
||||||
@@ -178,15 +214,36 @@ class _ProductGrid extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
if (products.isEmpty) {
|
if (products.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.inventory_2_outlined, size: 48),
|
Container(
|
||||||
const SizedBox(height: 12),
|
padding: const EdgeInsets.all(20),
|
||||||
const Text('No products yet.'),
|
decoration: BoxDecoration(
|
||||||
const SizedBox(height: 12),
|
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(
|
FilledButton.icon(
|
||||||
onPressed: () => context.go('/products/new'),
|
onPressed: () => context.go('/products/new'),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
@@ -197,32 +254,79 @@ class _ProductGrid extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return LayoutBuilder(
|
return Column(
|
||||||
builder: (context, constraints) {
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
final columns = ((constraints.maxWidth / 170).floor())
|
children: [
|
||||||
.clamp(3, 6)
|
Padding(
|
||||||
.toInt();
|
padding: const EdgeInsets.fromLTRB(20, 18, 20, 4),
|
||||||
|
child: Row(
|
||||||
return GridView.builder(
|
children: [
|
||||||
padding: const EdgeInsets.all(16),
|
Text('Products', style: Theme.of(context).textTheme.titleLarge),
|
||||||
itemCount: products.length,
|
const SizedBox(width: 10),
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
if (!hasSelectedTab)
|
||||||
crossAxisCount: columns,
|
Flexible(
|
||||||
crossAxisSpacing: 12,
|
child: Container(
|
||||||
mainAxisSpacing: 12,
|
padding: const EdgeInsets.symmetric(
|
||||||
childAspectRatio: 1,
|
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(
|
return GridView.builder(
|
||||||
product: product,
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||||
enabled: hasSelectedTab,
|
itemCount: products.length,
|
||||||
onTap: () => onProductTap(product),
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
final theme = Theme.of(context);
|
||||||
clipBehavior: Clip.antiAlias,
|
final scheme = theme.colorScheme;
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: InkWell(
|
return Container(
|
||||||
onTap: enabled ? onTap : null,
|
decoration: BoxDecoration(
|
||||||
child: Opacity(
|
borderRadius: BorderRadius.circular(16),
|
||||||
opacity: enabled ? 1 : 0.45,
|
border: Border.all(color: scheme.onSurface.withOpacity(0.08), width: 1),
|
||||||
child: _hasImage
|
),
|
||||||
? Image.file(
|
child: ClipRRect(
|
||||||
File(product.imagePath!),
|
borderRadius: BorderRadius.circular(15),
|
||||||
fit: BoxFit.scaleDown,
|
child: Material(
|
||||||
width: double.infinity,
|
color: theme.cardTheme.color ?? scheme.surface,
|
||||||
height: double.infinity,
|
child: InkWell(
|
||||||
)
|
onTap: enabled ? onTap : null,
|
||||||
: const Center(
|
child: Opacity(
|
||||||
child: Stack(
|
opacity: enabled ? 1 : 0.4,
|
||||||
children: [
|
child: Stack(
|
||||||
Icon(Icons.image_not_supported_outlined, size: 42),
|
fit: StackFit.expand,
|
||||||
Text("No Image found!"),
|
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 {
|
class _TabPanel extends StatefulWidget {
|
||||||
final List<BarTab> tabs;
|
final List<BarTab> tabs;
|
||||||
final BarTab? selectedTab;
|
final BarTab? selectedTab;
|
||||||
final String? selectedTabId;
|
final String? selectedTabId;
|
||||||
final VoidCallback onNewTabPressed;
|
final VoidCallback onNewTabPressed;
|
||||||
final ValueChanged<String> onTabSelected;
|
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 VoidCallback onCloseTabPressed;
|
||||||
final ValueChanged<String> onTabClosed;
|
final ValueChanged<String> onTabClosed;
|
||||||
|
|
||||||
@@ -315,79 +468,130 @@ class _TabPanelState extends State<_TabPanel> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
final filteredTabs = widget.tabs.where((tab) {
|
final filteredTabs = widget.tabs.where((tab) {
|
||||||
return tab.customerName.toLowerCase().contains(
|
return tab.customerName.toLowerCase().contains(
|
||||||
_searchQuery.toLowerCase(),
|
_searchQuery.toLowerCase(),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
return Padding(
|
return DecoratedBox(
|
||||||
padding: const EdgeInsets.all(12),
|
// A step darker/lighter than the main surface, whichever direction
|
||||||
child: Column(
|
// the active theme goes — matches how it read against the dark
|
||||||
children: [
|
// surface color originally.
|
||||||
SizedBox(
|
decoration: BoxDecoration(color: scheme.surfaceContainerLow),
|
||||||
width: double.infinity,
|
child: Padding(
|
||||||
child: FilledButton.icon(
|
padding: const EdgeInsets.all(16),
|
||||||
onPressed: widget.onNewTabPressed,
|
child: Column(
|
||||||
icon: const Icon(Icons.add),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
label: const Text('Open tab'),
|
children: [
|
||||||
),
|
Row(
|
||||||
),
|
children: [
|
||||||
|
Expanded(
|
||||||
const SizedBox(height: 12),
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
TextField(
|
onChanged: (value) {
|
||||||
controller: _searchController,
|
setState(() {
|
||||||
onChanged: (value) {
|
_searchQuery = value;
|
||||||
setState(() {
|
});
|
||||||
_searchQuery = value;
|
},
|
||||||
});
|
decoration: const InputDecoration(
|
||||||
},
|
hintText: 'Search by name…',
|
||||||
decoration: const InputDecoration(
|
prefixIcon: Icon(Icons.search_rounded, size: 20),
|
||||||
hintText: 'Search name...',
|
isDense: true,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
|
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 {
|
class _OpenTabsList extends StatelessWidget {
|
||||||
final List<BarTab> tabs;
|
final List<BarTab> tabs;
|
||||||
final String? selectedTabId;
|
final String? selectedTabId;
|
||||||
@@ -404,22 +608,30 @@ class _OpenTabsList extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (tabs.isEmpty) {
|
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(
|
return ListView.separated(
|
||||||
itemCount: tabs.length,
|
itemCount: tabs.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final tab = tabs[index];
|
final tab = tabs[index];
|
||||||
final selected = tab.id == selectedTabId;
|
final selected = tab.id == selectedTabId;
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
final primary = scheme.primary;
|
||||||
|
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
child: Slidable(
|
child: Slidable(
|
||||||
key: ValueKey(tab.id),
|
key: ValueKey(tab.id),
|
||||||
endActionPane: ActionPane(
|
endActionPane: ActionPane(
|
||||||
motion: const DrawerMotion(),
|
motion: const DrawerMotion(),
|
||||||
extentRatio: 0.65,
|
extentRatio: 0.6,
|
||||||
children: [
|
children: [
|
||||||
SlidableAction(
|
SlidableAction(
|
||||||
onPressed: (_) {
|
onPressed: (_) {
|
||||||
@@ -433,28 +645,63 @@ class _OpenTabsList extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
SlidableAction(
|
SlidableAction(
|
||||||
onPressed: (_) => onTabClosed(tab.id),
|
onPressed: (_) => onTabClosed(tab.id),
|
||||||
icon: Icons.close,
|
icon: Icons.close_rounded,
|
||||||
label: 'Close',
|
label: 'Close',
|
||||||
backgroundColor: Theme.of(context).colorScheme.error,
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Card(
|
child: AnimatedContainer(
|
||||||
margin: EdgeInsets.zero,
|
duration: const Duration(milliseconds: 150),
|
||||||
color: selected
|
decoration: BoxDecoration(
|
||||||
? Theme.of(context).colorScheme.primaryContainer
|
color: selected
|
||||||
: null,
|
? primary.withOpacity(0.14)
|
||||||
child: ListTile(
|
: scheme.onSurface.withOpacity(0.04),
|
||||||
dense: true,
|
borderRadius: BorderRadius.circular(14),
|
||||||
selected: selected,
|
border: Border.all(
|
||||||
title: Text(
|
color: selected
|
||||||
tab.customerName,
|
? primary.withOpacity(0.6)
|
||||||
maxLines: 1,
|
: scheme.onSurface.withOpacity(0.06),
|
||||||
overflow: TextOverflow.ellipsis,
|
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 {
|
class _SelectedTabDetails extends StatelessWidget {
|
||||||
final BarTab tab;
|
final BarTab tab;
|
||||||
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
|
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
|
||||||
@@ -477,52 +727,89 @@ class _SelectedTabDetails extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
tab.formattedTotal,
|
tab.customerName,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
FilledButton(
|
Divider()
|
||||||
onPressed: onCloseTabPressed,
|
|
||||||
child: const Text('Close'),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -548,7 +837,9 @@ class _TabItemRow extends StatelessWidget {
|
|||||||
item.productName,
|
item.productName,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'${item.quantity} × ${item.formattedUnitPrice}',
|
'${item.quantity} × ${item.formattedUnitPrice}',
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
@@ -556,20 +847,44 @@ class _TabItemRow extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
Container(
|
||||||
visualDensity: VisualDensity.compact,
|
decoration: BoxDecoration(
|
||||||
onPressed: () => onQuantityChanged(item, item.quantity - 1),
|
color: scheme.onSurface.withOpacity(0.05),
|
||||||
icon: const Icon(Icons.remove_circle_outline),
|
borderRadius: BorderRadius.circular(999),
|
||||||
),
|
),
|
||||||
Text('${item.quantity}'),
|
child: Row(
|
||||||
IconButton(
|
mainAxisSize: MainAxisSize.min,
|
||||||
visualDensity: VisualDensity.compact,
|
children: [
|
||||||
onPressed: () => onQuantityChanged(item, item.quantity + 1),
|
IconButton(
|
||||||
icon: const Icon(Icons.add_circle_outline),
|
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(
|
SizedBox(
|
||||||
width: 72,
|
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
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
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:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -25,6 +41,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
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:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -33,6 +97,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
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:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -89,6 +177,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.9"
|
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:
|
drift:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -97,6 +193,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.34.0"
|
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:
|
drift_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -224,6 +328,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "17.3.0"
|
version: "17.3.0"
|
||||||
|
graphs:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: graphs
|
||||||
|
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
hooks:
|
hooks:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -240,6 +352,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.0"
|
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:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -312,6 +432,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
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:
|
jni:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -328,6 +464,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -504,6 +648,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
pool:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pool
|
||||||
|
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.2"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -520,6 +672,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
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:
|
record_use:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -528,11 +696,35 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -565,6 +757,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0+eol"
|
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:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -581,6 +781,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
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:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -637,6 +845,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.2.0"
|
version: "15.2.0"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -645,6 +861,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
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:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ dependencies:
|
|||||||
image_picker: ^1.2.3
|
image_picker: ^1.2.3
|
||||||
path: ^1.9.1
|
path: ^1.9.1
|
||||||
flutter_slidable: ^4.0.3
|
flutter_slidable: ^4.0.3
|
||||||
|
intl: ^0.20.3
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -54,6 +56,8 @@ dev_dependencies:
|
|||||||
# package. See that file for information about deactivating specific lint
|
# package. See that file for information about deactivating specific lint
|
||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^6.0.0
|
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
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|||||||
Reference in New Issue
Block a user