Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a47a61c55 | ||
|
|
78035685a3 | ||
|
|
7db2ad88b3 |
No files matched your search
@@ -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
@@ -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`).*
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user