web configuration in progress

master_web
FaizHashmi 2 months ago
parent c0cd969558
commit 3c3c5e9423

@ -0,0 +1,414 @@
# Android vs Web Platform Comparison
> Quick reference for understanding differences between Android and Web versions
> Last Updated: July 30, 2026
---
## 🔍 Platform Feature Comparison
| Feature | Android (Current) | Web (After Migration) | Implementation Effort |
|---------|-------------------|----------------------|----------------------|
| **Kiosk Mode** | ✅ Native Android kiosk mode | ⚠️ Browser fullscreen + kiosk flags | Low |
| **Auto-Start on Boot** | ✅ BootBroadcastReceiver | ⚠️ Browser autostart config | Low |
| **Wake Lock** | ✅ WakelockPlus plugin | ✅ Browser Wake Lock API | Low |
| **Screen Always On** | ✅ WindowManager flags | ✅ Wake Lock API | Low |
| **Background Service** | ✅ Foreground service | ❌ Not possible (tab must stay open) | N/A |
| **File Logging** | ✅ External storage | ✅ IndexedDB + download | Medium |
| **SignalR Real-time** | ✅ signalr_core | ✅ Same (WebSocket) | None |
| **Audio Playback** | ✅ just_audio | ⚠️ just_audio web or HTML5 Audio | Low |
| **Text-to-Speech** | ✅ flutter_tts | ⚠️ Web Speech API | Medium |
| **QR Scanner** | ✅ qr_code_scanner_plus | ⚠️ mobile_scanner (requires HTTPS) | Low |
| **Scheduled Tasks** | ✅ AlarmManager | ⚠️ JavaScript timers | Medium |
| **Offline Support** | ✅ Native app | ⚠️ PWA with Service Worker | Medium |
| **Push Notifications** | ✅ Firebase Cloud Messaging | ⚠️ Web Push API | Medium |
| **Network Detection** | ✅ connectivity_plus | ✅ Same (web support) | None |
| **Local Storage** | ✅ SharedPreferences | ✅ localStorage (same plugin) | None |
| **Multi-language** | ✅ Built-in | ✅ Built-in | None |
| **Updates** | ⚠️ Manual APK deployment | ✅ Instant (refresh page) | N/A |
| **Installation** | ⚠️ APK + ADB | ✅ Just open URL | N/A |
| **Security** | ✅ Full device control | ⚠️ Browser sandbox | N/A |
| **Crash Recovery** | ✅ Can restart automatically | ⚠️ Browser dependent | Low |
**Legend:**
✅ Fully Supported | ⚠️ Supported with Limitations | ❌ Not Supported
---
## 📦 Dependencies Comparison
### Android-Only (Remove for Web)
```yaml
# These will be removed
wakelock_plus: ^1.2.10 # Use Browser Wake Lock API instead
path_provider: ^2.1.5 # Use IndexedDB/localStorage instead
qr_code_scanner_plus: ^2.0.10+1 # Use mobile_scanner instead
```
### Web-Compatible (Keep)
```yaml
# These work on both platforms
provider: ^6.1.2 # ✅ State management
get_it: ^8.0.2 # ✅ Dependency injection
signalr_core: ^1.1.1 # ✅ Real-time communication
connectivity_plus: ^6.1.0 # ✅ Network monitoring
shared_preferences: ^2.3.5 # ✅ Local storage
flutter_svg: ^2.0.14 # ✅ SVG rendering
intl: ^0.19.0 # ✅ Internationalization
logger: ^2.4.0 # ✅ Logging framework
http: ^1.2.2 # ✅ HTTP requests
```
### Web-Specific (Add)
```yaml
# Add these for web support
mobile_scanner: ^3.5.2 # QR scanner with web support
indexed_db: ^0.3.0 # Structured data storage
universal_html: ^2.2.4 # Access to web APIs
js: ^0.6.7 # JavaScript interop
```
### Requires Testing/Replacement
```yaml
just_audio: ^0.9.42 # ⚠️ Has web support, test needed
flutter_tts: ^4.2.0 # ⚠️ Limited web support, use Web Speech API
```
---
## 🔧 Code Changes Summary
### Files That Need Changes
| File | Type of Change | Complexity |
|------|---------------|------------|
| `lib/main.dart` | Modify | Low - Add platform detection |
| `lib/services/logger_service.dart` | New File | Medium - Create web version |
| `lib/services/wakelock_service.dart` | New File | Low - Wrapper for web/mobile |
| `lib/services/text_to_speech_service.dart` | Modify | Medium - Add web implementation |
| `lib/services/audio_service.dart` | Modify | Low - Test or add web fallback |
| `lib/utilities/native_method_handler.dart` | Modify | Low - Add platform checks |
| `lib/view_models/screen_config_view_model.dart` | Modify | Medium - Add web lifecycle |
| `lib/services/connectivity_service.dart` | Modify | Low - Add IP detection for web |
| `lib/config/dependency_injection.dart` | Modify | Medium - Platform-aware DI |
| `web/index.html` | Create | Low - Standard web config |
| `web/manifest.json` | Create | Low - PWA manifest |
### Files That Don't Need Changes
| File Category | Files | Reason |
|---------------|-------|--------|
| **Models** | All `lib/models/*.dart` | Platform-agnostic Dart code |
| **Repositories** | `lib/repositories/*.dart` | Use standard HTTP/SignalR |
| **Constants** | `lib/constants/*.dart` | Pure Dart constants |
| **UI Widgets** | Most of `lib/views/*.dart` | Flutter renders same |
| **View Models** | Business logic in ViewModels | Dart code, no platform API |
### Files to Delete (Not Needed for Web)
| File | Reason |
|------|--------|
| `android/` folder | Android-specific native code |
| `ios/` folder | iOS-specific native code |
| All `.kt` files | Kotlin is Android-only |
| `qline_scripts/*.sh` | Deployment scripts for Android APK |
---
## 🚀 Deployment Comparison
### Android Deployment (Current)
```bash
# Build
flutter build apk --release
# Deploy to devices
cd qline_scripts
sh deploy_qline.sh -ipfile device_ips_all.txt -version 9.4
# Update (requires ADB connection)
adb install -r app-release.apk
```
**Pros:**
- Full control over device
- Works offline always
- No browser dependencies
**Cons:**
- Manual deployment process
- Requires physical access or ADB
- Slower update rollout
- Version management complex
### Web Deployment (After Migration)
```bash
# Build
flutter build web --release
# Deploy to Firebase (example)
firebase deploy
# Or deploy to own server
rsync -avz build/web/ user@server:/var/www/qline/
```
**Pros:**
- ✅ Instant updates (just refresh)
- ✅ Deploy once, available everywhere
- ✅ No APK management
- ✅ Central control
- ✅ Easy rollback
**Cons:**
- ⚠️ Requires internet for updates
- ⚠️ Browser must be configured
- ⚠️ HTTPS required for camera
---
## 🎯 Feature Parity Matrix
### Core Features
| Feature | Android | Web | Notes |
|---------|---------|-----|-------|
| Patient Queue Display | ✅ | ✅ | Identical |
| Real-time SignalR Updates | ✅ | ✅ | Identical |
| Audio Call Tones | ✅ | ⚠️ | Web: autoplay restrictions |
| TTS Announcements | ✅ | ⚠️ | Web: Web Speech API |
| Multi-language (EN/AR) | ✅ | ✅ | Identical |
| RTL Support | ✅ | ✅ | Identical |
| Weather Widget | ✅ | ✅ | Identical |
| Prayer Times Widget | ✅ | ✅ | Identical |
| RSS Feed Widget | ✅ | ✅ | Identical |
| Configuration via IP | ✅ | ⚠️ | Web: URL param or manual |
| Health Check Logging | ✅ | ⚠️ | Web: IndexedDB + download |
| Crash Logging | ✅ | ⚠️ | Web: Console + IndexedDB |
### Kiosk Features
| Feature | Android | Web | Notes |
|---------|---------|-----|-------|
| QR Code Scanner | ✅ | ⚠️ | Web: Requires HTTPS |
| Manual ID Entry | ✅ | ✅ | Identical |
| Ticket Generation | ✅ | ✅ | Identical |
| Language Selection | ✅ | ✅ | Identical |
| Queue Selection | ✅ | ✅ | Identical |
| Thank You Screen | ✅ | ✅ | Identical |
### System Features
| Feature | Android | Web | Notes |
|---------|---------|-----|-------|
| 24/7 Operation | ✅ | ⚠️ | Web: Browser must stay open |
| Auto-restart | ✅ | ⚠️ | Web: Can reload page |
| Boot on Startup | ✅ | ⚠️ | Web: Configure browser autostart |
| Crash Recovery | ✅ | ⚠️ | Web: Browser dependent |
| Memory Management | ✅ | ⚠️ | Web: Browser handles |
| Log Rotation | ✅ | ⚠️ | Web: Manual download needed |
---
## 💰 Cost Comparison
### Android Deployment
**Initial Costs:**
- Android TV boxes/displays: $100-300 per device
- One-time setup per device
- ADB deployment setup
**Ongoing Costs:**
- Manual deployment time
- Physical access for updates
- Maintenance visits
**Advantages:**
- One-time cost
- No hosting fees
- Full offline operation
### Web Deployment
**Initial Costs:**
- Thin clients/mini PCs: $150-400 per device (optional - can reuse existing)
- SSL certificate: Free (Let's Encrypt)
- Domain name: $10-15/year
**Hosting Options:**
- Firebase Hosting: Free (up to 10GB)
- Self-hosted: Server costs
- Cloud (AWS/Azure): $20-100/month
**Ongoing Costs:**
- Hosting: $0-100/month
- Minimal deployment time
- Remote updates (no visits)
**Advantages:**
- Instant updates
- Remote management
- Central monitoring
---
## ⚡ Performance Comparison
### Android
| Metric | Performance |
|--------|-------------|
| App Startup | Fast (2-3 seconds) |
| Memory Usage | 80-150 MB |
| CPU Usage | Low (5-10%) |
| Network | SignalR WebSocket |
| Rendering | Native Android |
| Wake Lock | Native, reliable |
| Audio Latency | Low |
| Camera Access | Direct, fast |
### Web (Expected)
| Metric | Performance |
|--------|-------------|
| Page Load | Fast (2-4 seconds first load) |
| Memory Usage | 100-200 MB (browser + app) |
| CPU Usage | Low-Medium (10-20% with browser) |
| Network | SignalR WebSocket (same) |
| Rendering | Chromium rendering engine |
| Wake Lock | Browser API, reliable on Chrome |
| Audio Latency | Low-Medium (browser dependent) |
| Camera Access | WebRTC, slight delay |
**Overall:** Web performance is comparable, with slightly higher memory usage due to browser overhead.
---
## 🔒 Security Comparison
### Android
**Pros:**
- Full control over device
- Can lock down Android completely
- No browser vulnerabilities
- Native app sandbox
**Cons:**
- Physical device access required for setup
- APK can be extracted/reverse engineered
- Must manage device security
### Web
**Pros:**
- HTTPS encryption
- Browser security features
- Easy SSL certificate management
- Remote security updates
**Cons:**
- Browser can be accessed
- JavaScript can be viewed
- Depends on browser security
- Users can close browser (without kiosk mode)
**Recommendation:**
- Both can be secured adequately
- Web requires HTTPS + browser kiosk mode
- Android requires device lockdown
---
## 📈 Migration Timeline
| Phase | Android (Maintain) | Web (Build) | Total Time |
|-------|-------------------|-------------|------------|
| **Phase 1: Setup** | 0 days | 1-2 days | 1-2 days |
| **Phase 2: Services** | 0 days | 3-5 days | 3-5 days |
| **Phase 3: Core Updates** | 0 days | 2-3 days | 2-3 days |
| **Phase 4: UI Updates** | 0 days | 2-3 days | 2-3 days |
| **Phase 5: Testing** | 1 day | 4-6 days | 5-7 days |
| **Phase 6: Deployment** | 0 days | 3-5 days | 3-5 days |
| **Phase 7: Kiosk Setup** | 0 days | 2-3 days | 2-3 days |
| **Phase 8: Documentation** | 0 days | 1-2 days | 1-2 days |
| **TOTAL** | 1 day | 22-31 days | **23-32 days** |
---
## 🎯 Decision Matrix
### Choose Android If:
- ✅ You need complete device control
- ✅ You have IT staff for physical deployment
- ✅ You want true offline operation
- ✅ You need guaranteed 24/7 uptime
- ✅ You don't need frequent updates
### Choose Web If:
- ✅ You want instant updates across all locations
- ✅ You want centralized management
- ✅ You have stable HTTPS hosting
- ✅ You want easier deployment
- ✅ You can use browser kiosk mode
- ✅ You want cross-platform support (future: tablets, desktops)
### Choose Both If:
- ✅ You want maximum flexibility
- ✅ You can maintain both codebases (with shared business logic)
- ✅ You want to test web in pilot locations first
- ✅ You need backup deployment option
---
## 📊 Recommendation
### For HMG_QLine Project:
**Recommended Approach:** **Gradual Migration**
1. **Phase 1 (Now):** Keep Android as primary
2. **Phase 2 (2-4 weeks):** Build web version
3. **Phase 3 (1 week):** Pilot test web in 2-3 locations
4. **Phase 4 (2 weeks):** Evaluate feedback
5. **Phase 5 (Ongoing):** Migrate locations gradually
**Reasoning:**
- ✅ No disruption to current operations
- ✅ Test web thoroughly before full rollout
- ✅ Keep Android as fallback
- ✅ Evaluate real-world performance
- ✅ Easy to roll back if issues arise
---
## 📞 Quick Reference
**Need detailed implementation?**
→ See [WEB_MIGRATION_GUIDE.md](WEB_MIGRATION_GUIDE.md)
**Need step-by-step checklist?**
→ See [WEB_MIGRATION_CHECKLIST.md](WEB_MIGRATION_CHECKLIST.md)
**Need project architecture?**
→ See [PROJECT_CONTEXT.md](PROJECT_CONTEXT.md)
**Need recent changes info?**
→ See [QUICK_SUMMARY.md](QUICK_SUMMARY.md)
---
**Last Updated:** July 30, 2026
**Status:** Migration not started (planning phase)

@ -1,4 +1,4 @@
# HMG_QLine - Project Context Documentation
O# HMG_QLine - Project Context Documentation
> **Last Updated:** July 30, 2026
> **Project Type:** Flutter Healthcare Queue Management System
@ -1739,6 +1739,60 @@ adb shell cmd appops set com.example.hmg_qline.hmg_qline RUN_IN_BACKGROUND allow
---
nl## 🌐 Web Platform Compatibility
### Current Status
**Platform:** Android Only (AC-Powered Kiosk Displays)
**Web Support:** Not Implemented (Requires Migration)
### Migration Feasibility
✅ **VIABLE** - Core functionality can be migrated to web with platform abstraction
**For complete web migration details, see:** [WEB_MIGRATION_GUIDE.md](WEB_MIGRATION_GUIDE.md)
### Quick Migration Overview
#### What Works As-Is on Web
- ✅ Core business logic (MVVM, ViewModels)
- ✅ SignalR real-time communication
- ✅ Provider state management
- ✅ API calls and HTTP requests
- ✅ UI components and widgets
- ✅ Multi-language support (English/Arabic)
#### What Needs Replacement
- ❌ All Kotlin native code (MainActivity, AlarmScheduler, etc.)
- ❌ Platform channels and native methods
- ❌ File-based logging → Replace with IndexedDB
- ❌ Wakelock Plus → Replace with Browser Wake Lock API
- ❌ Flutter TTS → Replace with Web Speech API
- ❌ QR Scanner → Replace with mobile_scanner (web support)
- ❌ Android lifecycle → Replace with Page Visibility API
#### Key Limitations on Web
- No true kiosk mode (browser can be closed)
- No auto-start on boot (browser must be configured)
- No background service (tab must stay open)
- Camera requires HTTPS
- Audio autoplay restrictions
- Limited file system access
#### Estimated Migration Effort
- **Development:** 15-20 days
- **Testing:** 5-7 days
- **Deployment:** 3-5 days
- **Total:** 23-32 days
#### Recommended Deployment
- Chrome/Chromium in kiosk mode
- HTTPS with SSL certificate
- Progressive Web App (PWA) with Service Worker
- Dedicated mini PC or thin client hardware
**For step-by-step migration instructions, code examples, and deployment guides, see:** [WEB_MIGRATION_GUIDE.md](WEB_MIGRATION_GUIDE.md)
---
## 📖 Glossary
- **AC-Powered Kiosk**: Android device permanently plugged into power, not battery-powered
@ -1746,12 +1800,16 @@ adb shell cmd appops set com.example.hmg_qline.hmg_qline RUN_IN_BACKGROUND allow
- **Doze Mode**: Android 6+ battery optimization that restricts background processing
- **Health Check**: Periodic verification that app is functioning correctly
- **Hub**: SignalR WebSocket server for real-time communication
- **IndexedDB**: Browser database for storing structured data (web alternative to file storage)
- **Kiosk Mode**: Display mode where app runs continuously without user intervention
- **PWA**: Progressive Web App - web app that can work offline and be installed
- **QType**: Queue type (appointment, lab, rad, general)
- **SignalR**: Real-time communication library using WebSockets
- **TTS**: Text-to-Speech for voice announcements
- **Uptime**: Duration the app has been running since last start
- **Wake Lock API**: Browser API to prevent device from sleeping (web equivalent of wakelock)
- **Wakelock**: Android API to prevent device from sleeping
- **Web Speech API**: Browser API for speech synthesis (web equivalent of TTS)
---

@ -7,10 +7,22 @@ background service integration to ensure reliability and uptime.
**For complete project context, architecture, and implementation details, see:**
- **[PROJECT_CONTEXT.md](PROJECT_CONTEXT.md)** - Comprehensive documentation for developers and AI models
- **[WEB_MIGRATION_GUIDE.md](WEB_MIGRATION_GUIDE.md)** - Complete guide for migrating to web platform
- **[WEB_MIGRATION_CHECKLIST.md](WEB_MIGRATION_CHECKLIST.md)** - Step-by-step migration checklist
- **[ANDROID_VS_WEB_COMPARISON.md](ANDROID_VS_WEB_COMPARISON.md)** - Platform comparison and decision guide
- **[QUICK_SUMMARY.md](QUICK_SUMMARY.md)** - Quick reference for recent changes
- **[CHANGES_EXPLANATION.md](CHANGES_EXPLANATION.md)** - Detailed changelog
- **[AC_POWERED_KIOSK_SUMMARY.md](AC_POWERED_KIOSK_SUMMARY.md)** - Kiosk-specific documentation
### 🌐 Web Deployment
- **[WEB_DEPLOYMENT_NOTES.md](WEB_DEPLOYMENT_NOTES.md)** - ⚠️ **START HERE for web deployment**
- **[CORS_ISSUE_SOLUTION.md](CORS_ISSUE_SOLUTION.md)** - Complete CORS configuration guide (server-side)
- **[CONFIGURATION_API_STATUS.md](CONFIGURATION_API_STATUS.md)** - Configuration API details and status
- **[HUB_STATUS_BUG_FIX.md](HUB_STATUS_BUG_FIX.md)** - Fix for incorrect hub status display
- **[IP_PERSISTENCE_FIX.md](IP_PERSISTENCE_FIX.md)** - IP persistence and localStorage handling
- **[FINAL_STATUS_SUMMARY.md](FINAL_STATUS_SUMMARY.md)** - Complete status of all web issues
## Features
- Patient queue management across all screens

@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:hmg_qline/config/dependency_injection.dart';
import 'package:hmg_qline/config/routes.dart';
import 'package:hmg_qline/constants/app_constants.dart';
@ -12,6 +13,7 @@ import 'package:hmg_qline/view_models/screen_config_view_model.dart';
import 'package:hmg_qline/views/view_helpers/size_config.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:universal_html/html.dart' as html;
void main() {
runZonedGuarded(() async {
@ -20,7 +22,13 @@ void main() {
await AppDependencies.addDependencies();
// initialize crash handler after DI setup
getIt.get<CrashHandlerService>().setupGlobalErrorHandlers();
await WakelockPlus.enable();
// Platform-specific wake lock
if (kIsWeb) {
await _initializeWeb();
} else {
await WakelockPlus.enable();
}
} catch (e) {
log('Initialization error: $e');
}
@ -35,6 +43,21 @@ void main() {
});
}
Future<void> _initializeWeb() async {
try {
// Use wakelock_plus for web (it handles the Web Wake Lock API internally)
await WakelockPlus.enable();
log('Web wake lock activated');
} catch (e) {
log('Wake lock not supported: $e');
}
// Prevent browser context menu
html.document.onContextMenu.listen((event) => event.preventDefault());
log('Web platform initialized - Press F11 for fullscreen mode');
}
class MyApp extends StatelessWidget {
const MyApp({super.key});

@ -39,7 +39,7 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
@override
Future<GlobalConfigurationsModel?> getGlobalScreenConfigurations({required String ipAddress}) async {
// try {
try {
var params = {
"ipAddress": ipAddress.toString(),
"apiKey": AppConstants.apiKey.toString(),
@ -59,14 +59,21 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
return globalConfigurationsModel.first;
}
return null;
// } catch (e) {
// log("record:");
// log(e.toString());
// loggerService.logError(e.toString());
// loggerService.logToFile(message: e.toString(), source: "getGlobalScreenConfigurations-> screen_details_repo.dart", type: LogTypeEnum.error);
// InfoComponents.showToast(e.toString());
// return null;
// }
} catch (e) {
log("getGlobalScreenConfigurations error:");
log(e.toString());
loggerService.logError(e.toString());
loggerService.logToFile(message: e.toString(), source: "getGlobalScreenConfigurations-> screen_details_repo.dart", type: LogTypeEnum.error);
// Check if this is a CORS error on web
if (e.toString().contains('Failed to fetch') || e.toString().contains('CORS')) {
loggerService.logError('⚠️ CORS ERROR: Configuration API is also blocked by browser CORS policy.');
loggerService.logError('📋 The backend needs CORS headers for ALL API endpoints, not just SignalR hub.');
}
InfoComponents.showToast(e.toString());
return null;
}
}
@override

@ -1,10 +1,12 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:hmg_qline/services/logger_service.dart';
import 'package:hmg_qline/utilities/enums.dart';
import 'package:http/io_client.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:signalr_core/signalr_core.dart';
import 'package:universal_html/html.dart' as html;
abstract class SignalrRepo {
Future<bool?> startHubConnection({
@ -22,6 +24,7 @@ abstract class SignalrRepo {
class SignalrRepoImp implements SignalrRepo {
HubConnection? connection;
LoggerService loggerService;
String? _lastDeviceIp; // Store last used IP for reconnection
bool get isConnected => connection?.state == HubConnectionState.connected;
@ -37,19 +40,34 @@ class SignalrRepoImp implements SignalrRepo {
required Function(dynamic) onHubDisconnected,
}) async {
try {
// On web, retrieve from localStorage if deviceIp is empty
String actualDeviceIp = deviceIp;
if (kIsWeb && (deviceIp.isEmpty || deviceIp == '')) {
final storedIp = html.window.localStorage['device_ip'];
if (storedIp != null && storedIp.isNotEmpty && storedIp != 'null') {
actualDeviceIp = storedIp;
log('📡 Retrieved IP from localStorage for SignalR: $actualDeviceIp');
}
}
// Store the IP for future reconnection attempts
_lastDeviceIp = actualDeviceIp;
String hubBaseURL = ApiConstants.baseUrlHub;
if ((connection != null) && (connection!.state == HubConnectionState.connected || connection!.state == HubConnectionState.connecting)) {
return true;
}
connection = HubConnectionBuilder()
.withUrl(
"$hubBaseURL?IPAddress=$deviceIp",
HttpConnectionOptions(
client: IOClient(HttpClient()..badCertificateCallback = (x, y, z) => true),
logging: (level, message) => log(message),
))
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Custom retry delays
.build();
// Build connection with platform-specific options
final connectionBuilder = HubConnectionBuilder().withUrl(
"$hubBaseURL?IPAddress=$actualDeviceIp",
HttpConnectionOptions(
client: kIsWeb ? null : IOClient(HttpClient()..badCertificateCallback = (x, y, z) => true),
logging: (level, message) => log(message),
),
).withAutomaticReconnect([0, 2000, 5000, 10000, 30000]);
connection = connectionBuilder.build();
connection!.serverTimeoutInMilliseconds = 120000; // 2 minutes
connection!.keepAliveIntervalInMilliseconds = 15000; // 15 seconds keep-alive
@ -64,6 +82,46 @@ class SignalrRepoImp implements SignalrRepo {
loggerService.logToFile(message: "SignalR reconnect attempt #$reconnectAttempts", source: "startHubConnection -> signalR_repo.dart", type: LogTypeEnum.data);
await Future.delayed(reconnectDelay);
try {
// On web, check localStorage again for IP before reconnecting
String reconnectIp = _lastDeviceIp ?? actualDeviceIp;
if (kIsWeb) {
final storedIp = html.window.localStorage['device_ip'];
if (storedIp != null && storedIp.isNotEmpty && storedIp != 'null') {
reconnectIp = storedIp;
}
}
// If connection is completely dead, rebuild it
if (connection == null || connection!.state == HubConnectionState.disconnected) {
log('🔄 Rebuilding SignalR connection with IP: $reconnectIp');
// Build connection with platform-specific options
final connectionBuilder = HubConnectionBuilder().withUrl(
"$hubBaseURL?IPAddress=$reconnectIp",
HttpConnectionOptions(
client: kIsWeb ? null : IOClient(HttpClient()..badCertificateCallback = (x, y, z) => true),
logging: (level, message) => log(message),
),
).withAutomaticReconnect([0, 2000, 5000, 10000, 30000]);
connection = connectionBuilder.build();
connection!.serverTimeoutInMilliseconds = 120000;
connection!.keepAliveIntervalInMilliseconds = 15000;
// Re-register event handlers
connection!.on(ApiConstants.sendQLinePatientCall, (List<Object?>? message) {
onHubTicketCall(message);
});
connection!.on(ApiConstants.sendQLineConfig, (List<Object?>? message) {
onHubConfigCall(message);
});
connection!.onreconnecting((exception) => onHubConnecting(exception));
connection!.onreconnected((connectionId) {
log(connectionId.toString());
onHubReconnected(connectionId);
});
}
await connection!.start();
loggerService.logToFile(message: "SignalR reconnected after disconnect", source: "startHubConnection -> signalR_repo.dart", type: LogTypeEnum.data);
reconnectAttempts = 0; // Reset on success
@ -94,6 +152,20 @@ class SignalrRepoImp implements SignalrRepo {
} catch (e) {
loggerService.logError(e.toString());
loggerService.logToFile(message: e.toString(), source: "startHubConnection -> signalR_repo.dart", type: LogTypeEnum.error);
// Check if this is a CORS error (web-specific)
if (kIsWeb && e.toString().contains('Failed to fetch')) {
loggerService.logError('⚠️ CORS ERROR: The web browser is blocking the connection to the SignalR hub.');
loggerService.logError('📋 This is a server configuration issue. The backend needs to add CORS headers.');
loggerService.logError('✅ Mobile app works fine - this only affects web browsers.');
loggerService.logError('📖 See CORS_ISSUE_SOLUTION.md for detailed fix instructions.');
loggerService.logToFile(
message: 'CORS Error on Web - Server needs CORS configuration. See CORS_ISSUE_SOLUTION.md',
source: "startHubConnection -> signalR_repo.dart",
type: LogTypeEnum.error,
);
}
return false;
}
}
@ -108,7 +180,7 @@ class SignalrRepoImp implements SignalrRepo {
@override
closeConnection() async {
if (connection!.state == HubConnectionState.connected || connection!.state == HubConnectionState.connecting) {
if (connection != null && (connection!.state == HubConnectionState.connected || connection!.state == HubConnectionState.connecting)) {
await connection!.stop();
}
}

@ -1,4 +1,6 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:just_audio/just_audio.dart';
import 'package:universal_html/html.dart' as html;
abstract class AudioService {
Future<void> playTone({required String path, bool isMute = false});
@ -24,6 +26,8 @@ class AudioServiceImp implements AudioService {
@override
Future<void> playTone({required String path, bool isMute = false}) async {
// just_audio has web support - should work on both platforms
// Web note: First audio play may require user interaction due to browser autoplay policy
audioPlayerInstance.setAsset(path);
if (isMute) {
audioPlayerInstance.setVolume(0.0);

@ -2,6 +2,19 @@ import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:universal_html/html.dart' as html;
import 'dart:developer' as dev;
import 'package:js/js.dart';
@JS('startIPDetection')
external void startIPDetectionJS();
@JS('deviceIPResult')
external String? get deviceIPResultJS;
@JS('deviceIPDetecting')
external bool? get deviceIPDetectingJS;
abstract class ConnectivityService {
void subscribeToConnectivityChange({
@ -42,22 +55,128 @@ class ConnectivityServiceImp implements ConnectivityService {
@override
Future<String> getCurrentScreenIP() async {
// Web: Auto-detect IP or get from URL/localStorage
if (kIsWeb) {
dev.log('🌐 Web platform - attempting to get device IP...');
// Priority 1: Check URL query parameter: ?ip=12.4.5.1
final uri = Uri.base;
if (uri.queryParameters.containsKey('ip')) {
final ip = uri.queryParameters['ip']!;
dev.log('✅ IP from URL parameter: $ip');
try {
html.window.localStorage['device_ip'] = ip;
dev.log('💾 Saved IP to localStorage: $ip');
} catch (e) {
dev.log('❌ Failed to save IP to localStorage: $e');
}
return ip;
}
// Priority 2: Check localStorage FIRST (before auto-detect to avoid delay)
String? storedIp;
try {
storedIp = html.window.localStorage['device_ip'];
dev.log('🔍 Checking localStorage for saved IP...');
dev.log(' localStorage[device_ip] = $storedIp');
dev.log(' storedIp type: ${storedIp.runtimeType}');
dev.log(' storedIp == null: ${storedIp == null}');
dev.log(' storedIp.isEmpty: ${storedIp?.isEmpty}');
dev.log(' storedIp == "null": ${storedIp == 'null'}');
} catch (e) {
dev.log('❌ Error accessing localStorage: $e');
storedIp = null;
}
if (storedIp != null && storedIp.isNotEmpty && storedIp != 'null') {
dev.log('✅ IP from localStorage: $storedIp');
return storedIp;
} else {
dev.log('⚠️ localStorage check failed - storedIp: "$storedIp"');
}
// Priority 3: Try to auto-detect IP using WebRTC (only if not in localStorage)
dev.log('📡 No stored IP found, attempting auto-detection...');
try {
final detectedIP = await _autoDetectIP();
if (detectedIP != null && detectedIP.isNotEmpty) {
dev.log('✅ IP auto-detected from device: $detectedIP');
try {
html.window.localStorage['device_ip'] = detectedIP;
dev.log('💾 Saved auto-detected IP to localStorage');
} catch (e) {
dev.log('❌ Failed to save auto-detected IP to localStorage: $e');
}
return detectedIP;
}
} catch (e) {
dev.log('⚠️ Auto-detection failed: $e');
}
// No IP found
dev.log('⚠️ No IP address detected');
dev.log('📌 Add to URL: ${uri.origin}/?ip=YOUR_DEVICE_IP');
return '';
}
// Android Native: Get actual IP from network interface
dev.log('📱 Android native - getting IP from network interface...');
const eth = "eth0";
const wlan = "wlan0";
String currentDeviceIp = "";
final ips = await NetworkInterface.list(type: InternetAddressType.IPv4);
for (var interface in ips) {
if (interface.name == eth) {
for (var address in interface.addresses) {
currentDeviceIp = address.address;
try {
final ips = await NetworkInterface.list(type: InternetAddressType.IPv4);
for (var interface in ips) {
if (interface.name == eth) {
for (var address in interface.addresses) {
currentDeviceIp = address.address;
dev.log('✅ IP from ethernet (eth0): $currentDeviceIp');
}
}
}
if (interface.name == wlan) {
for (var address in interface.addresses) {
currentDeviceIp = address.address;
if (interface.name == wlan) {
for (var address in interface.addresses) {
currentDeviceIp = address.address;
dev.log('✅ IP from WiFi (wlan0): $currentDeviceIp');
}
}
}
} catch (e) {
dev.log('❌ Failed to get network interface IP: $e');
}
return currentDeviceIp;
}
/// Auto-detect device IP using WebRTC (works on web browsers)
Future<String?> _autoDetectIP() async {
try {
// Start IP detection in JavaScript
startIPDetectionJS();
// Poll for result
final stopwatch = Stopwatch()..start();
while (stopwatch.elapsed.inSeconds < 5) {
await Future.delayed(const Duration(milliseconds: 100));
// Check if still detecting
final isDetecting = deviceIPDetectingJS;
if (isDetecting == false || isDetecting == null) {
// Detection completed
final result = deviceIPResultJS;
if (result != null && result != 'TIMEOUT' && result != 'ERROR') {
return result;
}
break;
}
}
dev.log('⏱️ IP detection timeout or failed');
return null;
} catch (e) {
dev.log('❌ Auto-detection error: $e');
return null;
}
}
}

@ -1,4 +1,5 @@
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:hmg_qline/config/dependency_injection.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/services/cache_service.dart';
@ -59,10 +60,19 @@ class LoggerServiceImp implements LoggerService {
@override
Future<void> logToFile({required String message, required LogTypeEnum type, required String source}) async {
try {
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
final timestamp = DateFormat('yyyy-MM-dd hh:mm:ss a').format(DateTime.now());
final formattedMessage = "[$timestamp] [SOURCE: $source] ${type.name.toUpperCase()}: $message";
// Web: Console logging only
if (kIsWeb) {
// Always log to console on web
print(formattedMessage);
// TODO: Optional - Store in localStorage/IndexedDB for later download
return;
}
// Android: File logging (existing implementation)
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
final baseDir = await getExternalStorageDirectory();
final logDir = Directory('${baseDir!.path}/logs');
if (!await logDir.exists()) {
@ -93,7 +103,6 @@ class LoggerServiceImp implements LoggerService {
@override
Future<void> logCrash({required Object error, StackTrace? stackTrace, String? context, String source = "CRASH"}) async {
try {
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
final timestamp = DateFormat('yyyy-MM-dd hh:mm:ss a').format(DateTime.now());
const sep = '==================== CRASH ====================';
final contextStr = context ?? 'No context provided';
@ -102,6 +111,15 @@ class LoggerServiceImp implements LoggerService {
final crashMessage = '$sep\n[$timestamp]\nCONTEXT: $contextStr\nERROR: $errorMsg\nSTACKTRACE:\n$stack\n$sep\n';
// Web: Console logging only
if (kIsWeb) {
print(crashMessage);
logger.e('Crash logged: $errorMsg');
return;
}
// Android: File logging (existing implementation)
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
final baseDir = await getExternalStorageDirectory();
final logDir = Directory('${baseDir!.path}/logs');
if (!await logDir.exists()) {

@ -1,11 +1,13 @@
import 'dart:developer';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_tts/flutter_tts.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/models/global_config_model.dart';
import 'package:hmg_qline/models/ticket_model.dart';
import 'package:hmg_qline/services/logger_service.dart';
import 'package:hmg_qline/utilities/enums.dart';
import 'package:universal_html/html.dart' as html;
import 'package:hmg_qline/utilities/extensions.dart';
abstract class TextToSpeechService {
@ -16,6 +18,9 @@ abstract class TextToSpeechService {
});
void listenToTextToSpeechEvents({required Function() onVoiceCompleted});
// Test function to speak any text
Future<void> testSpeak(String text, {LanguageEnum language = LanguageEnum.english});
}
class TextToSpeechServiceImp implements TextToSpeechService {
@ -34,6 +39,13 @@ class TextToSpeechServiceImp implements TextToSpeechService {
required GlobalConfigurationsModel globalConfigurationsModel,
bool isMute = false,
}) async {
// Web Speech API implementation
if (kIsWeb) {
await _speechTextWeb(ticket: ticket, globalConfigurationsModel: globalConfigurationsModel, isMute: isMute);
return;
}
// Android implementation (existing code)
log("ticket data i got: ${ticket.ticketModel.toString()}");
const ttsGoogleEngine = 'com.google.android.tts';
LanguageEnum langEnum = ticket.ticketModel!.voiceLanguageEnum;
@ -188,4 +200,107 @@ class TextToSpeechServiceImp implements TextToSpeechService {
loggerService.logInfo("setCancelHandler");
});
}
// Web Speech API implementation
Future<void> _speechTextWeb({
required TicketDetailsModel ticket,
required GlobalConfigurationsModel globalConfigurationsModel,
bool isMute = false,
}) async {
if (isMute) return;
try {
final synthesis = html.window.speechSynthesis;
if (synthesis == null) {
log('Web Speech API not supported');
return;
}
// Construct message
String preVoice = ticket.ticketModel!.ticketNoText;
String postVoice = '';
if (globalConfigurationsModel.qTypeEnum == QTypeEnum.appointment) {
postVoice = ticket.ticketModel!.callTypeEnum.getCallTextFromCallType(ticket.ticketModel!);
} else {
postVoice = ticket.ticketModel!.postVoiceText;
}
String roomNo = '';
if (globalConfigurationsModel.qTypeEnum != QTypeEnum.appointment && ticket.ticketModel!.roomNo.isNotEmpty) {
roomNo = " ${ticket.ticketModel!.roomNo.toString()}";
}
String ticketNo = ticket.ticketModel!.queueNo.trim().toString();
String message = "$preVoice $ticketNo $postVoice $roomNo";
// Get language
final lang = ticket.ticketModel!.voiceLanguageEnum == LanguageEnum.arabic ? 'ar-SA' : 'en-US';
// Create utterance
final utterance = html.SpeechSynthesisUtterance(message);
utterance.lang = lang;
utterance.rate = 0.9;
utterance.pitch = 1.0;
utterance.volume = 1.0;
// Cancel any ongoing speech
synthesis.cancel();
// Speak
synthesis.speak(utterance);
log('Web TTS: Speaking "$message" in $lang');
} catch (e) {
log('Web TTS Error: $e');
}
}
// Test function to speak any text
@override
Future<void> testSpeak(String text, {LanguageEnum language = LanguageEnum.english}) async {
log('🎤 TEST TTS: Speaking "$text" in ${language.name}');
if (kIsWeb) {
// Web implementation
try {
final synthesis = html.window.speechSynthesis;
if (synthesis == null) {
log('Web Speech API not supported');
return;
}
final lang = language == LanguageEnum.arabic ? 'ar-SA' : 'en-US';
final utterance = html.SpeechSynthesisUtterance(text);
utterance.lang = lang;
utterance.rate = 0.9;
utterance.pitch = 1.0;
utterance.volume = 1.0;
synthesis.cancel();
synthesis.speak(utterance);
log('✅ Web TTS Test: Spoke "$text" in $lang');
} catch (e) {
log('❌ Web TTS Test Error: $e');
}
} else {
// Android implementation
try {
const ttsGoogleEngine = 'com.google.android.tts';
List engines = await textToSpeechInstance.getEngines;
if (engines.contains(ttsGoogleEngine)) {
await textToSpeechInstance.setEngine(ttsGoogleEngine);
}
await textToSpeechInstance.setLanguage(language.enumToString());
textToSpeechInstance.setVolume(1.0);
textToSpeechInstance.setSpeechRate(0.5);
textToSpeechInstance.setPitch(0.9);
await textToSpeechInstance.speak(text);
log('✅ Android TTS Test: Spoke "$text" in ${language.name}');
} catch (e) {
log('❌ Android TTS Test Error: $e');
}
}
}
}

@ -1,6 +1,8 @@
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:hmg_qline/services/logger_service.dart';
import 'package:hmg_qline/utilities/enums.dart';
import 'package:universal_html/html.dart' as html;
abstract class NativeMethodChannelService {
void reopenApp();
@ -89,6 +91,14 @@ class NativeMethodChannelServiceImp implements NativeMethodChannelService {
try {
loggerService.logInfo("Starting smart restart - forceRestart: $forceRestart, cleanupFirst: $cleanupFirst");
// Web: Reload the page
if (kIsWeb) {
loggerService.logInfo("Web: Reloading page");
html.window.location.reload();
return;
}
// Android: Use native restart
if (cleanupFirst) {
await clearAllResources();
await Future.delayed(const Duration(seconds: 1));
@ -159,6 +169,8 @@ class NativeMethodChannelServiceImp implements NativeMethodChannelService {
/// Returns true on Android < 12 (always allowed) or if permission is granted on Android 12+.
@override
Future<bool> canScheduleExactAlarms() async {
if (kIsWeb) return false; // Not applicable on web
try {
final result = await _platform.invokeMethod('canScheduleExactAlarms');
loggerService.logInfo("Can schedule exact alarms: $result");

@ -34,6 +34,11 @@ class QueuingViewModel extends ChangeNotifier {
Future<void> initializeQueueingVM() async {
try {
// Mark as disconnected initially
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
screenConfigViewModel.updateIsHubConnected(false);
// Attempt to connect (will update status based on result)
await startHubConnection();
initializeAudioPlayer();
initializeTextToSpeech();
@ -45,7 +50,7 @@ class QueuingViewModel extends ChangeNotifier {
Future<bool?> startHubConnection() async {
try {
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
return await signalrRepo.startHubConnection(
final result = await signalrRepo.startHubConnection(
deviceIp: screenConfigViewModel.currentScreenIP,
onHubTicketCall: onHubTicketCall,
onHubConfigCall: onHubConfigCall,
@ -53,7 +58,30 @@ class QueuingViewModel extends ChangeNotifier {
onHubDisconnected: onHubDisconnected,
onHubConnecting: onHubConnecting,
);
// Update hub connection status based on result
if (result == true) {
screenConfigViewModel.updateIsHubConnected(true);
loggerService.logToFile(
message: "Hub connection successful",
source: "startHubConnection -> queuing_view_model.dart",
type: LogTypeEnum.connectivity,
);
} else {
screenConfigViewModel.updateIsHubConnected(false);
loggerService.logToFile(
message: "Hub connection failed - result: $result",
source: "startHubConnection -> queuing_view_model.dart",
type: LogTypeEnum.connectivity,
);
}
return result;
} catch (e, st) {
// On error, mark as disconnected
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
screenConfigViewModel.updateIsHubConnected(false);
await getIt.get<CrashHandlerService>().handleCrash(error: e, stackTrace: st, context: 'startHubConnection -> queuing_view_model.dart');
return false;
}

@ -23,8 +23,9 @@ import 'package:hmg_qline/utilities/extensions.dart';
import 'package:hmg_qline/utilities/native_method_handler.dart';
import 'package:hmg_qline/view_models/queuing_view_model.dart';
import 'package:hmg_qline/views/view_helpers/info_components.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:universal_html/html.dart' as html;
// import 'package:timezone/browser.dart' as tz;
class ScreenConfigViewModel extends ChangeNotifier {
@ -49,12 +50,36 @@ class ScreenConfigViewModel extends ChangeNotifier {
await getLastTimeLogsClearedFromCache();
listenNetworkConnectivity();
initializeTimer();
// Platform-specific lifecycle
if (kIsWeb) {
_initializeWebLifecycle();
}
}
StreamSubscription? _visibilitySubscription;
void _initializeWebLifecycle() {
// Listen to page visibility changes
_visibilitySubscription = html.document.onVisibilityChange.listen((event) {
if (html.document.hidden!) {
onAppPaused();
} else {
onAppResumed();
}
});
// Prevent accidental tab close
html.window.onBeforeUnload.listen((event) {
event.preventDefault();
});
}
Future<void> onAppResumed() async {
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
loggerService.logToFile(
message: "[didChangeAppLifecycleState] : [onAppResumed] - App uptime: ${uptimeHours}h",
message:
"[didChangeAppLifecycleState] : [onAppResumed] - App uptime: ${uptimeHours}h",
source: "onAppResumed -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
@ -66,14 +91,50 @@ class ScreenConfigViewModel extends ChangeNotifier {
loggerService.logError("Failed to enable wakelock on resume: $e");
}
// Verify connections are still alive
// On web, wait a bit for network to stabilize after tab becomes visible
if (kIsWeb) {
await Future.delayed(const Duration(milliseconds: 500));
}
// Verify connections are still alive and reconnect if needed
syncHubConnectionState();
// On web, proactively try to reconnect if disconnected
if (kIsWeb && !isHubConnected) {
loggerService.logToFile(
message: "Web page resumed - attempting SignalR reconnection",
source: "onAppResumed -> screen_config_view_model.dart",
type: LogTypeEnum.connectivity,
);
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
try {
// Try to stop existing connection (may be null/disconnected already)
try {
await queuingViewModel.stopHubConnection();
} catch (e) {
log("Stop connection error (expected if already disconnected): $e");
}
await Future.delayed(const Duration(seconds: 1));
// Start fresh connection
final result = await queuingViewModel.startHubConnection();
if (result == true) {
loggerService.logToFile(
message: "SignalR reconnected successfully on resume",
source: "onAppResumed -> screen_config_view_model.dart",
type: LogTypeEnum.connectivity,
);
}
} catch (e) {
loggerService.logError("Failed to reconnect on resume: $e");
}
}
}
Future<void> onAppPaused() async {
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
loggerService.logToFile(
message: "[didChangeAppLifecycleState] : [onAppPaused] - App uptime: ${uptimeHours}h - WARNING: App going to background!",
message:
"[didChangeAppLifecycleState] : [onAppPaused] - App uptime: ${uptimeHours}h - WARNING: App going to background!",
source: "onAppPaused -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
@ -82,19 +143,56 @@ class ScreenConfigViewModel extends ChangeNotifier {
Future<void> onAppDetached() async {
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
loggerService.logToFile(
message: "[didChangeAppLifecycleState] : [onAppDetached] - App uptime: ${uptimeHours}h - CRITICAL: App being killed!",
message:
"[didChangeAppLifecycleState] : [onAppDetached] - App uptime: ${uptimeHours}h - CRITICAL: App being killed!",
source: "onAppDetached -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
}
Future<void> waitForIPAndInitializeConfigVM() async {
int attempts = 0;
const maxAttempts = kIsWeb ? 3 : 30;
// On web, check localStorage immediately on startup (before any detection)
if (kIsWeb && currentScreenIP == "") {
try {
final storedIp = html.window.localStorage['device_ip'];
if (storedIp != null && storedIp.isNotEmpty && storedIp != 'null') {
currentScreenIP = storedIp;
log('✅ IP retrieved from localStorage on startup: $currentScreenIP');
initializeScreenConfigVM();
return;
} else {
log('⚠️ No IP found in localStorage on startup (storedIp: "$storedIp")');
}
} catch (e) {
log('❌ Error accessing localStorage on startup: $e');
loggerService.logError('localStorage access error: $e');
}
}
while (currentScreenIP == "") {
await getCurrentScreenIP();
if (currentScreenIP != "") {
initializeScreenConfigVM();
return;
} else {
await Future.delayed(const Duration(seconds: 2));
attempts++;
if (attempts >= maxAttempts) {
if (kIsWeb) {
// On web, show configuration dialog
loggerService.logError(
"No IP auto-detected or configured after $attempts attempts.");
throw Exception("IP_CONFIGURATION_REQUIRED");
} else {
// On Android native, keep trying
loggerService.logError("Failed to get IP after $attempts attempts");
}
}
// Wait 4 seconds on first attempt to allow auto-detection
final waitTime = attempts == 1 ? 4 : 2;
await Future.delayed(Duration(seconds: waitTime));
}
}
}
@ -106,7 +204,7 @@ class ScreenConfigViewModel extends ChangeNotifier {
notifyListeners();
}
bool isHubConnected = true;
bool isHubConnected = false; // Start as disconnected until connection succeeds
updateIsHubConnected(bool value) {
isHubConnected = value;
@ -140,11 +238,33 @@ class ScreenConfigViewModel extends ChangeNotifier {
String currentScreenIP = "";
Future<void> getCurrentScreenIP() async {
log("Using TEST IP");
if (useTestIP) {
currentScreenIP = AppConstants.testIP;
log("🧪 Using TEST IP: $currentScreenIP");
} else {
currentScreenIP = await connectivityService.getCurrentScreenIP();
log("currentScreenIP: $currentScreenIP");
if (currentScreenIP.isEmpty) {
log("⚠️ WARNING: No IP address detected!");
if (kIsWeb) {
log("📌 For web, add IP to URL: ?ip=YOUR_IP_ADDRESS");
log(" Example: http://localhost:port/?ip=12.4.5.1");
}
} else {
log("✅ Current Screen IP: $currentScreenIP");
// On web, ensure IP is saved to localStorage
if (kIsWeb) {
try {
html.window.localStorage['device_ip'] = currentScreenIP;
log("💾 Saved IP to localStorage: $currentScreenIP");
} catch (e) {
log("❌ Failed to save IP to localStorage: $e");
loggerService.logError('localStorage save error: $e');
}
}
}
}
}
@ -162,10 +282,15 @@ class ScreenConfigViewModel extends ChangeNotifier {
);
}
GlobalConfigurationsModel globalConfigurationsModel = GlobalConfigurationsModel();
GlobalConfigurationsModel globalConfigurationsModel =
GlobalConfigurationsModel();
Future<void> getGlobalConfigurationsByIP() async {
GlobalConfigurationsModel? response = await screenDetailsRepo.getGlobalScreenConfigurations(ipAddress: currentScreenIP);
GlobalConfigurationsModel? response = await screenDetailsRepo
.getGlobalScreenConfigurations(ipAddress: currentScreenIP);
loggerService.logError("response was: $response");
if (response == null) {
loggerService.logError("response was: $response");
@ -179,12 +304,16 @@ class ScreenConfigViewModel extends ChangeNotifier {
notifyListeners();
}
void updateGlobalConfigurationsModel({required var value, bool needNotify = false, bool shouldUpdateNextPrayer = false}) {
void updateGlobalConfigurationsModel(
{required var value,
bool needNotify = false,
bool shouldUpdateNextPrayer = false}) {
// Ensure the incoming value is the GlobalConfigurationsModel and set the Takhasusi flag based on projectID
if (value is GlobalConfigurationsModel) {
// Use projectID-based detection (projectID = 16 for Takhasusi Main)
try {
value.isFromTakhasusiMain = value.projectID == AppConstants.projectIdTakhasusiMain;
value.isFromTakhasusiMain =
value.projectID == AppConstants.projectIdTakhasusiMain;
// Debug logging for Takhasusi Main detection
log("=== TAKHASUSI MAIN CHECK (ProjectID-based) ===");
@ -227,13 +356,15 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
int currentDate = DateTime.now().millisecondsSinceEpoch;
await cacheService.setLastTimeUpdatedInCache(lasTimeUpdated: currentDate.toString());
await cacheService.setLastTimeUpdatedInCache(
lasTimeUpdated: currentDate.toString());
}
RssFeedModel rssFeedModel = RssFeedModel();
Future<void> getRssFeedDetailsFromServer() async {
RssFeedModel? response = await screenDetailsRepo.getRssFeedDetailsByLanguageID(languageId: 0);
RssFeedModel? response =
await screenDetailsRepo.getRssFeedDetailsByLanguageID(languageId: 0);
if (response == null) {
return;
@ -247,9 +378,14 @@ class ScreenConfigViewModel extends ChangeNotifier {
Future<void> getPrayerDetailsFromServer() async {
double testLatitude = 24.722136;
double testLongitude = 46.774303;
PrayersWidgetModel? response = await screenDetailsRepo.getPrayerDetailsByLatLong(
latitude: globalConfigurationsModel.projectLatitude == 0.0 ? testLatitude : globalConfigurationsModel.projectLatitude ?? testLatitude,
longitude: globalConfigurationsModel.projectLongitude == 0.0 ? testLongitude : globalConfigurationsModel.projectLongitude ?? testLongitude,
PrayersWidgetModel? response =
await screenDetailsRepo.getPrayerDetailsByLatLong(
latitude: globalConfigurationsModel.projectLatitude == 0.0
? testLatitude
: globalConfigurationsModel.projectLatitude ?? testLatitude,
longitude: globalConfigurationsModel.projectLongitude == 0.0
? testLongitude
: globalConfigurationsModel.projectLongitude ?? testLongitude,
);
if (response == null) {
@ -265,10 +401,13 @@ class ScreenConfigViewModel extends ChangeNotifier {
Future<void> getWeatherDetailsFromServer() async {
int testCityKey = 297030;
WeathersWidgetModel? response = await screenDetailsRepo.getWeatherDetailsByCity(
cityId:
((globalConfigurationsModel.cityKey == null || globalConfigurationsModel.cityKey == 0) ? testCityKey : globalConfigurationsModel.cityKey)
.toString(),
WeathersWidgetModel? response =
await screenDetailsRepo.getWeatherDetailsByCity(
cityId: ((globalConfigurationsModel.cityKey == null ||
globalConfigurationsModel.cityKey == 0)
? testCityKey
: globalConfigurationsModel.cityKey)
.toString(),
);
if (response == null) {
@ -289,7 +428,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
// log("Data Before Check : ${prayersWidgetModel.fajr}");
// log("Data Before Check : ${globalConfigurationsModel.isPrayerTimeReq}");
if (globalConfigurationsModel.isPrayerTimeReq && prayersWidgetModel.fajr == null) {
if (globalConfigurationsModel.isPrayerTimeReq &&
prayersWidgetModel.fajr == null) {
await getPrayerDetailsFromServer();
}
@ -298,7 +438,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
// log("current: ${DateFormat('yyyy-MM-dd hh:mm a').format(DateTime.now().toLocal())}");
// log("current: ${DateTime.now().timeZoneName}");
if (prayersWidgetModel.fajr != null && prayersWidgetModel.fajr!.toDateTimeFromInt().isAfter(current)) {
if (prayersWidgetModel.fajr != null &&
prayersWidgetModel.fajr!.toDateTimeFromInt().isAfter(current)) {
final namazTime = prayersWidgetModel.fajr!.toFormattedDateTimeFromInt();
nextPrayerTime = namazTime;
nextPrayerToShowEng = "${globalConfigurationsModel.fajarTextEng}";
@ -306,7 +447,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
notifyListeners();
return;
}
if (prayersWidgetModel.dhuhr != null && prayersWidgetModel.dhuhr!.toDateTimeFromInt().isAfter(current)) {
if (prayersWidgetModel.dhuhr != null &&
prayersWidgetModel.dhuhr!.toDateTimeFromInt().isAfter(current)) {
final namazTime = prayersWidgetModel.dhuhr!.toFormattedDateTimeFromInt();
nextPrayerTime = namazTime;
nextPrayerToShowEng = "${globalConfigurationsModel.dhuhrTextEng}";
@ -315,7 +457,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
notifyListeners();
return;
}
if (prayersWidgetModel.asr != null && prayersWidgetModel.asr!.toDateTimeFromInt().isAfter(current)) {
if (prayersWidgetModel.asr != null &&
prayersWidgetModel.asr!.toDateTimeFromInt().isAfter(current)) {
final namazTime = prayersWidgetModel.asr!.toFormattedDateTimeFromInt();
nextPrayerTime = namazTime;
nextPrayerToShowEng = "${globalConfigurationsModel.asarTextEng}";
@ -324,8 +467,10 @@ class ScreenConfigViewModel extends ChangeNotifier {
notifyListeners();
return;
}
if (prayersWidgetModel.maghrib != null && prayersWidgetModel.maghrib!.toDateTimeFromInt().isAfter(current)) {
final namazTime = prayersWidgetModel.maghrib!.toFormattedDateTimeFromInt();
if (prayersWidgetModel.maghrib != null &&
prayersWidgetModel.maghrib!.toDateTimeFromInt().isAfter(current)) {
final namazTime =
prayersWidgetModel.maghrib!.toFormattedDateTimeFromInt();
nextPrayerTime = namazTime;
nextPrayerToShowEng = "${globalConfigurationsModel.maghribTextEng}";
nextPrayerToShowArb = "${globalConfigurationsModel.maghribTextArb}";
@ -333,7 +478,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
notifyListeners();
return;
}
if (prayersWidgetModel.isha != null && prayersWidgetModel.isha!.toDateTimeFromInt().isAfter(current)) {
if (prayersWidgetModel.isha != null &&
prayersWidgetModel.isha!.toDateTimeFromInt().isAfter(current)) {
final namazTime = prayersWidgetModel.isha!.toFormattedDateTimeFromInt();
nextPrayerTime = namazTime;
nextPrayerToShowEng = "${globalConfigurationsModel.ishaTextEng}";
@ -363,7 +509,9 @@ class ScreenConfigViewModel extends ChangeNotifier {
_midnightTimer?.cancel();
_healthCheckTimer?.cancel();
if (!(globalConfigurationsModel.isWeatherReq) && !(globalConfigurationsModel.isPrayerTimeReq) && !(globalConfigurationsModel.isRssFeedReq)) {
if (!(globalConfigurationsModel.isWeatherReq) &&
!(globalConfigurationsModel.isPrayerTimeReq) &&
!(globalConfigurationsModel.isRssFeedReq)) {
// Even if widgets are not required, start a minimal health check
_startHealthCheckTimer();
return;
@ -391,8 +539,17 @@ class ScreenConfigViewModel extends ChangeNotifier {
// At midnight, update weather and prayer details if required
if (now.day != lastChecked.day) {
if (now.difference(now.copyWith(hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0)).inMinutes >= 5) {
await nativeMethodChannelService.smartRestart(forceRestart: true, cleanupFirst: true);
if (now
.difference(now.copyWith(
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0))
.inMinutes >=
5) {
await nativeMethodChannelService.smartRestart(
forceRestart: true, cleanupFirst: true);
lastChecked = now;
}
}
@ -418,7 +575,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
log("Health check - App uptime: ${uptimeHours}h ${uptimeMinutes}m");
loggerService.logToFile(
message: "Health check performed - App uptime: ${uptimeHours}h ${uptimeMinutes}m (started: ${appStartTime.toString().substring(0, 19)})",
message:
"Health check performed - App uptime: ${uptimeHours}h ${uptimeMinutes}m (started: ${appStartTime.toString().substring(0, 19)})",
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
@ -426,7 +584,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
// Check if app has been running for dangerously long (>60 hours)
if (uptimeHours > 60) {
loggerService.logToFile(
message: "WARNING: App running for ${uptimeHours} hours - scheduling proactive restart",
message:
"WARNING: App running for ${uptimeHours} hours - scheduling proactive restart",
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
@ -435,20 +594,47 @@ class ScreenConfigViewModel extends ChangeNotifier {
// Sync hub connection state
try {
syncHubConnectionState();
// On web, if disconnected for 2 consecutive checks, force reconnection
if (kIsWeb && !isHubConnected && healthCheckFailures >= 2) {
loggerService.logToFile(
message:
"Web disconnected for ${healthCheckFailures} checks - forcing reconnection",
source:
"_startHealthCheckTimer -> screen_config_view_model.dart",
type: LogTypeEnum.connectivity,
);
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
try {
await queuingViewModel.stopHubConnection();
} catch (e) {
log("Stop connection error during health check (may be expected): $e");
}
await Future.delayed(const Duration(seconds: 2));
await queuingViewModel.startHubConnection();
}
healthCheckFailures = 0; // Reset on success
} catch (e) {
healthCheckFailures++;
loggerService.logError("Health check - Hub sync failed (${healthCheckFailures} consecutive): $e");
loggerService.logError(
"Health check - Hub sync failed (${healthCheckFailures} consecutive): $e");
// If multiple consecutive failures, try to recover
if (healthCheckFailures >= 3) {
loggerService.logToFile(
message: "CRITICAL: ${healthCheckFailures} consecutive health check failures - attempting recovery",
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
message:
"CRITICAL: ${healthCheckFailures} consecutive health check failures - attempting recovery",
source:
"_startHealthCheckTimer -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
await queuingViewModel.stopHubConnection();
try {
await queuingViewModel.stopHubConnection();
} catch (e) {
log("Stop connection error during recovery (may be expected): $e");
}
await Future.delayed(const Duration(seconds: 2));
await queuingViewModel.startHubConnection();
}
@ -463,7 +649,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
// Log memory/performance indicators
loggerService.logToFile(
message: "Health check - Hub connected: $isHubConnected, Internet: $isInternetConnected",
message:
"Health check - Hub connected: $isHubConnected, Internet: $isInternetConnected",
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
type: LogTypeEnum.connectivity,
);
@ -472,12 +659,14 @@ class ScreenConfigViewModel extends ChangeNotifier {
notifyListeners();
} catch (e, stackTrace) {
healthCheckFailures++;
loggerService.logError("Health check timer error (${healthCheckFailures} consecutive): $e\n$stackTrace");
loggerService.logError(
"Health check timer error (${healthCheckFailures} consecutive): $e\n$stackTrace");
// If health check itself is failing repeatedly, something is seriously wrong
if (healthCheckFailures >= 5) {
loggerService.logToFile(
message: "FATAL: Health check failing repeatedly - app may be in bad state",
message:
"FATAL: Health check failing repeatedly - app may be in bad state",
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
@ -489,9 +678,11 @@ class ScreenConfigViewModel extends ChangeNotifier {
@override
void dispose() {
_visibilitySubscription?.cancel();
_midnightTimer?.cancel();
_healthCheckTimer?.cancel();
patientIdController.dispose();
mobileScannerController?.dispose();
super.dispose();
}
@ -511,14 +702,16 @@ class ScreenConfigViewModel extends ChangeNotifier {
lastTimeLogsCleared = await cacheService.getLastTimeLogsCleared();
if (lastTimeLogsCleared == null) {
await cacheService
.setLastTimeLogsCleared(lastTimeCleared: DateTime.now().millisecondsSinceEpoch)
.setLastTimeLogsCleared(
lastTimeCleared: DateTime.now().millisecondsSinceEpoch)
.whenComplete(() => lastTimeLogsCleared = DateTime.now());
}
}
// *************************** KIOSK FUNCTIONS *************************
KioskScreenStateEnums kioskScreenStateEnum = KioskScreenStateEnums.languageState;
KioskScreenStateEnums kioskScreenStateEnum =
KioskScreenStateEnums.languageState;
void updateKioskScreenState(KioskScreenStateEnums state) {
kioskScreenStateEnum = state;
@ -546,7 +739,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
notifyListeners();
}
Future<bool> generateTicketForQueue({required KioskQueueModel kioskQueueModel, int patientId = 0}) async {
Future<bool> generateTicketForQueue(
{required KioskQueueModel kioskQueueModel, int patientId = 0}) async {
updateKioskScreenState(KioskScreenStateEnums.busyState);
KioskPatientTicket? kioskPatientTicket = await createTicketFromKiosk(
projectId: kioskQueueModel.projectID ?? 0,
@ -566,7 +760,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
int startTicket = 123457100;
for (int i = 0; i < numOfTicketsToCreate; i++) {
GenericRespModel? response = await screenDetailsRepo.createTestTickets(ticketNumber: startTicket);
GenericRespModel? response =
await screenDetailsRepo.createTestTickets(ticketNumber: startTicket);
startTicket = startTicket + 1;
if (response == null || response.messageStatus != 1) {
log("response null from createNextTickets");
@ -577,9 +772,11 @@ class ScreenConfigViewModel extends ChangeNotifier {
log("last ticket is: $startTicket ");
}
Future<KioskPatientTicket?> createTicketFromKiosk({required int projectId, required int queueId, int patientId = 0}) async {
Future<KioskPatientTicket?> createTicketFromKiosk(
{required int projectId, required int queueId, int patientId = 0}) async {
try {
GenericRespModel? response = await screenDetailsRepo.createTicketFromKiosk(
GenericRespModel? response =
await screenDetailsRepo.createTicketFromKiosk(
projectId: projectId,
queueId: queueId,
patientId: patientId,
@ -596,7 +793,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
}
Future<void> acknowledgeTicket({required int ticketQueueID, required String ipAddress}) async {
Future<void> acknowledgeTicket(
{required int ticketQueueID, required String ipAddress}) async {
GenericRespModel? response = await screenDetailsRepo.acknowledgeTicket(
ipAddress: ipAddress,
ticketQueueID: ticketQueueID,
@ -611,17 +809,22 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
Future<void> acknowledgeTicketForAppointmentOnly(
{required int ticketQueueID, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
GenericRespModel? response = await screenDetailsRepo.acknowledgeTicketForAppointment(
{required int ticketQueueID,
required String ipAddress,
required CallTypeEnum callTypeEnum}) async {
GenericRespModel? response =
await screenDetailsRepo.acknowledgeTicketForAppointment(
ticketId: ticketQueueID,
ipAddress: ipAddress,
callTypeEnum: callTypeEnum,
);
if (response == null || response.messageStatus != 1) {
loggerService.logError("response null from acknowledgeTicketForAppointmentOnly");
loggerService
.logError("response null from acknowledgeTicketForAppointmentOnly");
return;
} else {
loggerService.logInfo("response from acknowledgeTicketForAppointmentOnly ${response.data}");
loggerService.logInfo(
"response from acknowledgeTicketForAppointmentOnly ${response.data}");
}
}
@ -633,73 +836,52 @@ class ScreenConfigViewModel extends ChangeNotifier {
int? patientId = int.tryParse(text);
if (patientId != null && patientId > 0) {
isGeneratingTicket = true;
await generateTicketForQueue(kioskQueueModel: currentSelectedKioskQueueModel, patientId: patientId);
await generateTicketForQueue(
kioskQueueModel: currentSelectedKioskQueueModel,
patientId: patientId);
patientIdController.clear();
await Future.delayed(const Duration(seconds: 2)).whenComplete(() => isGeneratingTicket = false);
await Future.delayed(const Duration(seconds: 2))
.whenComplete(() => isGeneratingTicket = false);
}
}
// *************************** QR SCANNER FUNCTIONS *************************
Barcode? qrCodeResult;
QRViewController? qrViewController;
MobileScannerController? mobileScannerController;
bool isGeneratingTicket = false;
Future<void> flipCamera() async {
await qrViewController!.flipCamera();
void initializeMobileScanner() {
mobileScannerController = MobileScannerController(
facing: kIsWeb ? CameraFacing.front : CameraFacing.back,
detectionSpeed: DetectionSpeed.normal,
);
}
void onQRViewCreated({
required QRViewController controller,
required VoidCallback onSuccess,
required VoidCallback onFailure,
}) async {
qrViewController = controller;
void onQRDetected(BarcodeCapture barcodes) async {
if (isGeneratingTicket) return;
controller.scannedDataStream.listen((scanData) async {
loggerService.logInfo("Found: ${scanData.code} and isGeneratingTicket: $isGeneratingTicket");
if (isGeneratingTicket) return;
await validateAndGenerateTicketFromQR(data: scanData, onFailure: onFailure, onSuccess: onSuccess);
});
await flipCamera();
}
final barcode = barcodes.barcodes.firstOrNull;
if (barcode == null || barcode.rawValue == null) return;
Future<void> validateAndGenerateTicketFromQR({required var data, required VoidCallback onSuccess, required VoidCallback onFailure}) async {
if (data == null) return;
qrCodeResult = data;
notifyListeners();
loggerService.logInfo(
"QR Found: ${barcode.rawValue} and isGeneratingTicket: $isGeneratingTicket");
final code = qrCodeResult!.code;
int? patientId = int.tryParse(code ?? '');
final patientId = int.tryParse(barcode.rawValue!);
if (patientId != null && patientId > 0) {
isGeneratingTicket = true;
bool status = await generateTicketForQueue(kioskQueueModel: currentSelectedKioskQueueModel, patientId: patientId);
qrCodeResult = null;
notifyListeners();
if (status) {
log("status: $status");
onSuccess();
}
await Future.delayed(const Duration(seconds: 2)).whenComplete(() => isGeneratingTicket = false);
}
}
Future<void> reassemble() async {
if (qrViewController != null) {
if (defaultTargetPlatform == TargetPlatform.android) {
await qrViewController!.pauseCamera();
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
await qrViewController!.resumeCamera();
}
await qrViewController!.flipCamera();
await generateTicketForQueue(
kioskQueueModel: currentSelectedKioskQueueModel,
patientId: patientId,
);
await Future.delayed(const Duration(seconds: 2))
.whenComplete(() => isGeneratingTicket = false);
}
}
void syncHubConnectionState() async {
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
bool newState = (queuingViewModel.signalrRepo as SignalrRepoImp).isConnected;
bool newState =
(queuingViewModel.signalrRepo as SignalrRepoImp).isConnected;
loggerService.logToFile(
message: "Hub Current Status : $newState",
@ -713,8 +895,12 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
if (!isHubConnected) {
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
bool? status = await queuingViewModel.startHubConnection();
// syncHubConnectionState will update isHubConnected
try {
await queuingViewModel.startHubConnection();
// syncHubConnectionState will update isHubConnected
} catch (e) {
loggerService.logError("Failed to start hub connection in sync: $e");
}
} else {
log("Hub is Connected!");
}

@ -4,18 +4,31 @@ import 'package:hmg_qline/utilities/extensions.dart';
import 'package:hmg_qline/view_models/screen_config_view_model.dart';
import 'package:hmg_qline/views/view_helpers/size_config.dart';
import 'package:provider/provider.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class KioskQrScannerScreen extends StatelessWidget {
KioskQrScannerScreen({super.key});
class KioskQrScannerScreen extends StatefulWidget {
const KioskQrScannerScreen({super.key});
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
@override
State<KioskQrScannerScreen> createState() => _KioskQrScannerScreenState();
}
class _KioskQrScannerScreenState extends State<KioskQrScannerScreen> {
@override
void initState() {
super.initState();
// Initialize scanner
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ScreenConfigViewModel>().initializeMobileScanner();
});
}
@override
Widget build(BuildContext context) {
final screenConfigViewModel = context.read<ScreenConfigViewModel>();
final size = MediaQuery.of(context).size;
final scanArea = (size.width < size.height ? size.width : size.height) * 0.6;
return Scaffold(
body: SafeArea(
child: Stack(
@ -24,22 +37,19 @@ class KioskQrScannerScreen extends StatelessWidget {
children: <Widget>[
Expanded(
flex: 9,
child: QRView(
key: qrKey,
overlay: QrScannerOverlayShape(
child: MobileScanner(
controller: screenConfigViewModel.mobileScannerController,
onDetect: screenConfigViewModel.onQRDetected,
overlay: CustomPaint(
painter: ScannerOverlayPainter(
borderColor: AppColors.redColor,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: scanArea,
),
onQRViewCreated: (controller) {
screenConfigViewModel.onQRViewCreated(
controller: controller,
onSuccess: () => context.popScreen(),
onFailure: () {},
);
}),
),
),
),
],
),
@ -48,7 +58,7 @@ class KioskQrScannerScreen extends StatelessWidget {
left: 0,
right: 0,
child: Container(
color: Colors.black.withOpacity(0.4),
color: Colors.black.withValues(alpha: 0.4),
padding: const EdgeInsets.all(16),
child: Align(
alignment: Alignment.centerLeft,
@ -78,7 +88,7 @@ class KioskQrScannerScreen extends StatelessWidget {
),
),
GestureDetector(
onTap: () => screenConfigViewModel.flipCamera(),
onTap: () => screenConfigViewModel.mobileScannerController?.switchCamera(),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -104,16 +114,8 @@ class KioskQrScannerScreen extends StatelessWidget {
builder: (BuildContext context, ScreenConfigViewModel screenConfigViewModel, _) {
String text = "";
if (screenConfigViewModel.qrCodeResult != null) {
final code = screenConfigViewModel.qrCodeResult?.code;
int? patientId = int.tryParse(code ?? '');
if (patientId != null && patientId > 0) {
text = "Patient ID: $patientId\nGenerating Ticket...";
} else {
text = "Invalid QR Code. Please scan a valid patient ID.";
}
if (screenConfigViewModel.isGeneratingTicket) {
text = "Generating Ticket...";
} else {
text = "Please scan your Patient QR from Sulaiman Al Habib Mobile App";
}
@ -140,3 +142,108 @@ class KioskQrScannerScreen extends StatelessWidget {
);
}
}
/// Custom painter for QR scanner overlay with corner borders
class ScannerOverlayPainter extends CustomPainter {
final Color borderColor;
final double borderRadius;
final double borderLength;
final double borderWidth;
final double cutOutSize;
ScannerOverlayPainter({
required this.borderColor,
required this.borderRadius,
required this.borderLength,
required this.borderWidth,
required this.cutOutSize,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black.withValues(alpha: 0.5)
..style = PaintingStyle.fill;
final borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = borderWidth;
final centerX = size.width / 2;
final centerY = size.height / 2;
final halfCutOut = cutOutSize / 2;
final cutOutRect = Rect.fromCenter(
center: Offset(centerX, centerY),
width: cutOutSize,
height: cutOutSize,
);
// Draw overlay with cutout
final overlayPath = Path()
..addRect(Rect.fromLTWH(0, 0, size.width, size.height))
..addRRect(RRect.fromRectAndRadius(cutOutRect, Radius.circular(borderRadius)))
..fillType = PathFillType.evenOdd;
canvas.drawPath(overlayPath, paint);
// Draw corner borders
final left = centerX - halfCutOut;
final top = centerY - halfCutOut;
final right = centerX + halfCutOut;
final bottom = centerY + halfCutOut;
// Top-left corner
canvas.drawLine(
Offset(left, top + borderRadius),
Offset(left, top + borderRadius + borderLength),
borderPaint,
);
canvas.drawLine(
Offset(left + borderRadius, top),
Offset(left + borderRadius + borderLength, top),
borderPaint,
);
// Top-right corner
canvas.drawLine(
Offset(right, top + borderRadius),
Offset(right, top + borderRadius + borderLength),
borderPaint,
);
canvas.drawLine(
Offset(right - borderRadius, top),
Offset(right - borderRadius - borderLength, top),
borderPaint,
);
// Bottom-left corner
canvas.drawLine(
Offset(left, bottom - borderRadius),
Offset(left, bottom - borderRadius - borderLength),
borderPaint,
);
canvas.drawLine(
Offset(left + borderRadius, bottom),
Offset(left + borderRadius + borderLength, bottom),
borderPaint,
);
// Bottom-right corner
canvas.drawLine(
Offset(right, bottom - borderRadius),
Offset(right, bottom - borderRadius - borderLength),
borderPaint,
);
canvas.drawLine(
Offset(right - borderRadius, bottom),
Offset(right - borderRadius - borderLength, bottom),
borderPaint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

@ -29,7 +29,8 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
@override
void initState() {
super.initState();
final ScreenConfigViewModel screenConfigViewModel = context.read<ScreenConfigViewModel>();
final ScreenConfigViewModel screenConfigViewModel =
context.read<ScreenConfigViewModel>();
lifecycleHandler = LifecycleHandler(
onResumed: () => screenConfigViewModel.onAppResumed(),
onPaused: () => screenConfigViewModel.onAppPaused(),
@ -46,9 +47,16 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
Widget dataContent({required BuildContext context}) {
return Consumer2(
builder: (BuildContext context, ScreenConfigViewModel screenConfigViewModel, QueuingViewModel queuingViewModel, Widget? child) {
builder: (BuildContext context,
ScreenConfigViewModel screenConfigViewModel,
QueuingViewModel queuingViewModel,
Widget? child) {
//
// // TODO: FOR TEST
// TODO: FOR TEST - Test TTS with number 50
// queuingViewModel.textToSpeechService.testSpeak("Ticket Number.. A-21. Please visit Doctor", language: LanguageEnum.english);
queuingViewModel.textToSpeechService.testSpeak("مرحبا بك، هذا نص عربي للتجربة", language: LanguageEnum.arabic);
// // TODO: FOR TEST - Original ticket speech test
// queuingViewModel.textToSpeechService.speechText(
// globalConfigurationsModel: screenConfigViewModel.globalConfigurationsModel,
// ticket: queuingViewModel.currentTickets.first,
@ -74,7 +82,9 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
if (screenConfigViewModel.currentQTypeEnum == QTypeEnum.general) {
text = AppStrings.awaitingQueueNumberEng;
}
if (screenConfigViewModel.globalConfigurationsModel.screenLanguageEnum == LanguageEnum.arabic) {
if (screenConfigViewModel
.globalConfigurationsModel.screenLanguageEnum ==
LanguageEnum.arabic) {
if (screenConfigViewModel.currentQTypeEnum == QTypeEnum.general) {
text = AppStrings.awaitingQueueNumberAr;
}
@ -82,7 +92,8 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
fontFamily = AppStrings.fontNameCairo;
}
if (screenConfigViewModel.state == ViewState.error && queuingViewModel.currentTickets.isEmpty) {
if (screenConfigViewModel.state == ViewState.error &&
queuingViewModel.currentTickets.isEmpty) {
widget = intimationWidget(
text: AppStrings.configurationIssueContactAdmin,
fontName: AppStrings.fontNamePoppins,
@ -93,16 +104,25 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
roomText: '',
);
} else if (queuingViewModel.currentTickets.isEmpty) {
bool isForRoomLevel = screenConfigViewModel.globalConfigurationsModel.screenTypeEnum == ScreenTypeEnum.roomLevelScreen;
bool isForRoomLevel =
screenConfigViewModel.globalConfigurationsModel.screenTypeEnum ==
ScreenTypeEnum.roomLevelScreen;
widget = intimationWidget(
text: text,
fontName: fontFamily,
isForRoomLevel: isForRoomLevel,
isRoomNoRequired: screenConfigViewModel.globalConfigurationsModel.isRoomNoRequired ?? true,
counterNo: screenConfigViewModel.globalConfigurationsModel.roomNo ?? 0,
roomText: screenConfigViewModel.globalConfigurationsModel.roomTextEng ?? AppStrings.counterNo,
isRoomNoRequired: screenConfigViewModel
.globalConfigurationsModel.isRoomNoRequired ??
true,
counterNo:
screenConfigViewModel.globalConfigurationsModel.roomNo ?? 0,
roomText:
screenConfigViewModel.globalConfigurationsModel.roomTextEng ??
AppStrings.counterNo,
);
} else if (screenConfigViewModel.globalConfigurationsModel.screenTypeEnum == ScreenTypeEnum.roomLevelScreen) {
} else if (screenConfigViewModel
.globalConfigurationsModel.screenTypeEnum ==
ScreenTypeEnum.roomLevelScreen) {
widget = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -111,13 +131,20 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
counterNoText(
counterNo: screenConfigViewModel.globalConfigurationsModel.roomNo ?? 0,
roomText: screenConfigViewModel.globalConfigurationsModel.roomTextEng ?? AppStrings.counterNo,
isRoomNoRequired: screenConfigViewModel.globalConfigurationsModel.isRoomNoRequired ?? true,
counterNo: screenConfigViewModel
.globalConfigurationsModel.roomNo ??
0,
roomText: screenConfigViewModel
.globalConfigurationsModel.roomTextEng ??
AppStrings.counterNo,
isRoomNoRequired: screenConfigViewModel
.globalConfigurationsModel.isRoomNoRequired ??
true,
),
PriorityTickets(
tickets: [queuingViewModel.currentTickets.first],
globalConfigurationsModel: screenConfigViewModel.globalConfigurationsModel,
globalConfigurationsModel:
screenConfigViewModel.globalConfigurationsModel,
),
const SizedBox.shrink()
],
@ -127,9 +154,12 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
} else {
widget = PriorityTicketsWithSideSection(
tickets: queuingViewModel.currentTickets,
globalConfigurationsModel: screenConfigViewModel.globalConfigurationsModel,
screenOrientationEnum: screenConfigViewModel.globalConfigurationsModel.orientationTypeEnum,
showThankyouMessage: queuingViewModel.currentTickets.length <= AppConstants.thresholdForListUI,
globalConfigurationsModel:
screenConfigViewModel.globalConfigurationsModel,
screenOrientationEnum: screenConfigViewModel
.globalConfigurationsModel.orientationTypeEnum,
showThankyouMessage: queuingViewModel.currentTickets.length <=
AppConstants.thresholdForListUI,
// showThankyouMessage: true,
);
}
@ -147,13 +177,15 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
Widget getBody({required BuildContext context}) {
return Consumer<ScreenConfigViewModel>(
builder: (BuildContext context, ScreenConfigViewModel screenConfigVM, Widget? child) {
builder: (BuildContext context, ScreenConfigViewModel screenConfigVM,
Widget? child) {
return Column(
children: [
Expanded(
flex: 10,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: SizeConfig.getWidthMultiplier()! * 4),
padding: EdgeInsets.symmetric(
horizontal: SizeConfig.getWidthMultiplier()! * 4),
child: dataContent(context: context),
),
),
@ -165,18 +197,21 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
@override
Widget build(BuildContext context) {
final globalConfigurationsModel = context.read<ScreenConfigViewModel>().globalConfigurationsModel;
final globalConfigurationsModel =
context.read<ScreenConfigViewModel>().globalConfigurationsModel;
log("isFromTakhasusiMain: ${globalConfigurationsModel.isFromTakhasusiMain}");
return Selector<ScreenConfigViewModel, ScreenOrientationEnum>(
selector: (context, screenConfigViewModel) => screenConfigViewModel.globalConfigurationsModel.orientationTypeEnum,
builder: (BuildContext context, ScreenOrientationEnum screenOrientationEnum, Widget? child) {
selector: (context, screenConfigViewModel) =>
screenConfigViewModel.globalConfigurationsModel.orientationTypeEnum,
builder: (BuildContext context,
ScreenOrientationEnum screenOrientationEnum, Widget? child) {
//TODO: For Testing Only
log("=== ROTATEDBOX DEBUG ===");
log("ScreenOrientationEnum: $screenOrientationEnum");
log("isFromTakhasusiMain: ${globalConfigurationsModel.isFromTakhasusiMain}");
final quarterTurns =
globalConfigurationsModel.isFromTakhasusiMain ? screenOrientationEnum.getTurnsByOrientationForOlderVersions() : screenOrientationEnum.getTurnsByOrientationForNewVersions();
final quarterTurns = globalConfigurationsModel.isFromTakhasusiMain
? screenOrientationEnum.getTurnsByOrientationForOlderVersions()
: screenOrientationEnum.getTurnsByOrientationForNewVersions();
log("QuarterTurns applied: $quarterTurns");
log("MediaQuery size: ${MediaQuery.of(context).size}");
log("MediaQuery orientation: ${MediaQuery.of(context).orientation}");

@ -1,45 +1,161 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:hmg_qline/config/routes.dart';
import 'package:hmg_qline/utilities/extensions.dart';
import 'package:hmg_qline/view_models/queuing_view_model.dart';
import 'package:hmg_qline/view_models/screen_config_view_model.dart';
import 'package:hmg_qline/views/common_widgets/ip_config_dialog.dart';
import 'package:provider/provider.dart';
import 'package:hmg_qline/utilities/enums.dart';
import 'dart:developer';
import 'package:universal_html/html.dart' as html;
class SplashScreen extends StatelessWidget {
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
Future<void> _loadData(BuildContext context) async {
final screenConfigViewModel = context.read<ScreenConfigViewModel>();
final queuingViewModel = context.read<QueuingViewModel>();
await screenConfigViewModel.waitForIPAndInitializeConfigVM();
await queuingViewModel.initializeQueueingVM();
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
Future<void>? _initializationFuture;
bool _showIpDialog = false;
@override
void initState() {
super.initState();
_initializationFuture = _loadData();
}
Future<void> _loadData() async {
try {
final screenConfigViewModel = context.read<ScreenConfigViewModel>();
final queuingViewModel = context.read<QueuingViewModel>();
await screenConfigViewModel.waitForIPAndInitializeConfigVM();
await queuingViewModel.initializeQueueingVM();
} catch (e) {
if (e.toString().contains('IP_CONFIGURATION_REQUIRED')) {
setState(() => _showIpDialog = true);
rethrow;
}
rethrow;
}
}
static Future<void>? _initializationFuture;
void _reloadWithIp(String ip) {
if (kIsWeb) {
// Ensure IP is saved to localStorage
log('💾 Attempting to save IP to localStorage: $ip');
try {
html.window.localStorage['device_ip'] = ip;
final saved = html.window.localStorage['device_ip'];
log('💾 Saved IP to localStorage in splash: $saved');
log(' Verify: saved == ip: ${saved == ip}');
log(' localStorage keys: ${html.window.localStorage.keys.toList()}');
if (saved != ip) {
log('⚠️ WARNING: Verification mismatch! Expected: $ip, Got: $saved');
}
} catch (e) {
log('❌ Error saving to localStorage: $e');
}
}
final screenConfigViewModel = context.read<ScreenConfigViewModel>();
screenConfigViewModel.currentScreenIP = ip;
log('✅ IP configured in viewModel: ${screenConfigViewModel.currentScreenIP}');
log('✅ IP configured: $ip - Reloading...');
setState(() {
_showIpDialog = false;
_initializationFuture = _loadData();
});
}
@override
Widget build(BuildContext context) {
_initializationFuture ??= _loadData(context);
return FutureBuilder<void>(
future: _initializationFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
kIsWeb ? 'Detecting device IP...' : 'Initializing...',
style: const TextStyle(fontSize: 16),
),
if (kIsWeb) const SizedBox(height: 8),
if (kIsWeb)
const Text(
'Please wait',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
);
}
// If an error occurs
if (snapshot.hasError) {
final error = snapshot.error.toString();
// Check if error is due to missing IP on web
if (kIsWeb &&
(error.contains('IP_CONFIGURATION_REQUIRED') || _showIpDialog)) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_showIpDialog) {
setState(() => _showIpDialog = true);
_showIpConfigDialog();
}
});
return _buildIpConfigScreen();
}
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text('${snapshot.stackTrace}'),
), // Error message
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 64,
),
const SizedBox(height: 16),
const Text(
'Initialization Error',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
snapshot.error.toString(),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
setState(() {
_initializationFuture = _loadData();
});
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
],
),
),
),
);
}
@ -47,20 +163,83 @@ class SplashScreen extends StatelessWidget {
// If the data is loaded successfully
if (snapshot.connectionState == ConnectionState.done) {
Future.delayed(const Duration(seconds: 1)).whenComplete(() {
if (context.read<ScreenConfigViewModel>().currentScreenTypeEnum == ScreenTypeEnum.kioskScreen) {
context.navigateReplaceTo(AppRoutes.kioskMainScreen);
} else {
context.navigateReplaceTo(AppRoutes.mainQueueScreen);
if (mounted) {
if (context.read<ScreenConfigViewModel>().currentScreenTypeEnum ==
ScreenTypeEnum.kioskScreen) {
context.navigateReplaceTo(AppRoutes.kioskMainScreen);
} else {
context.navigateReplaceTo(AppRoutes.mainQueueScreen);
}
}
});
}
return const Scaffold(
body: Center(
child: CircularProgressIndicator(), // Loading indicator
child: CircularProgressIndicator(),
),
);
},
);
}
Widget _buildIpConfigScreen() {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.settings_ethernet,
size: 64,
color: Colors.blue,
),
const SizedBox(height: 24),
const Text(
'Device IP Configuration Required',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 32),
child: Text(
'Please configure the device IP address to continue',
style: TextStyle(fontSize: 14, color: Colors.grey),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: _showIpConfigDialog,
icon: const Icon(Icons.router),
label: const Text('Configure IP Address'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
),
),
],
),
),
);
}
void _showIpConfigDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => IpConfigDialog(
onIpSubmitted: (ip) {
Navigator.of(context).pop();
_reloadWithIp(ip);
},
),
);
}
}

@ -45,7 +45,15 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
@ -89,6 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.6"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
@ -216,6 +232,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.2.0"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http:
dependency: "direct overridden"
description:
@ -240,6 +264,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.19.0"
js:
dependency: "direct main"
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
just_audio:
dependency: "direct main"
description:
@ -324,26 +356,34 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "1b60b8f9d4ce0cb0e7d7bc223c955d083a0737bee66fa1fcfe5de48225e0d5b3"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "3.5.7"
nested:
dependency: transitive
description:
@ -472,14 +512,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5"
qr_code_scanner_plus:
dependency: "direct main"
description:
name: qr_code_scanner_plus
sha256: "39696b50d277097ee4d90d4292de36f38c66213a4f5216a06b2bdd2b63117859"
url: "https://pub.dev"
source: hosted
version: "2.0.10+1"
rxdart:
dependency: transitive
description:
@ -625,10 +657,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
version: "0.7.11"
tuple:
dependency: transitive
description:
@ -645,6 +677,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
universal_html:
dependency: "direct main"
description:
name: universal_html
sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4
url: "https://pub.dev"
source: hosted
version: "2.3.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.dev"
source: hosted
version: "2.3.1"
uuid:
dependency: transitive
description:
@ -758,5 +806,5 @@ packages:
source: hosted
version: "1.0.1"
sdks:
dart: ">=3.8.0-0 <4.0.0"
dart: ">=3.10.0-0 <4.0.0"
flutter: ">=3.24.0"

@ -43,6 +43,7 @@ dependencies:
# http: ^1.2.2
blinking_text: ^1.0.2
just_audio: ^0.9.42
flutter_tts: ^4.2.0
wakelock_plus: ^1.2.10
shared_preferences: ^2.3.5
@ -51,11 +52,14 @@ dependencies:
marquee: ^2.2.3
logger: ^2.4.0
fluttertoast: ^8.2.8
qr_code_scanner_plus: ^2.0.10+1
mobile_scanner: ^3.5.2 # Web-compatible QR scanner (replaces qr_code_scanner_plus)
path_provider: ^2.1.5
flutter_foreground_task: ^9.1.0
# restart_app: ^1.3.2
zo_animated_border: ^1.0.1
# Web support dependencies
universal_html: ^2.2.4 # For web APIs access
js: ^0.6.7 # For JavaScript interop
# animated_flip_counter: ^0.3.4
# smooth_corner: ^1.1.1

Loading…
Cancel
Save