fix: resolve Sheetifye Excel viewer integration, Riverpod autoDispose type bounds, and Dashboard initState error
This commit is contained in:
1 parent
93f419f6d1
commit
db42cf3050
11 files changed
+78
-635
No files matched your search
+5
-2
@@ -158,9 +158,12 @@ Files backup/restore - mirror mode or zip -->
|
||||
- Document preveiew modal -> make it fullscreen modal -> add "add to print list" icon button -> move the zoom in/out inside of the pdf or image viewer -> add "open with" icon button for excel files to open external excel viewer
|
||||
- hold feature to see more menu in android add "view student profile" -->
|
||||
|
||||
# ARCHVES SCREEN
|
||||
<!-- # ARCHVES SCREEN
|
||||
- From "DOCUMENTS SCREEN", add hold feature for android and right click for windows to see more menu, add multi select and its multi select menu, and FAB print list for android, button print list for windows
|
||||
- fix grid view in all archived docs tab
|
||||
- fix grid view in all archived docs tab -->
|
||||
|
||||
# DOCUMENTS SCREEN
|
||||
- document preview modal -> use sheetifye package for excel viewer
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ final userHistoryQueryProvider =
|
||||
UserHistoryQueryParams
|
||||
>(UserHistoryQueryNotifier.new);
|
||||
|
||||
class UserHistoryQueryNotifier extends Notifier<UserHistoryQueryParams> {
|
||||
class UserHistoryQueryNotifier
|
||||
extends AutoDisposeNotifier<UserHistoryQueryParams> {
|
||||
@override
|
||||
UserHistoryQueryParams build() => const UserHistoryQueryParams();
|
||||
|
||||
|
||||
@@ -5,13 +5,8 @@ import '../../domain/entities/user_model.dart';
|
||||
import 'document_provider.dart';
|
||||
import 'archives_provider.dart';
|
||||
import 'student_provider.dart' hide academicYearsProvider;
|
||||
import 'users_provider.dart';
|
||||
import 'notification_provider.dart';
|
||||
import 'navigation_provider.dart';
|
||||
import 'dashboard_provider.dart';
|
||||
import 'setup_provider.dart';
|
||||
import 'reports_provider.dart';
|
||||
import 'ocr_provider.dart';
|
||||
import 'connected_users_provider.dart';
|
||||
|
||||
final authRepositoryProvider = Provider<AuthRepository>(
|
||||
@@ -102,7 +97,7 @@ class AuthNotifier extends AsyncNotifier<UserModel?> {
|
||||
final repository = ref.read(authRepositoryProvider);
|
||||
final updatedUser = await repository.getProfile();
|
||||
state = AsyncData(updatedUser);
|
||||
} catch (e, stack) {
|
||||
} catch (e) {
|
||||
// Don't emit error state to prevent UI disruptions on background refresh failures
|
||||
print('Background refresh failed: $e');
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ final activityQueryProvider =
|
||||
ActivityQueryNotifier.new,
|
||||
);
|
||||
|
||||
class ActivityQueryNotifier extends Notifier<ActivityQueryParams> {
|
||||
class ActivityQueryNotifier extends AutoDisposeNotifier<ActivityQueryParams> {
|
||||
@override
|
||||
ActivityQueryParams build() => const ActivityQueryParams();
|
||||
|
||||
|
||||
@@ -62,10 +62,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
|
||||
ref.invalidate(dashboardDataProvider);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
ref.invalidate(dashboardDataProvider);
|
||||
ref.read(notificationsProvider.notifier).refreshNotifications();
|
||||
|
||||
if (ref.read(activeTabProvider) == 'Dashboard') {
|
||||
|
||||
@@ -107,23 +107,26 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
}
|
||||
});
|
||||
|
||||
// If a specific student was passed, jump to Folders tab and open that folder
|
||||
final initialFolder = ref.read(openedFolderProvider);
|
||||
if (initialFolder != null) {
|
||||
_openedFolderStudentId = initialFolder.id;
|
||||
_openedFolderName = initialFolder.name;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
// If a specific student was passed, jump to Folders tab and open that folder
|
||||
final initialFolder = ref.read(openedFolderProvider);
|
||||
if (initialFolder != null) {
|
||||
setState(() {
|
||||
_openedFolderStudentId = initialFolder.id;
|
||||
_openedFolderName = initialFolder.name;
|
||||
});
|
||||
ref.read(documentQueryProvider.notifier).setStudentId(initialFolder.id);
|
||||
});
|
||||
} else if (widget.initialStudentId != null) {
|
||||
_openedFolderStudentId = widget.initialStudentId;
|
||||
_openedFolderName = 'Student Documents';
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
} else if (widget.initialStudentId != null) {
|
||||
setState(() {
|
||||
_openedFolderStudentId = widget.initialStudentId;
|
||||
_openedFolderName = 'Student Documents';
|
||||
});
|
||||
ref
|
||||
.read(documentQueryProvider.notifier)
|
||||
.setStudentId(widget.initialStudentId);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_tabController.addListener(() {
|
||||
setState(() {}); // Ensure header title updates immediately on tab change
|
||||
|
||||
@@ -61,7 +61,6 @@ class _DocumentPreviewDialog extends ConsumerStatefulWidget {
|
||||
class _DocumentPreviewDialogState
|
||||
extends ConsumerState<_DocumentPreviewDialog> {
|
||||
bool _imageError = false;
|
||||
bool _imageLoaded = false;
|
||||
String? _token;
|
||||
bool _isOpeningExternal = false;
|
||||
|
||||
@@ -71,13 +70,13 @@ class _DocumentPreviewDialogState
|
||||
|
||||
void _zoomImageIn() {
|
||||
final matrix = _imageTransformationController.value.clone();
|
||||
matrix.scale(1.25, 1.25, 1.0);
|
||||
matrix.scaleByDouble(1.25, 1.25, 1.0, 1.0);
|
||||
_imageTransformationController.value = matrix;
|
||||
}
|
||||
|
||||
void _zoomImageOut() {
|
||||
final matrix = _imageTransformationController.value.clone();
|
||||
matrix.scale(1 / 1.25, 1 / 1.25, 1.0);
|
||||
matrix.scaleByDouble(1 / 1.25, 1 / 1.25, 1.0, 1.0);
|
||||
_imageTransformationController.value = matrix;
|
||||
}
|
||||
|
||||
@@ -93,9 +92,6 @@ class _DocumentPreviewDialogState
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.localFile != null) {
|
||||
_imageLoaded = true;
|
||||
}
|
||||
_loadToken();
|
||||
}
|
||||
|
||||
@@ -447,16 +443,7 @@ class _DocumentPreviewDialogState
|
||||
_fileUrl,
|
||||
fit: BoxFit.contain,
|
||||
loadingBuilder: (ctx, child, progress) {
|
||||
if (progress == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((
|
||||
_,
|
||||
) {
|
||||
if (mounted) {
|
||||
setState(() => _imageLoaded = true);
|
||||
}
|
||||
});
|
||||
return child;
|
||||
}
|
||||
if (progress == null) return child;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -480,7 +467,7 @@ class _DocumentPreviewDialogState
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (_, __, ___) {
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => _imageError = true);
|
||||
});
|
||||
|
||||
@@ -1,100 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:excel/excel.dart' hide Border;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:sheetifye/sheetifye.dart';
|
||||
import '../../../../core/constants/app_colors.dart';
|
||||
|
||||
/// Background isolate function to decode CSV bytes without blocking UI thread.
|
||||
Map<String, List<List<String>>> _decodeCsvInBackground(List<int> bytes) {
|
||||
final content = utf8.decode(bytes);
|
||||
final lines = content.split('\n');
|
||||
final List<List<String>> rows = [];
|
||||
for (var line in lines) {
|
||||
if (line.trim().isEmpty) continue;
|
||||
if (rows.length >= 500) break; // Cap at 500 rows for mobile/desktop instant preview
|
||||
final cols = line.split(',').map((e) => e.trim()).toList();
|
||||
if (cols.length > 30) {
|
||||
rows.add(cols.sublist(0, 30));
|
||||
} else {
|
||||
rows.add(cols);
|
||||
}
|
||||
}
|
||||
return {'Sheet 1': rows};
|
||||
}
|
||||
|
||||
/// Background isolate function to decode Excel bytes without blocking UI thread.
|
||||
Map<String, List<List<String>>> _decodeExcelInBackground(List<int> bytes) {
|
||||
final excel = Excel.decodeBytes(bytes);
|
||||
final Map<String, List<List<String>>> parsedSheets = {};
|
||||
|
||||
for (final table in excel.tables.keys) {
|
||||
final sheet = excel.tables[table];
|
||||
if (sheet == null || sheet.rows.isEmpty) continue;
|
||||
|
||||
// 1st pass: find the actual last non-empty row and column in the sheet
|
||||
int maxNonEmptyRow = -1;
|
||||
int maxNonEmptyCol = -1;
|
||||
final int rowLimit = math.min(sheet.rows.length, 500); // Cap at 500 rows
|
||||
|
||||
for (int r = 0; r < rowLimit; r++) {
|
||||
final row = sheet.rows[r];
|
||||
final int colLimit = math.min(row.length, 30); // Cap at 30 columns
|
||||
for (int c = 0; c < colLimit; c++) {
|
||||
final cell = row[c];
|
||||
if (cell != null &&
|
||||
cell.value != null &&
|
||||
cell.value.toString().trim().isNotEmpty) {
|
||||
if (r > maxNonEmptyRow) maxNonEmptyRow = r;
|
||||
if (c > maxNonEmptyCol) maxNonEmptyCol = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maxNonEmptyRow == -1 || maxNonEmptyCol == -1) {
|
||||
continue; // Skip completely empty sheets
|
||||
}
|
||||
|
||||
// 2nd pass: extract ONLY up to maxNonEmptyRow and maxNonEmptyCol
|
||||
final List<List<String>> sheetRows = [];
|
||||
for (int r = 0; r <= maxNonEmptyRow; r++) {
|
||||
final row = sheet.rows[r];
|
||||
final List<String> rowCells = [];
|
||||
for (int c = 0; c <= maxNonEmptyCol; c++) {
|
||||
String valStr = '';
|
||||
if (c < row.length) {
|
||||
final cell = row[c];
|
||||
if (cell != null && cell.value != null) {
|
||||
final val = cell.value;
|
||||
try {
|
||||
if (val is TextCellValue) {
|
||||
valStr = val.value.toString();
|
||||
} else if (val is IntCellValue) {
|
||||
valStr = val.value.toString();
|
||||
} else if (val is DoubleCellValue) {
|
||||
valStr = val.value.toString();
|
||||
} else if (val is BoolCellValue) {
|
||||
valStr = val.value ? 'TRUE' : 'FALSE';
|
||||
} else {
|
||||
valStr = val.toString();
|
||||
}
|
||||
} catch (_) {
|
||||
valStr = val.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
rowCells.add(valStr);
|
||||
}
|
||||
sheetRows.add(rowCells);
|
||||
}
|
||||
parsedSheets[table] = sheetRows;
|
||||
}
|
||||
return parsedSheets;
|
||||
}
|
||||
|
||||
class ExcelViewerWidget extends StatefulWidget {
|
||||
final File? localFile;
|
||||
final String? networkUrl;
|
||||
@@ -116,17 +27,7 @@ class ExcelViewerWidget extends StatefulWidget {
|
||||
class _ExcelViewerWidgetState extends State<ExcelViewerWidget> {
|
||||
bool _isLoading = true;
|
||||
String? _errorMessage;
|
||||
|
||||
// Workbook data
|
||||
Map<String, List<List<String>>> _sheetData = {};
|
||||
List<String> _sheetNames = [];
|
||||
String? _activeSheetName;
|
||||
|
||||
// Search query
|
||||
String _searchQuery = '';
|
||||
|
||||
// Scroll controllers for synchronized scrolling
|
||||
final ScrollController _horizontalController = ScrollController();
|
||||
Uint8List? _fileBytes;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -134,12 +35,6 @@ class _ExcelViewerWidgetState extends State<ExcelViewerWidget> {
|
||||
_loadSpreadsheet();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_horizontalController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadSpreadsheet() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
@@ -147,7 +42,7 @@ class _ExcelViewerWidgetState extends State<ExcelViewerWidget> {
|
||||
});
|
||||
|
||||
try {
|
||||
List<int> bytes;
|
||||
Uint8List bytes;
|
||||
if (widget.localFile != null) {
|
||||
bytes = await widget.localFile!.readAsBytes();
|
||||
} else if (widget.networkUrl != null && widget.networkUrl!.isNotEmpty) {
|
||||
@@ -163,49 +58,27 @@ class _ExcelViewerWidgetState extends State<ExcelViewerWidget> {
|
||||
if (response.data == null) {
|
||||
throw Exception('Failed to download spreadsheet file.');
|
||||
}
|
||||
bytes = response.data!;
|
||||
bytes = Uint8List.fromList(response.data!);
|
||||
} else {
|
||||
throw Exception('No file source provided.');
|
||||
}
|
||||
|
||||
final lowerName = widget.fileName.toLowerCase();
|
||||
Map<String, List<List<String>>> parsedSheets;
|
||||
if (lowerName.endsWith('.csv')) {
|
||||
// Run CSV parsing in background isolate
|
||||
parsedSheets = await compute(_decodeCsvInBackground, bytes);
|
||||
} else {
|
||||
// Run Excel parsing in background isolate to avoid Windows UI freeze
|
||||
parsedSheets = await compute(_decodeExcelInBackground, bytes);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_sheetData = parsedSheets;
|
||||
_sheetNames = parsedSheets.keys.toList();
|
||||
_activeSheetName = _sheetNames.isNotEmpty ? _sheetNames.first : null;
|
||||
_fileBytes = bytes;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = 'Could not read spreadsheet: $e';
|
||||
_errorMessage = 'Could not load spreadsheet: $e';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _columnLetter(int index) {
|
||||
String name = '';
|
||||
int idx = index;
|
||||
while (idx >= 0) {
|
||||
name = String.fromCharCode((idx % 26) + 65) + name;
|
||||
idx = (idx ~/ 26) - 1;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
@@ -224,7 +97,7 @@ class _ExcelViewerWidgetState extends State<ExcelViewerWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
if (_errorMessage != null) {
|
||||
if (_errorMessage != null || _fileBytes == null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
@@ -243,7 +116,7 @@ class _ExcelViewerWidgetState extends State<ExcelViewerWidget> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
_errorMessage ?? 'Unable to display spreadsheet',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
@@ -256,295 +129,9 @@ class _ExcelViewerWidgetState extends State<ExcelViewerWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
final List<List<String>> activeRows =
|
||||
_activeSheetName != null
|
||||
? (_sheetData[_activeSheetName] ?? <List<String>>[])
|
||||
: <List<String>>[];
|
||||
final List<List<String>> filteredRows =
|
||||
_searchQuery.isEmpty
|
||||
? activeRows
|
||||
: activeRows.where((row) {
|
||||
return row.any(
|
||||
(cell) =>
|
||||
cell.toLowerCase().contains(_searchQuery.toLowerCase()),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// Determine max columns in this sheet (capped at 50 to prevent horizontal bloat)
|
||||
int maxCols = 0;
|
||||
for (final r in filteredRows) {
|
||||
if (r.length > maxCols) maxCols = r.length;
|
||||
}
|
||||
if (maxCols == 0) maxCols = 1;
|
||||
maxCols = math.min(maxCols, 50);
|
||||
|
||||
const double rowNumberWidth = 50.0;
|
||||
const double cellWidth = 120.0;
|
||||
final double totalTableWidth = rowNumberWidth + (maxCols * cellWidth);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Top search and statistics toolbar
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.table_chart_rounded,
|
||||
size: 18,
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${filteredRows.length} Rows × $maxCols Columns',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// Search field
|
||||
SizedBox(
|
||||
width: widget.isMobile ? 140 : 220,
|
||||
height: 34,
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search table...',
|
||||
hintStyle: const TextStyle(fontSize: 12),
|
||||
prefixIcon: const Icon(Icons.search, size: 16),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 0,
|
||||
),
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
onChanged: (val) {
|
||||
setState(() => _searchQuery = val);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Main table grid - using Scrollbar and virtualized ListView.builder for instant rendering
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
child: Scrollbar(
|
||||
controller: _horizontalController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: _horizontalController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
width: totalTableWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
// Fixed Column Header Row (A, B, C...)
|
||||
Container(
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.grey.shade400,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Row number corner cell
|
||||
Container(
|
||||
width: rowNumberWidth,
|
||||
alignment: Alignment.center,
|
||||
color: Colors.grey.shade300,
|
||||
child: const Text(
|
||||
'#',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (int c = 0; c < maxCols; c++)
|
||||
Container(
|
||||
width: cellWidth,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_columnLetter(c),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Virtualized Data Rows - builds ONLY visible rows instantly
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: filteredRows.length,
|
||||
itemBuilder: (context, r) {
|
||||
final rowCells = filteredRows[r];
|
||||
return Container(
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
r % 2 == 0
|
||||
? Colors.white
|
||||
: Colors.grey.shade50,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Row Number (1, 2, 3...)
|
||||
Container(
|
||||
width: rowNumberWidth,
|
||||
alignment: Alignment.center,
|
||||
color: Colors.grey.shade200,
|
||||
child: Text(
|
||||
'${r + 1}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Cell Values
|
||||
for (int c = 0; c < maxCols; c++)
|
||||
Container(
|
||||
width: cellWidth,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
c < rowCells.length ? rowCells[c] : '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom sheets TabBar (if multiple tabs exist)
|
||||
if (_sheetNames.isNotEmpty)
|
||||
Container(
|
||||
height: 44,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
),
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _sheetNames.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final name = _sheetNames[index];
|
||||
final isSelected = name == _activeSheetName;
|
||||
return Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() => _activeSheetName = name);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected
|
||||
? Colors.green.shade700
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected
|
||||
? Colors.green.shade700
|
||||
: Colors.grey.shade300,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
isSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color:
|
||||
isSelected
|
||||
? Colors.white
|
||||
: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
return Sheetifye.memory(
|
||||
_fileBytes!,
|
||||
readOnly: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -51,16 +51,15 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Sync initial search text if it was set externally (e.g. from Dashboard)
|
||||
final initialQuery = ref.read(studentQueryProvider).search;
|
||||
if (initialQuery.isNotEmpty) {
|
||||
_searchController.text = initialQuery;
|
||||
}
|
||||
|
||||
_searchController.addListener(_onSearchChanged);
|
||||
_searchFocusNode.addListener(_onSearchFocusChanged);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
// Sync initial search text if it was set externally (e.g. from Dashboard)
|
||||
final initialQuery = ref.read(studentQueryProvider).search;
|
||||
if (initialQuery.isNotEmpty) {
|
||||
_searchController.text = initialQuery;
|
||||
}
|
||||
if (ref.read(activeTabProvider) == 'Students') {
|
||||
_shortcutFocusNode.requestFocus();
|
||||
}
|
||||
|
||||
+24
-160
@@ -1,22 +1,6 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "99.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -129,14 +113,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.10"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_config
|
||||
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -185,14 +161,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -450,10 +418,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
sha256: "9255e1e3ad6e38906a1b4f8287678f95f378744c5b46b1985588543f3f19046e"
|
||||
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.2"
|
||||
version: "2.6.1"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -520,14 +488,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
frontend_server_client:
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
name: freezed_annotation
|
||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "2.4.4"
|
||||
get_it:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -536,14 +504,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.1"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -584,14 +544,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -680,14 +632,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.3"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -816,14 +760,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.17.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -992,14 +928,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
printing:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1052,10 +980,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod
|
||||
sha256: "17100416c51db7810c71a7bb2c34d1f881faa0074fd452afb0c4db6f8f126c76"
|
||||
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.2"
|
||||
version: "2.6.1"
|
||||
riverpod_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod_annotation
|
||||
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1152,35 +1088,19 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
sheetifye:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
name: sheetifye
|
||||
sha256: "096537cc78c896b51c1427ab31296ebe59539b50b5f0fcd2db2a65be1d9e00d9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_packages_handler:
|
||||
version: "1.1.0"
|
||||
shimmer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
name: shimmer
|
||||
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
@@ -1205,22 +1125,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.13"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1341,14 +1245,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.31.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1357,14 +1253,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.17"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1493,14 +1381,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1517,22 +1397,6 @@ packages:
|
||||
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"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -34,7 +34,7 @@ dependencies:
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_riverpod: ^3.3.1
|
||||
flutter_riverpod: ^2.6.1
|
||||
dio: ^5.9.2
|
||||
flutter_secure_storage: ^10.1.0
|
||||
google_fonts: ^8.1.0
|
||||
@@ -58,7 +58,8 @@ dependencies:
|
||||
wolt_modal_sheet: ^0.11.0
|
||||
printing: ^5.14.3
|
||||
pdf: ^3.12.0
|
||||
intl: ^0.20.3
|
||||
intl: ^0.19.0
|
||||
sheetifye: ^1.1.0
|
||||
flutter_local_notifications: ^17.2.2
|
||||
workmanager: ^0.9.0+3
|
||||
desktop_drop: ^0.7.1
|
||||
@@ -79,6 +80,10 @@ dev_dependencies:
|
||||
flutter_native_splash: ^2.4.4
|
||||
cider: ^0.2.10
|
||||
|
||||
dependency_overrides:
|
||||
google_fonts: ^8.1.0
|
||||
intl: ^0.20.3
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
|
||||
Reference in new issue
Block a user