Compare commits

...
3 Commits
27 changed files with 335 additions and 135 deletions

No files matched your search

+82 -5
View File
@@ -57,6 +57,20 @@ jobs:
& "$env:JAVA_HOME\bin\java.exe" -version
# ------------------------------------------------------------
# .NET
# ------------------------------------------------------------
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Verify .NET
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
dotnet --info
# ------------------------------------------------------------
# Flutter
# ------------------------------------------------------------
@@ -322,6 +336,67 @@ jobs:
throw "Flutter Windows build failed."
}
# ------------------------------------------------------------
# Bundle Microsoft Visual C++ CRT Runtime DLLs
# Fixes: vcruntime140.dll, vcruntime140_1.dll, msvcp140.dll missing errors
# ------------------------------------------------------------
Write-Host ""
Write-Host "Locating Microsoft Visual C++ CRT x64 redistributable libraries..."
$releaseDir = "build\windows\x64\runner\Release"
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$copied = $false
if (Test-Path $vswhere) {
$vsPath = & $vswhere -latest -products * -property installationPath
if ($vsPath -and (Test-Path "$vsPath\VC\Redist\MSVC")) {
$crtDirs = Get-ChildItem -Path "$vsPath\VC\Redist\MSVC" -Recurse -Filter "Microsoft.VC*.CRT" |
Where-Object { $_.FullName -like "*\x64\*" -and $_.FullName -notlike "*debug*" -and $_.FullName -notlike "*onecore*" }
$targetCrtDir = $crtDirs | Select-Object -First 1
if ($targetCrtDir) {
Write-Host "Found CRT DLL directory at: $($targetCrtDir.FullName)"
Copy-Item -Path "$($targetCrtDir.FullName)\*.dll" -Destination $releaseDir -Force
$copied = $true
}
}
}
if (-not $copied) {
Write-Warning "CRT directory not found via vswhere. Searching System32 for required VC runtime DLLs..."
$requiredDlls = @(
"vcruntime140.dll",
"vcruntime140_1.dll",
"msvcp140.dll",
"msvcp140_1.dll",
"msvcp140_2.dll",
"msvcp140_atomic_wait.dll",
"msvcp140_codecvt_ids.dll",
"vcruntime140_threads.dll",
"concrt140.dll",
"vccorlib140.dll"
)
foreach ($dll in $requiredDlls) {
$sysPath = "$env:SystemRoot\System32\$dll"
if (Test-Path $sysPath) {
Copy-Item -Path $sysPath -Destination $releaseDir -Force
Write-Host "Copied $dll from System32"
}
}
}
$criticalDlls = @("vcruntime140.dll", "vcruntime140_1.dll", "msvcp140.dll")
foreach ($dll in $criticalDlls) {
$targetPath = Join-Path $releaseDir $dll
if (-not (Test-Path $targetPath)) {
Write-Warning "Missing critical CRT DLL: $dll in $releaseDir"
} else {
Write-Host "Verified bundled: $dll"
}
}
Write-Host ""
Write-Host "All DLLs bundled into Windows release:"
Get-ChildItem -Path "$releaseDir\*.dll" | Select-Object Name
# ------------------------------------------------------------
# Windows Installer
# ------------------------------------------------------------
@@ -339,6 +414,7 @@ jobs:
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
"/DMyAppVersion=${{ steps.release_info.outputs.version_name }}" `
"/DOutputBaseFilename=TIS_RMS_Client_Setup_${{ steps.release_info.outputs.tag }}" `
TIS_RMS_Client.iss
if ($LASTEXITCODE -ne 0) {
@@ -374,7 +450,7 @@ jobs:
}
$apkPath = "build\app\outputs\flutter-apk\app-release.apk"
$outputPath = "..\installers\TIS_RMS_Android_Universal.apk"
$outputPath = "..\installers\TIS_RMS_Android_Universal_${{ steps.release_info.outputs.tag }}.apk"
if (-not (Test-Path $apkPath)) {
throw "Android build completed without producing $apkPath"
@@ -395,9 +471,10 @@ jobs:
run: |
$ErrorActionPreference = 'Stop'
$tag = "${{ steps.release_info.outputs.tag }}"
$files = @(
"installers\TIS_RMS_Client_Setup.exe",
"installers\TIS_RMS_Android_Universal.apk"
"installers\TIS_RMS_Client_Setup_$tag.exe",
"installers\TIS_RMS_Android_Universal_$tag.apk"
)
foreach ($file in $files) {
@@ -425,8 +502,8 @@ jobs:
target_commitish: ${{ github.sha }}
name: Release ${{ steps.release_info.outputs.tag }}
files: |
installers/TIS_RMS_Client_Setup.exe
installers/TIS_RMS_Android_Universal.apk
installers/TIS_RMS_Client_Setup_${{ steps.release_info.outputs.tag }}.exe
installers/TIS_RMS_Android_Universal_${{ steps.release_info.outputs.tag }}.apk
generate_release_notes: true
draft: false
prerelease: false
+9 -8
View File
@@ -12,7 +12,7 @@ The Android application is distributed as a single universal APK supporting both
| APK File | Architecture | Description |
| :--- | :--- | :--- |
| **`TIS_RMS_Android_Universal.apk`** | **Universal (32-bit & 64-bit)** | Single standalone installer compatible with all modern and legacy Android devices (Android 7.0+). |
| **`TIS_RMS_Android_Universal_v<version>.apk`** | **Universal (32-bit & 64-bit)** | Single standalone installer compatible with all modern and legacy Android devices (Android 7.0+). |
### Build Command (Flutter):
```bash
@@ -22,7 +22,7 @@ flutter build apk --release --obfuscate --split-debug-info=build/app/outputs/sym
*Output APK located in `frontend/build/app/outputs/flutter-apk/app-release.apk`.*
### Installation Steps:
1. Transfer `TIS_RMS_Android_Universal.apk` to your Android device (via USB, local network share, or download).
1. Transfer `TIS_RMS_Android_Universal_v<version>.apk` (e.g. `TIS_RMS_Android_Universal_v1.0.0.apk`) to your Android device (via USB, local network share, or download).
2. Tap the `.apk` file in your file manager.
3. If prompted, enable **"Install unknown apps"** or **"Allow from this source"** in Android Settings.
4. Tap **Install** and open the app.
@@ -35,8 +35,8 @@ flutter build apk --release --obfuscate --split-debug-info=build/app/outputs/sym
The Windows client runs natively on Windows 10 and Windows 11 (64-bit).
### Prerequisites:
- **Microsoft Visual C++ 2015–2022 Redistributable (x64)** (usually pre-installed on Windows).
- **Microsoft .NET Desktop Runtime 6.0/8.0+ (x64)** (automatically detected and installed by setup if missing).
- **Microsoft Visual C++ 2015–2022 Redistributable (x64)** (Core runtime libraries `vcruntime140_1.dll`, `msvcp140.dll`, etc. are bundled directly with the release binaries; the installer also automatically installs the full redistributable if missing).
- **Microsoft .NET Desktop Runtime 6.0/8.0+ (x64)** (automatically detected and installed silently by setup if missing).
---
@@ -59,11 +59,12 @@ flutter build windows --release --obfuscate --split-debug-info=build/windows/sym
### B. Compiling the Windows Inno Setup Installer (`TIS_RMS_Client.iss`)
The installer script [`frontend/TIS_RMS_Client.iss`](file:///f:/SumbrerongBato/tis_rms_server/frontend/TIS_RMS_Client.iss) packages the app into an ultra-compact standalone setup file (`TIS_RMS_Client_Setup.exe` ~**16.7 MB**).
The installer script [`frontend/TIS_RMS_Client.iss`](file:///d:/Capstone/TIS_RIS_Server_Manager/TIS_RMS/frontend/TIS_RMS_Client.iss) packages the app into an ultra-compact standalone setup file (`TIS_RMS_Client_Setup_v<version>.exe` ~**16.7 MB**).
#### Features:
- **Small File Size (16.7 MB)**: Uses `lzma2/ultra64` solid 64MB dictionary compression.
- **Automated .NET Runtime Setup**: Automatically checks for and installs Microsoft .NET Desktop Runtime silently if missing.
- **Small File Size (~17 MB)**: Uses `lzma2/ultra64` solid 64MB dictionary compression.
- **Bundled VC++ Runtime DLLs**: Packages MSVC CRT libraries (`vcruntime140.dll`, `vcruntime140_1.dll`, `msvcp140.dll`, etc.) directly into the application folder to prevent missing DLL crashes.
- **Automated VC++ & .NET Runtime Setup**: Checks system registry and automatically installs Microsoft Visual C++ 2015-2022 Redistributable and Microsoft .NET Desktop Runtime silently if missing.
- **Selectable Destination Path**: Lets the user choose where to install (defaults to `C:\Program Files\TIS RMS Client`).
- **Desktop Shortcut Checkbox**: Optional checkbox on the tasks page to create a desktop shortcut with the official school icon logo.
- **Start Menu & Uninstaller**: Registers a Start Menu program group and includes a clean uninstaller in Windows *Apps & Features* / *Settings*.
@@ -74,7 +75,7 @@ The installer script [`frontend/TIS_RMS_Client.iss`](file:///f:/SumbrerongBato/t
# Using Inno Setup Command Line Compiler (ISCC)
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" frontend\TIS_RMS_Client.iss
```
*The compiled installer will be saved to `installers/TIS_RMS_Client_Setup.exe`.*
*The compiled installer will be saved to `installers/TIS_RMS_Client_Setup_v<version>.exe` (e.g., `installers/TIS_RMS_Client_Setup_v1.0.0.exe`).*
---
+10 -1
View File
@@ -6,6 +6,9 @@
#ifndef MyAppVersion
#define MyAppVersion "1.0.0"
#endif
#ifndef OutputBaseFilename
#define OutputBaseFilename "TIS_RMS_Client_Setup_v" + MyAppVersion
#endif
#define MyAppPublisher "Talisay Integrated School"
#define MyAppURL "https://tis-rms.cc.cd"
#define MyAppExeName "frontend.exe"
@@ -30,7 +33,7 @@ DisableProgramGroupPage=no
; Output installer file configuration
OutputDir=..\installers
OutputBaseFilename=TIS_RMS_Client_Setup
OutputBaseFilename={#OutputBaseFilename}
SetupIconFile={#MyAppIcon}
UninstallDisplayIcon={app}\{#MyAppExeName}
UninstallDisplayName={#MyAppName}
@@ -98,6 +101,12 @@ var
begin
if CurStep = ssPostInstall then
begin
// If Microsoft Visual C++ 2015-2022 Redistributable (x64) is not installed, automatically install it silently
if not IsVCRedistInstalled then
begin
Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -Command "$ProgressPreference = ''SilentlyContinue''; try { if (Get-Command winget -ErrorAction SilentlyContinue) { winget install --id Microsoft.VCRedist.2015+.x64 --silent --accept-package-agreements --accept-source-agreements } else { $url = ''https://aka.ms/vs/17/release/vc_redist.x64.exe''; $out = ''$env:TEMP\vc_redist.x64.exe''; Invoke-WebRequest -Uri $url -OutFile $out; Start-Process -FilePath $out -ArgumentList ''/install /quiet /norestart'' -Wait } } catch {}"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
// If .NET Desktop Runtime is not installed, automatically install it silently
if not IsDotNetRuntimeInstalled then
begin
+14 -3
View File
@@ -1,3 +1,4 @@
import 'dart:io' show Platform;
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@@ -12,11 +13,14 @@ class SoundService {
static bool get isMuted => _muted;
static set isMuted(bool value) => _muted = value;
static bool get _isDesktop =>
!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS);
static void _ensureInitialized() {
if (_isDesktop) return;
if (_initialized && _player != null) return;
try {
_player = AudioPlayer();
_player!.setPlayerMode(PlayerMode.lowLatency);
_player!.setVolume(1.0);
_initialized = true;
} catch (e) {
@@ -27,13 +31,20 @@ class SoundService {
static Future<void> _playSound(String assetFileName, {bool isErrorOrAlert = false}) async {
if (_muted) return;
try {
if (_isDesktop) {
if (isErrorOrAlert) {
await SystemSound.play(SystemSoundType.alert);
} else {
await SystemSound.play(SystemSoundType.click);
}
return;
}
_ensureInitialized();
if (_player != null) {
// Stop current sound if playing, then play new sound
await _player!.stop();
await _player!.play(AssetSource('sounds/$assetFileName'));
} else {
// Fallback to system sound
if (isErrorOrAlert) {
await SystemSound.play(SystemSoundType.alert);
} else {
+26 -7
View File
@@ -17,6 +17,14 @@ class DownloadService {
final dir = await getDownloadsDirectory();
if (dir != null) {
path = '${dir.path}\\TIS_RMS';
} else {
final userProfile = Platform.environment['USERPROFILE'];
if (userProfile != null && userProfile.isNotEmpty) {
path = '$userProfile\\Downloads\\TIS_RMS';
} else {
final docsDir = await getApplicationDocumentsDirectory();
path = '${docsDir.path}\\TIS_RMS';
}
}
}
@@ -78,7 +86,16 @@ class DownloadService {
if (!await file.exists()) {
throw Exception('File does not exist at $filePath');
}
await OpenFilex.open(filePath);
try {
final res = await OpenFilex.open(filePath);
if (res.type != ResultType.done && Platform.isWindows) {
await Process.run('explorer.exe', [filePath]);
}
} catch (_) {
if (Platform.isWindows) {
await Process.run('explorer.exe', [filePath]);
}
}
}
/// Opens the storage directory (Download/TIS_RMS) or highlights the file in File Explorer.
@@ -105,12 +122,14 @@ class DownloadService {
await fallback.launch();
}
} else if (Platform.isWindows) {
if (filePath != null && await File(filePath).exists()) {
await Process.run('explorer.exe', ['/select,', filePath]);
} else {
final dirPath = await getDownloadDirectoryPath();
await Process.run('explorer.exe', [dirPath]);
}
try {
if (filePath != null && await File(filePath).exists()) {
await Process.run('explorer.exe', ['/select,$filePath']);
} else {
final dirPath = await getDownloadDirectoryPath();
await Process.run('explorer.exe', [dirPath]);
}
} catch (_) {}
}
}
}
@@ -61,16 +61,16 @@ class PrintQueueItem {
factory PrintQueueItem.fromJson(Map<String, dynamic> json) {
return PrintQueueItem(
queueId: json['queue_id'] as int,
documentId: json['document_id'] as int,
fileName: json['file_name'] as String? ?? '',
filePath: json['file_path'] as String? ?? '',
documentType: json['document_type'] as String?,
status: json['status'] as String? ?? 'Pending',
studentName: json['student_name'] as String?,
studentLrn: json['student_lrn'] as String?,
queueId: (json['queue_id'] as num?)?.toInt() ?? 0,
documentId: (json['document_id'] as num?)?.toInt() ?? 0,
fileName: json['file_name']?.toString() ?? '',
filePath: json['file_path']?.toString() ?? '',
documentType: json['document_type']?.toString(),
status: json['status']?.toString() ?? 'Pending',
studentName: json['student_name']?.toString(),
studentLrn: json['student_lrn']?.toString(),
addedAt: json['added_at'] != null
? DateTime.parse(json['added_at'] as String)
? (DateTime.tryParse(json['added_at'].toString()) ?? DateTime.now())
: DateTime.now(),
);
}
@@ -99,15 +99,15 @@ class PrintHistoryItem {
factory PrintHistoryItem.fromJson(Map<String, dynamic> json) {
return PrintHistoryItem(
id: json['id'] as int,
documentId: json['document_id'] as int,
documentName: json['document_name'] as String? ?? 'Document',
studentName: json['student_name'] as String? ?? 'Unknown',
studentLrn: json['student_lrn'] as String?,
fileName: json['file_name'] as String?,
documentType: json['document_type'] as String?,
id: (json['id'] as num?)?.toInt() ?? 0,
documentId: (json['document_id'] as num?)?.toInt() ?? 0,
documentName: json['document_name']?.toString() ?? 'Document',
studentName: json['student_name']?.toString() ?? 'Unknown',
studentLrn: json['student_lrn']?.toString(),
fileName: json['file_name']?.toString(),
documentType: json['document_type']?.toString(),
printedAt: json['printed_at'] != null
? DateTime.parse(json['printed_at'] as String)
? (DateTime.tryParse(json['printed_at'].toString()) ?? DateTime.now())
: DateTime.now(),
);
}
+20 -18
View File
@@ -48,25 +48,27 @@ void main() async {
);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
final view = WidgetsBinding.instance.platformDispatcher.views.first;
final physicalSize = view.physicalSize;
final devicePixelRatio = view.devicePixelRatio;
final width = physicalSize.width / devicePixelRatio;
final height = physicalSize.height / devicePixelRatio;
final shortestSide = width < height ? width : height;
final view = WidgetsBinding.instance.platformDispatcher.views.firstOrNull;
if (view != null) {
final physicalSize = view.physicalSize;
final devicePixelRatio = view.devicePixelRatio;
final width = physicalSize.width / devicePixelRatio;
final height = physicalSize.height / devicePixelRatio;
final shortestSide = width < height ? width : height;
if (shortestSide < 600) {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
} else {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
if (shortestSide < 600) {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
} else {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
}
await NotificationService().initialize();
@@ -560,7 +560,8 @@ class _ArchivesScreenState extends ConsumerState<ArchivesScreen>
final dirPath = await DownloadService.getDownloadDirectoryPath();
for (final docId in _selectedDocumentIds) {
final doc = docs.firstWhere((d) => d.id == docId);
final doc = docs.where((d) => d.id == docId).firstOrNull;
if (doc == null) continue;
final url =
'${ApiConstants.baseUrl}/documents/${doc.id}/view?token=$token&download=true';
lastSavedPath = await DownloadService.downloadFile(url: url, fileName: doc.fileName);
@@ -1867,8 +1868,9 @@ class _ArchivesScreenState extends ConsumerState<ArchivesScreen>
final studentName =
'${folder.studentLastName ?? ''}, ${folder.studentFirstName ?? ''}';
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RenderBox? overlay =
Overlay.maybeOf(context)?.context.findRenderObject() as RenderBox?;
if (overlay == null) return;
final value = await showMenu<String>(
context: context,
position: RelativeRect.fromRect(
@@ -173,9 +173,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final notificationsAsync = ref.read(notificationsProvider);
final list = notificationsAsync.value ?? [];
final RenderBox button = context.findRenderObject() as RenderBox;
final RenderBox overlay =
Navigator.of(context).overlay!.context.findRenderObject() as RenderBox;
final RenderBox? button = context.findRenderObject() as RenderBox?;
final RenderBox? overlay =
Navigator.of(context).overlay?.context.findRenderObject() as RenderBox?;
if (button == null || overlay == null) return;
final position = RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(Offset.zero, ancestor: overlay),
@@ -524,7 +524,8 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
int successCount = 0;
for (final docId in _selectedDocumentIds) {
final doc = docs.firstWhere((d) => d.id == docId);
final doc = docs.where((d) => d.id == docId).firstOrNull;
if (doc == null) continue;
final url =
'${ApiConstants.baseUrl}/documents/${doc.id}/view?token=$token&download=true';
await DownloadService.downloadFile(url: url, fileName: doc.fileName);
@@ -2428,8 +2429,9 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
) async {
if (folder.studentId == null) return;
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RenderBox? overlay =
Overlay.maybeOf(context)?.context.findRenderObject() as RenderBox?;
if (overlay == null) return;
final value = await showMenu<String>(
context: context,
position: RelativeRect.fromRect(
@@ -226,8 +226,9 @@ class _FileFolderCardState extends State<FileFolderCard> {
void _showContextMenu(BuildContext context, Offset position) {
if (widget.isMultiSelectMode) return;
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RenderBox? overlay =
Overlay.maybeOf(context)?.context.findRenderObject() as RenderBox?;
if (overlay == null) return;
showMenu<String>(
context: context,
position: RelativeRect.fromRect(
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../core/services/haptic_service.dart';
import '../../../../domain/repositories/document_repository.dart'
show MissingRequirements;
import '../../../../domain/entities/document_requirement_model.dart';
@@ -1182,7 +1183,7 @@ class _CopyableLrnButtonState extends State<_CopyableLrnButton> {
if (lrn.isEmpty) return;
Clipboard.setData(ClipboardData(text: lrn));
HapticFeedback.lightImpact();
HapticService.light();
setState(() {
_copied = true;
@@ -151,6 +151,15 @@ class _UploadOcrModalState extends ConsumerState<UploadOcrModal> {
// Requirements strip expand/collapse state
bool _requirementsExpanded = true;
int _safeFileSize(File file) {
try {
if (file.existsSync()) {
return file.lengthSync();
}
} catch (_) {}
return 0;
}
@override
void initState() {
super.initState();
@@ -171,7 +180,7 @@ class _UploadOcrModalState extends ConsumerState<UploadOcrModal> {
// Pre-populate entries from drag-and-drop files
if (widget.preloadedFiles != null && widget.preloadedFiles!.isNotEmpty) {
_entries = widget.preloadedFiles!.map((f) {
final bytes = f.lengthSync();
final bytes = _safeFileSize(f);
final kb = bytes / 1024;
final size = kb >= 1024
? '${(kb / 1024).toStringAsFixed(1)} MB'
@@ -326,7 +335,7 @@ class _UploadOcrModalState extends ConsumerState<UploadOcrModal> {
final ext = path.split('.').last.toLowerCase();
if (!allowed.contains(ext)) continue;
final file = File(path);
final len = file.lengthSync();
final len = _safeFileSize(file);
final sizeKb = (len / 1024).toStringAsFixed(1);
final sizeLabel = len > 1048576
? '${(len / 1048576).toStringAsFixed(1)} MB'
@@ -569,6 +578,7 @@ class _UploadOcrModalState extends ConsumerState<UploadOcrModal> {
}
Future<void> _scanDocument() async {
if (!Platform.isAndroid && !Platform.isIOS) return;
try {
final scanner = DocumentScanner(
options: DocumentScannerOptions(
@@ -604,9 +614,10 @@ class _UploadOcrModalState extends ConsumerState<UploadOcrModal> {
if (format == ScannedSaveFormat.pdf && hasPdf) {
final file = File(result.pdf!.uri);
final sizeKb = (file.lengthSync() / 1024).toStringAsFixed(1);
final sizeLabel = file.lengthSync() > 1048576
? '${(file.lengthSync() / 1048576).toStringAsFixed(1)} MB'
final len = _safeFileSize(file);
final sizeKb = (len / 1024).toStringAsFixed(1);
final sizeLabel = len > 1048576
? '${(len / 1048576).toStringAsFixed(1)} MB'
: '$sizeKb KB';
final name = 'Scanned_Doc_$timestamp.pdf';
@@ -630,9 +641,10 @@ class _UploadOcrModalState extends ConsumerState<UploadOcrModal> {
final imagePath = result.images![i];
final file = File(imagePath);
if (!file.existsSync()) continue;
final sizeKb = (file.lengthSync() / 1024).toStringAsFixed(1);
final sizeLabel = file.lengthSync() > 1048576
? '${(file.lengthSync() / 1048576).toStringAsFixed(1)} MB'
final len = _safeFileSize(file);
final sizeKb = (len / 1024).toStringAsFixed(1);
final sizeLabel = len > 1048576
? '${(len / 1048576).toStringAsFixed(1)} MB'
: '$sizeKb KB';
final pageSuffix =
result.images!.length > 1 ? '_page${i + 1}' : '';
@@ -3568,9 +3568,11 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
List<String> allYears,
Set<String> selected,
) {
final RenderBox button = context.findRenderObject() as RenderBox;
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RenderBox? button = context.findRenderObject() as RenderBox?;
final RenderBox? overlay =
Overlay.maybeOf(context)?.context.findRenderObject() as RenderBox?;
if (button == null || overlay == null) return;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(Offset.zero, ancestor: overlay),
@@ -3742,9 +3744,11 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
List<_StatusOption> options,
Set<String> selected,
) {
final RenderBox button = context.findRenderObject() as RenderBox;
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RenderBox? button = context.findRenderObject() as RenderBox?;
final RenderBox? overlay =
Overlay.maybeOf(context)?.context.findRenderObject() as RenderBox?;
if (button == null || overlay == null) return;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(Offset.zero, ancestor: overlay),
@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/constants/app_colors.dart';
import '../../../core/constants/app_sizes.dart';
import '../../../core/services/haptic_service.dart';
import '../../../domain/entities/student_model.dart';
import '../../../domain/entities/setup_models.dart';
import '../../shared/buttons/primary_button.dart';
@@ -353,8 +354,9 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
Offset globalPosition,
StudentModel student,
) async {
final RenderBox overlay =
Overlay.of(ctx).context.findRenderObject()! as RenderBox;
final RenderBox? overlay =
Overlay.maybeOf(ctx)?.context.findRenderObject() as RenderBox?;
if (overlay == null) return;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromLTWH(globalPosition.dx, globalPosition.dy, 0, 0),
Offset.zero & overlay.size,
@@ -1403,7 +1405,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
behavior: HitTestBehavior.opaque,
onTap: widget.userRole != 'teacher'
? () {
HapticFeedback.selectionClick();
HapticService.selection();
_updateSelection(() {
if (!_showMultiSelect) {
_showMultiSelect = true;
@@ -1668,7 +1670,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
behavior: HitTestBehavior.opaque,
onTap: widget.userRole != 'teacher'
? () {
HapticFeedback.selectionClick();
HapticService.selection();
_updateSelection(() {
if (!_showMultiSelect) {
_showMultiSelect = true;
@@ -34,13 +34,17 @@ Future<bool?> showConfirmDialog(
final effectiveConfirmColor = confirmColor ?? (isDanger ? AppColors.error : AppColors.primaryGreen);
final effectiveIcon = icon ?? (isDanger ? Icons.warning_amber_rounded : Icons.help_outline_rounded);
if (isDanger) {
SoundService.playWarning();
HapticService.warning();
} else {
SoundService.playConfirm();
HapticService.medium();
}
if (!context.mounted) return Future.value(false);
try {
if (isDanger) {
SoundService.playWarning();
HapticService.warning();
} else {
SoundService.playConfirm();
HapticService.medium();
}
} catch (_) {}
return showDialog<bool>(
context: context,
@@ -22,10 +22,13 @@ class DisconnectedDialog extends StatefulWidget {
BuildContext context, {
VoidCallback? onReconnected,
}) async {
if (!context.mounted) return;
if (_isShowing) return;
_isShowing = true;
SoundService.playError();
HapticService.error();
try {
SoundService.playError();
HapticService.error();
} catch (_) {}
await showDialog<void>(
context: context,
@@ -18,6 +18,7 @@ class DocumentPropertiesDialog extends StatelessWidget {
BuildContext context, {
required DocumentModel document,
}) {
if (!context.mounted) return Future.value();
return showDialog(
context: context,
builder: (ctx) => DocumentPropertiesDialog(document: document),
@@ -19,8 +19,15 @@ void showErrorDialog(
String buttonLabel = 'OK',
VoidCallback? onDismissed,
}) {
SoundService.playError();
HapticService.error();
try {
SoundService.playError();
} catch (_) {}
try {
HapticService.error();
} catch (_) {}
if (!context.mounted) return;
showDialog(
context: context,
barrierDismissible: false,
@@ -111,6 +111,7 @@ Future<bool?> showFileSavePreviewDialog(
File? imageFile,
required Future<void> Function(String resolvedFileName) onSave,
}) {
if (!context.mounted) return Future.value(null);
return showDialog<bool>(
context: context,
barrierDismissible: false,
+15 -11
View File
@@ -27,17 +27,21 @@ void showInfoDialog(
}) {
final btnColor = buttonColor ?? iconColor;
if (iconColor == AppColors.error ||
iconColor == Colors.red ||
iconColor == Colors.orange ||
title.toLowerCase().contains('warning') ||
title.toLowerCase().contains('alert')) {
SoundService.playWarning();
HapticService.warning();
} else {
SoundService.playInfo();
HapticService.info();
}
if (!context.mounted) return;
try {
if (iconColor == AppColors.error ||
iconColor == Colors.red ||
iconColor == Colors.orange ||
title.toLowerCase().contains('warning') ||
title.toLowerCase().contains('alert')) {
SoundService.playWarning();
HapticService.warning();
} else {
SoundService.playInfo();
HapticService.info();
}
} catch (_) {}
showDialog(
context: context,
@@ -8,8 +8,11 @@ import '../../screens/login/login_screen.dart';
/// A reusable function to show the logout confirmation and handle the logout process.
Future<void> showLogoutConfirmationDialog(BuildContext context) async {
SoundService.playWarning();
HapticService.warning();
if (!context.mounted) return;
try {
SoundService.playWarning();
HapticService.warning();
} catch (_) {}
return showDialog(
context: context,
builder: (ctx) => Consumer(
@@ -24,8 +24,15 @@ Future<void> showSuccessDialog(
String? filePath,
String? notes,
}) {
SoundService.playSuccess();
HapticService.success();
try {
SoundService.playSuccess();
} catch (_) {}
try {
HapticService.success();
} catch (_) {}
if (!context.mounted) return Future.value();
return showDialog<void>(
context: context,
barrierDismissible: false,
@@ -19,8 +19,14 @@ void showWarningDialog(
String buttonLabel = 'Got It',
VoidCallback? onDismissed,
}) {
SoundService.playWarning();
HapticService.warning();
try {
SoundService.playWarning();
} catch (_) {}
try {
HapticService.warning();
} catch (_) {}
if (!context.mounted) return;
final isDark = Theme.of(context).brightness == Brightness.dark;
final textPrimary = isDark ? AppColors.darkTextPrimary : AppColors.textPrimary;
@@ -36,6 +36,15 @@ class _DocumentSourcePickerState extends State<DocumentSourcePicker> {
super.dispose();
}
int _safeFileSize(File file) {
try {
if (file.existsSync()) {
return file.lengthSync();
}
} catch (_) {}
return 0;
}
Future<void> _pickFile() async {
try {
final allowed =
@@ -46,9 +55,10 @@ class _DocumentSourcePickerState extends State<DocumentSourcePicker> {
allowedExtensions: allowed,
);
if (result != null) {
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
final size = (file.lengthSync() / (1024 * 1024)).toStringAsFixed(2);
final bytes = _safeFileSize(file);
final size = (bytes / (1024 * 1024)).toStringAsFixed(2);
// Send the data back to the parent screen
widget.onFileSelected(file, result.files.single.name, '$size MB');
}
@@ -58,6 +68,7 @@ class _DocumentSourcePickerState extends State<DocumentSourcePicker> {
}
Future<void> _takePhoto() async {
if (!Platform.isAndroid && !Platform.isIOS) return;
try {
final scanner = DocumentScanner(
options: DocumentScannerOptions(
@@ -75,7 +86,8 @@ class _DocumentSourcePickerState extends State<DocumentSourcePicker> {
final images = result.images;
if (images != null && images.isNotEmpty) {
final file = File(images.first);
final size = (file.lengthSync() / (1024 * 1024)).toStringAsFixed(2);
final bytes = _safeFileSize(file);
final size = (bytes / (1024 * 1024)).toStringAsFixed(2);
final name =
'Scanned_Doc_${DateTime.now().millisecondsSinceEpoch}.jpg';
@@ -255,18 +267,23 @@ class _DocumentSourcePickerState extends State<DocumentSourcePicker> {
onDragDone: (details) {
_dragResetTimer?.cancel();
if (mounted) setState(() => _isDragOver = false);
if (details.files.isNotEmpty) {
final xfile = details.files.first;
final ext = xfile.path.split('.').last.toLowerCase();
if (allowed.contains(ext)) {
final file = File(xfile.path);
final size =
(file.lengthSync() / (1024 * 1024)).toStringAsFixed(2);
widget.onFileSelected(file, xfile.name, '$size MB');
} else {
widget.onError?.call(
'Unsupported file format: .$ext. Allowed: ${allowed.join(', ')}');
try {
if (details.files.isNotEmpty) {
final xfile = details.files.first;
final ext = xfile.path.split('.').last.toLowerCase();
if (allowed.contains(ext)) {
final file = File(xfile.path);
final bytes = _safeFileSize(file);
final size =
(bytes / (1024 * 1024)).toStringAsFixed(2);
widget.onFileSelected(file, xfile.name, '$size MB');
} else {
widget.onError?.call(
'Unsupported file format: .$ext. Allowed: ${allowed.join(', ')}');
}
}
} catch (e) {
widget.onError?.call('Failed to read dropped file: $e');
}
},
child: content,
@@ -24,9 +24,11 @@ void showNotificationDropdownMenu(BuildContext context, WidgetRef ref) {
final notificationsAsync = ref.read(notificationsProvider);
final list = notificationsAsync.value ?? [];
final RenderBox button = context.findRenderObject() as RenderBox;
final RenderBox overlay =
Navigator.of(context).overlay!.context.findRenderObject() as RenderBox;
final RenderBox? button = context.findRenderObject() as RenderBox?;
final RenderBox? overlay =
Navigator.of(context).overlay?.context.findRenderObject() as RenderBox?;
if (button == null || overlay == null) return;
final position = RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.13+13
version: 1.0.22+22
environment:
sdk: ^3.11.5