Compare commits

..

No commits in common. 'master_web' and 'master' have entirely different histories.

7
.gitignore vendored

@ -10,11 +10,8 @@
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
@ -44,7 +41,3 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/qline_scripts/apks
/qline_scripts/logs

@ -1,414 +0,0 @@
# 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,160 +0,0 @@
# Configuration API - Status Update
## Question Asked
> "What about the API call to get the configuration? I don't see any API call? API: Common_Config_GetByIP"
## Answer
### ✅ The API Call EXISTS!
**Location:** `/lib/repositories/screen_details_repo.dart`
**Method:** `getGlobalScreenConfigurations()` (lines 41-70)
**Endpoint:** `ApiConstants.commonConfigGet` → `https://ms.hmg.com/nscapi/api/PatientCall/Common_Config_GetByIP`
**Code:**
```dart
Future<GlobalConfigurationsModel?> getGlobalScreenConfigurations({required String ipAddress}) async {
try {
var params = {
"ipAddress": ipAddress.toString(),
"apiKey": AppConstants.apiKey.toString(),
};
GenericRespModel genericModel = await apiClientInstance.postJsonForObject(
(json) => GenericRespModel.fromJson(json),
ApiConstants.commonConfigGet, // ← Uses Common_Config_GetByIP
params,
);
// ... returns GlobalConfigurationsModel
} catch (e) {
// Error handling
}
}
```
### ⚠️ The Problem
The try-catch was **commented out** in the original code, which meant:
1. CORS errors wouldn't be caught properly
2. Errors would bubble up and crash initialization
3. No clear error messages for debugging
### ✅ What Was Fixed
1. **Uncommented the try-catch block**
2. **Added CORS-specific error detection**
3. **Added comprehensive error logging**
**Now the code:**
- Catches CORS errors gracefully
- Logs detailed error information
- Shows user-friendly error messages
- Detects if the error is CORS-related
### 🔍 When Is This Called?
**Call Chain:**
```
Splash Screen
└─> waitForIPAndInitializeConfigVM()
└─> initializeScreenConfigVM()
└─> getGlobalConfigurationsByIP()
└─> screenDetailsRepo.getGlobalScreenConfigurations()
└─> API Call: Common_Config_GetByIP
```
**Location in ViewModel:** `/lib/view_models/screen_config_view_model.dart`
```dart
Future<void> getGlobalConfigurationsByIP() async {
GlobalConfigurationsModel? response = await screenDetailsRepo
.getGlobalScreenConfigurations(ipAddress: currentScreenIP);
// ...
}
```
### ❌ Current Issue: CORS
**On Web:**
```
✅ IP: 10.71.50.53 configured
✅ Initialization started
❌ API call blocked by browser CORS policy
❌ Error: ClientException: Failed to fetch
```
**Why it fails on web:**
- Browser blocks the request to `https://ms.hmg.com`
- Server doesn't have CORS headers configured
- Same issue affects ALL API endpoints:
- `/PatientCallingHub` (SignalR)
- `/api/PatientCall/Common_Config_GetByIP` (Configuration)
- `/api/PatientCall/WeatherForecast_GetBy5Days` (Weather)
- `/api/PatientCall/PrayerTime_Today` (Prayer)
- `/api/PatientCall/RssFeed_Get` (RSS Feed)
- And all other REST APIs
**Why it works on mobile:**
- Native apps don't have CORS restrictions
- Only web browsers enforce CORS policy
### 📋 What Needs to Be Done
**Backend team must:**
1. Add CORS configuration to ASP.NET Core app
2. Apply CORS policy to **ALL endpoints**:
- SignalR Hub
- API Controllers
- All routes under `/api/*`
3. Test with browser console (see CORS_ISSUE_SOLUTION.md)
**Complete instructions:** See [CORS_ISSUE_SOLUTION.md](./CORS_ISSUE_SOLUTION.md)
### 🧪 How to Test Configuration API
**In browser console:**
```javascript
fetch('https://ms.hmg.com/nscapi/api/PatientCall/Common_Config_GetByIP', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ipAddress: '10.71.50.53',
apiKey: 'EE17D21C7943485D9780223CCE55DCE5'
})
})
.then(r => {
console.log('✅ Config API CORS working!');
return r.json();
})
.then(data => console.log('Response:', data))
.catch(e => console.error('❌ Config API CORS blocked:', e));
```
**Expected results:**
- ❌ **Before CORS config:** "Failed to fetch" error
- ✅ **After CORS config:** JSON response with configuration data
### 📊 Summary
| Component | Status | Notes |
|-----------|--------|-------|
| API Call Exists | ✅ Yes | In `screen_details_repo.dart` |
| Called on Startup | ✅ Yes | Via `getGlobalConfigurationsByIP()` |
| Works on Mobile | ✅ Yes | Native app, no CORS |
| Works on Web | ❌ No | Blocked by CORS |
| Error Handling | ✅ Fixed | Uncommented try-catch, added CORS detection |
| Solution | ⚠️ Pending | Backend needs CORS configuration |
### 🎯 Next Steps
1. **Share [CORS_ISSUE_SOLUTION.md](./CORS_ISSUE_SOLUTION.md) with backend team**
2. **Backend adds CORS headers to ALL API endpoints**
3. **Test using browser console commands above**
4. **Once working, deploy web app**
---
**Date:** August 2, 2026
**Status:**
- ✅ API call found and documented
- ✅ Error handling improved
- ⚠️ CORS configuration required for web deployment

@ -1,269 +0,0 @@
# CORS Issue - Web SignalR Connection & API Calls
## Problem
```
ClientException: Failed to fetch, uri=https://ms.hmg.com/nscapi/PatientCallingHub/negotiate?IPAddress=10.71.50.53
ClientException: Failed to fetch, uri=https://ms.hmg.com/nscapi/api/PatientCall/Common_Config_GetByIP
```
### What is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security feature that blocks web apps from making requests to different domains unless the server explicitly allows it.
- ✅ **Mobile app**: Works fine (no CORS restrictions)
- ❌ **Web app**: Blocked by browser (CORS policy)
## Why This Happens
The web app is trying to connect to:
```
https://ms.hmg.com/nscapi/PatientCallingHub (SignalR Hub)
https://ms.hmg.com/nscapi/api/PatientCall/* (REST APIs)
https://ms.hmg.com/nscapi/api/Gen/* (Generic APIs)
https://ms.hmg.com/nscapi/api/Common/* (Common APIs)
```
But the browser blocks it because:
1. The web app is running on a different origin (e.g., `http://localhost:port` or `http://device-ip:port`)
2. The server (`ms.hmg.com`) doesn't send CORS headers allowing the web app's origin
**⚠️ IMPORTANT:** CORS must be configured for **ALL API endpoints**, not just SignalR!
## Solutions
### **Solution 1: Configure CORS on Server (RECOMMENDED)**
The backend server needs to add CORS headers to allow web requests. The server admin needs to configure:
#### For ASP.NET Core (SignalR Hub + REST APIs):
**In `Startup.cs` or `Program.cs`:**
```csharp
public void ConfigureServices(IServiceCollection services)
{
// Add CORS policy - MUST be before other services
services.AddCors(options =>
{
options.AddPolicy("AllowQLineWeb", builder =>
{
builder
.WithOrigins(
"http://localhost:*", // Development
"http://10.71.50.53:*", // Device IP
"https://qline.hmg.com", // Production
"https://your-web-domain.com" // Your web domain
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials() // Required for SignalR
.SetIsOriginAllowedToAllowWildcardSubdomains();
});
});
// Add SignalR
services.AddSignalR();
// Add MVC/API Controllers
services.AddControllers();
}
public void Configure(IApplicationBuilder app)
{
// CRITICAL: Enable CORS BEFORE routing and endpoints
app.UseCors("AllowQLineWeb");
// Other middleware...
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
// Apply CORS to SignalR hub
endpoints.MapHub<PatientCallingHub>("/PatientCallingHub")
.RequireCors("AllowQLineWeb");
// Apply CORS to API controllers (applies to ALL API routes)
endpoints.MapControllers()
.RequireCors("AllowQLineWeb");
});
}
```
**Alternative for Controllers (if endpoints not working):**
Add `[EnableCors("AllowQLineWeb")]` to your controllers:
```csharp
[ApiController]
[Route("api/[controller]")]
[EnableCors("AllowQLineWeb")] // Add this to EACH controller
public class PatientCallController : ControllerBase
{
// Your actions here...
}
```
#### For IIS:
Add to `web.config`:
```xml
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
<add name="Access-Control-Allow-Headers" value="Content-Type, Authorization" />
<add name="Access-Control-Allow-Credentials" value="true" />
</customHeaders>
</httpProtocol>
</system.webServer>
```
---
### **Solution 2: Run Web App from Same Domain**
Deploy the web app to the same domain as the API:
- API: `https://ms.hmg.com/nscapi`
- Web App: `https://ms.hmg.com/qline-web` ✅ (Same origin, no CORS issue)
---
### **Solution 3: Use Reverse Proxy (Development)**
For local development, use a reverse proxy to avoid CORS:
#### Using Flutter web with proxy:
1. Create `proxy_server.js`:
```javascript
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use('/nscapi', createProxyMiddleware({
target: 'https://ms.hmg.com',
changeOrigin: true,
ws: true, // Proxy websockets for SignalR
}));
app.listen(3000, () => {
console.log('Proxy running on http://localhost:3000');
});
```
2. Update `ApiConstants` to use proxy:
```dart
static String baseUrl = kIsWeb ? 'http://localhost:3000/nscapi' : 'https://ms.hmg.com/nscapi';
```
---
### **Solution 4: Browser Extension (Development Only)**
For testing, use a CORS browser extension:
- Chrome: "CORS Unblock" or "Allow CORS"
- Firefox: "CORS Everywhere"
⚠️ **Warning**: Only for development! Don't use in production.
---
## Quick Test
To verify CORS is the issue, open browser console and run:
### Test SignalR Hub:
```javascript
fetch('https://ms.hmg.com/nscapi/PatientCallingHub/negotiate?IPAddress=10.71.50.53')
.then(r => console.log('✅ SignalR CORS working!', r))
.catch(e => console.error('❌ SignalR CORS blocked:', e));
```
### Test Configuration API:
```javascript
fetch('https://ms.hmg.com/nscapi/api/PatientCall/Common_Config_GetByIP', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ipAddress: '10.71.50.53', apiKey: 'EE17D21C7943485D9780223CCE55DCE5' })
})
.then(r => console.log('✅ Config API CORS working!', r))
.catch(e => console.error('❌ Config API CORS blocked:', e));
```
### Test Weather API:
```javascript
fetch('https://ms.hmg.com/nscapi/api/PatientCall/WeatherForecast_GetBy5Days', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cityID: '297030' })
})
.then(r => console.log('✅ Weather API CORS working!', r))
.catch(e => console.error('❌ Weather API CORS blocked:', e));
```
If you see "CORS blocked" errors, the server needs CORS configuration.
---
## Current Status
- ✅ IP persistence: FIXED (localStorage working)
- ✅ IP retrieval on reload: FIXED
- ❌ SignalR connection on web: **BLOCKED BY CORS**
- ❌ REST API calls on web: **BLOCKED BY CORS**
- ❌ Configuration API on web: **BLOCKED BY CORS**
- ✅ All functionality on mobile: Working fine
**IMPORTANT:** The web app requires CORS for:
1. SignalR Hub (`/PatientCallingHub`)
2. Configuration API (`/api/PatientCall/Common_Config_GetByIP`)
3. Weather API (`/api/PatientCall/WeatherForecast_GetBy5Days`)
4. Prayer API (`/api/PatientCall/PrayerTime_Today`)
5. RSS Feed API (`/api/PatientCall/RssFeed_Get`)
6. Ticket APIs (`/api/Common/*`, `/api/Gen/*`)
**All APIs must have CORS enabled for web deployment.**
---
## Next Steps
1. **Contact backend/server admin** to add CORS headers for web app
2. **Provide them the allowed origins**:
- Development: `http://localhost:*`
- Production: Your web app's domain
- Device IPs: `http://10.71.50.53:*` (if needed)
3. **Test after server configuration** is updated
---
## Technical Details
**Why Mobile Works:**
- Native apps use platform HTTP clients (URLSession, HttpURLConnection)
- No browser, no CORS enforcement
**Why Web Fails:**
- Web browsers enforce Same-Origin Policy
- Requires server to send `Access-Control-Allow-Origin` header
- SignalR needs `Access-Control-Allow-Credentials: true`
**What the browser is blocking:**
```
Request: http://localhost:xxxxx → https://ms.hmg.com/nscapi
Response missing: Access-Control-Allow-Origin: http://localhost:xxxxx
Result: ❌ BLOCKED
```
---
**Date:** August 2, 2026
**Status:** ⚠️ SERVER CONFIGURATION REQUIRED

@ -1,289 +0,0 @@
# Final Status Summary - Web Issues Resolution
## Issues Addressed
### ✅ Issue 1: SignalR Disconnection on Web (FIXED)
**Problem:** SignalR would disconnect after some time on web
**Status:** **FIXED**
- Added platform-specific HTTP client configuration (web uses default, mobile uses IOClient)
- Improved reconnection logic with localStorage IP retrieval
- Better error handling for connection failures
- Web lifecycle management (visibility changes)
- Health check system with automatic recovery
### ✅ Issue 2: IP Reset on Page Reload (FIXED)
**Problem:** IP showed as empty "" after browser reload, even though it was in localStorage
**Status:** **FIXED**
- Added immediate localStorage check on app startup (before detection loop)
- Try-catch protection for all localStorage operations
- Comprehensive logging to debug localStorage access
- Multiple redundant checks in different initialization paths
- Verification logging for save/load operations
### ✅ Issue 3: Hub Status Showing "Connected" When Failed (FIXED)
**Problem:** UI showed "Hub Status: Connected" even when SignalR connection failed
**Status:** **FIXED**
- Changed initial `isHubConnected` state from `true` to `false`
- Updated status based on actual connection result (`true` or `false`)
- Added explicit status reset at initialization
- Added logging for connection success/failure
- See **[HUB_STATUS_BUG_FIX.md](./HUB_STATUS_BUG_FIX.md)** for details
### ⚠️ Issue 4: CORS Error Blocking Web Connection (REQUIRES SERVER FIX)
**Problem:** `ClientException: Failed to fetch` when connecting to SignalR hub AND calling REST APIs
**Status:** **DOCUMENTED - REQUIRES BACKEND CONFIGURATION**
**What's happening:**
- The web browser blocks ALL requests to `https://ms.hmg.com` due to CORS policy
- This includes:
- SignalR Hub connection (`/PatientCallingHub`)
- Configuration API (`/api/PatientCall/Common_Config_GetByIP`)
- Weather API, Prayer API, RSS Feed API, etc.
- Mobile works fine (no CORS restrictions on native apps)
- This is a **server-side configuration issue**
**What's needed:**
- Backend server needs to add CORS headers to **ALL API endpoints**
- See **[CORS_ISSUE_SOLUTION.md](./CORS_ISSUE_SOLUTION.md)** for complete fix instructions
- See **[WEB_DEPLOYMENT_NOTES.md](./WEB_DEPLOYMENT_NOTES.md)** for deployment checklist
---
## What Works Now
### ✅ Fully Working (Mobile & Web)
- IP detection (URL parameter, localStorage, auto-detect)
- IP persistence across app restarts
- IP configuration dialog
- All UI components
- View models and business logic
- Logging system
- Error handling
- Offline detection
### ✅ Working (Mobile Only)
- SignalR real-time connection ← **CORS blocks this on web**
- Configuration API calls ← **CORS blocks this on web**
- Weather, Prayer, RSS APIs ← **CORS blocks this on web**
- Ticket notifications
- Config updates
- All queue management features
### ⚠️ Blocked (Web Only)
- SignalR connection ← **Browser CORS policy blocks this**
- All REST API calls ← **Browser CORS policy blocks this**
- Configuration loading ← **Browser CORS policy blocks this**
- Weather/Prayer/RSS data ← **Browser CORS policy blocks this**
---
## Code Changes Made
### 1. signalR_repo.dart
```dart
✅ Platform-specific HTTP client (kIsWeb ? null : IOClient(...))
✅ localStorage IP retrieval during connection
✅ Connection rebuilding on disconnect
✅ CORS error detection and logging
✅ Improved reconnection logic
```
### 2. queuing_view_model.dart
```dart
✅ Status update based on connection result
✅ Initial status reset in initializeQueueingVM()
✅ Status set to false on connection failure
✅ Logging for connection success/failure
✅ Error handling updates status correctly
```
### 3. screen_config_view_model.dart
```dart
✅ Immediate localStorage check on startup
✅ IP saving to localStorage whenever retrieved
✅ Web lifecycle handling (onAppResumed, onAppPaused)
✅ Proactive reconnection on web visibility change
✅ Better health check with web-specific recovery
✅ Try-catch for localStorage operations
```
### 3. connectivity_service.dart
```dart
✅ Comprehensive logging for localStorage access
✅ Try-catch for localStorage operations
✅ Better debugging info (type, value, conditions)
```
### 4. splash_screen.dart
```dart
✅ Verification logging for IP save
✅ Try-catch for localStorage operations
✅ Mismatch warnings
```
### 5. ip_config_dialog.dart
```dart
✅ Try-catch for localStorage save
✅ Verification and error logging
```
### 6. screen_details_repo.dart
```dart
✅ Uncommented try-catch for getGlobalScreenConfigurations
✅ Added CORS error detection for configuration API
✅ Better error logging for API failures
```
---
## Documentation Created
1. **[CORS_ISSUE_SOLUTION.md](./CORS_ISSUE_SOLUTION.md)**
- Complete CORS configuration guide
- Server-side code examples (ASP.NET Core, IIS)
- Multiple solution approaches
- Testing methods
2. **[WEB_DEPLOYMENT_NOTES.md](./WEB_DEPLOYMENT_NOTES.md)**
- Quick reference for web deployment
- Pre-deployment checklist
- CORS testing procedure
- Troubleshooting guide
3. **[IP_PERSISTENCE_FIX.md](./IP_PERSISTENCE_FIX.md)**
- Technical details of IP persistence fixes
- Flow diagrams
- Testing checklist
4. **[README.md](./README.md)** (Updated)
- Added web deployment section
- Links to all CORS documentation
---
## Next Steps
### For Backend/Server Team:
1. **Read [CORS_ISSUE_SOLUTION.md](./CORS_ISSUE_SOLUTION.md)**
2. **Add CORS configuration to SignalR hub**
- Allow origins from web app domain
- Enable credentials for SignalR
- Test with browser console
3. **Verify configuration works** (see testing section in docs)
### For Web Deployment:
1. **Don't deploy to production yet** - CORS must be fixed first
2. **Test CORS** using browser console method (in WEB_DEPLOYMENT_NOTES.md)
3. **Once CORS is configured:**
- Deploy web app
- Test SignalR connection
- Verify "Hub Status: Connected" in footer
- Test ticket reception
### For Development/Testing:
1. **Use mobile app** - works perfectly without CORS issues
2. **For web testing** - use CORS browser extension temporarily (dev only)
3. **Or use reverse proxy** - see CORS_ISSUE_SOLUTION.md
---
## Testing Checklist
### ✅ IP Persistence (Ready to Test)
- [ ] Configure IP via dialog
- [ ] Reload page (F5)
- [ ] IP should load instantly from localStorage
- [ ] Check console: "✅ IP retrieved from localStorage on startup"
- [ ] No IP config dialog should appear
### ✅ SignalR Reconnection on Web (Ready to Test - After CORS Fixed)
- [ ] Connect to app
- [ ] Switch browser tabs (make app hidden)
- [ ] Switch back to app tab
- [ ] Connection should auto-reconnect
- [ ] Check footer: "Hub Status: Connected"
### ⚠️ CORS Fix (Requires Server Team)
- [ ] Backend adds CORS headers
- [ ] Test with browser console (see WEB_DEPLOYMENT_NOTES.md)
- [ ] Web app connects to SignalR successfully
- [ ] No "Failed to fetch" errors
- [ ] Hub status shows "Connected"
---
## Current Logs Explained
### What You're Seeing:
```
✅ IP configured in viewModel: 10.71.50.53 ← IP working!
✅ IP configured: 10.71.50.53 - Reloading... ← Reload working!
[log] Starting HubConnection. ← SignalR starting...
❌ ClientException: Failed to fetch ← Browser CORS blocking SignalR
❌ ClientException: Failed to fetch ← Browser CORS blocking Config API
```
**This means:**
1. ✅ IP is working correctly
2. ✅ IP is saved and loaded properly
3. ✅ SignalR connection is starting
4. ❌ Browser blocks SignalR connection (CORS)
5. ❌ Browser blocks REST API calls (CORS)
**This is expected** until CORS is configured on the server for **all endpoints**.
---
## Success Criteria
### Phase 1: ✅ COMPLETE
- [x] Fix IP persistence
- [x] Fix SignalR reconnection logic
- [x] Add comprehensive logging
- [x] Document CORS issue
- [x] Create deployment guides
### Phase 2: ⚠️ BLOCKED (Waiting on Server Team)
- [ ] Backend adds CORS configuration
- [ ] Test CORS from browser
- [ ] Verify SignalR connects on web
- [ ] Full end-to-end testing
### Phase 3: 🔜 NEXT (After CORS Fixed)
- [ ] Deploy web app to staging
- [ ] Production deployment
- [ ] User acceptance testing
- [ ] Monitor logs for issues
---
## Summary
### What You Can Do Now:
✅ Use mobile app (fully functional)
✅ Test IP persistence on web
✅ Test UI and navigation on web
### What You Need Before Web Production:
⚠️ Backend team to configure CORS (see CORS_ISSUE_SOLUTION.md)
⚠️ Test CORS with browser console
⚠️ Verify SignalR connection works
### Files to Share with Backend Team:
📋 [CORS_ISSUE_SOLUTION.md](./CORS_ISSUE_SOLUTION.md) - **Server configuration guide**
📋 [WEB_DEPLOYMENT_NOTES.md](./WEB_DEPLOYMENT_NOTES.md) - **Quick reference**
---
**Date:** August 2, 2026
**Status:**
- ✅ Client-side fixes: **COMPLETE**
- ⚠️ Server-side CORS: **PENDING**
- 🎯 Ready for web deployment: **Once CORS is configured**

@ -1,247 +0,0 @@
# Bug Fix - Hub Status Showing "Connected" When Connection Fails
## 🐛 Bug Reported
**Issue:** "If it's unable to connect to SignalR, why does it say hub status connected?"
**What was happening:**
Even when SignalR failed to connect (due to CORS or any other error), the UI footer was showing:
```
Hub Status: Connected ✅
```
This was misleading and made debugging difficult.
---
## 🔍 Root Cause Analysis
### Problem 1: Initial State Was `true`
**File:** `/lib/view_models/screen_config_view_model.dart` (Line 207)
```dart
bool isHubConnected = true; // ❌ Wrong! Started as true by default
```
The hub status was initialized to `true` even before attempting to connect.
### Problem 2: Connection Result Not Used
**File:** `/lib/view_models/queuing_view_model.dart` (Line 45-60)
```dart
Future<bool?> startHubConnection() async {
return await signalrRepo.startHubConnection(...); // ❌ Returned but not used to update status
}
```
When `startHubConnection()` returned `false` (connection failed), the status wasn't being updated.
### Problem 3: No Initial Status Reset
**File:** `/lib/view_models/queuing_view_model.dart` (Line 35-43)
```dart
Future<void> initializeQueueingVM() async {
await startHubConnection(); // ❌ No initial status set
// ...
}
```
The initialization didn't explicitly set the status to disconnected before attempting to connect.
---
## ✅ Fixes Applied
### Fix 1: Changed Initial State to `false`
**File:** `/lib/view_models/screen_config_view_model.dart`
```dart
bool isHubConnected = false; // ✅ Start as disconnected until connection succeeds
```
**Why:** The app should assume disconnected until proven connected.
### Fix 2: Update Status Based on Connection Result
**File:** `/lib/view_models/queuing_view_model.dart`
```dart
Future<bool?> startHubConnection() async {
try {
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
final result = await signalrRepo.startHubConnection(...);
// ✅ Update hub connection status based on result
if (result == true) {
screenConfigViewModel.updateIsHubConnected(true);
loggerService.logToFile(message: "Hub connection successful", ...);
} else {
screenConfigViewModel.updateIsHubConnected(false);
loggerService.logToFile(message: "Hub connection failed - result: $result", ...);
}
return result;
} catch (e, st) {
// ✅ On error, mark as disconnected
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
screenConfigViewModel.updateIsHubConnected(false);
// ...
}
}
```
**Why:** The connection status should accurately reflect the result of the connection attempt.
### Fix 3: Set Initial Status in Initialization
**File:** `/lib/view_models/queuing_view_model.dart`
```dart
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();
// ...
}
}
```
**Why:** Clear initialization prevents race conditions and ensures consistent state.
---
## 🎯 Expected Behavior After Fix
### Scenario 1: Successful Connection (Mobile)
```
1. App starts
→ Hub Status: Disconnected ❌
2. Attempting SignalR connection...
→ Hub Status: Disconnected ❌
3. Connection successful!
→ Hub Status: Connected ✅
```
### Scenario 2: Failed Connection (Web - CORS)
```
1. App starts
→ Hub Status: Disconnected ❌
2. Attempting SignalR connection...
→ Hub Status: Disconnected ❌
3. Connection fails (CORS error)
→ Hub Status: Disconnected ❌ ← Stays disconnected!
```
### Scenario 3: Connection Lost
```
1. Connected and running
→ Hub Status: Connected ✅
2. Network issue / Server restart
→ onHubDisconnected() called
→ Hub Status: Disconnected ❌
3. Auto-reconnect attempts...
→ Hub Status: Disconnected ❌
4. Reconnection successful
→ onHubReconnected() called
→ Hub Status: Connected ✅
```
---
## 🧪 How to Test
### Test 1: Web (CORS Blocked)
1. Open web app
2. Check footer: Should show "Hub Status: Disconnected" ❌
3. Check logs: Should show "Hub connection failed"
### Test 2: Mobile (Working)
1. Open mobile app
2. Wait for connection
3. Check footer: Should show "Hub Status: Connected" ✅
4. Check logs: Should show "Hub connection successful"
### Test 3: Connection Loss
1. Start mobile app (connected)
2. Turn off network
3. Check footer: Should change to "Disconnected" ❌
4. Turn on network
5. Check footer: Should change back to "Connected" ✅
---
## 📊 Status Flow Diagram
### Before Fix:
```
App Start → isHubConnected = true ✅ (WRONG!)
→ Attempt connection
→ Connection fails
→ isHubConnected = true ✅ (Still shows connected!)
```
### After Fix:
```
App Start → isHubConnected = false ❌
→ Attempt connection
→ Connection fails
→ Update status: false ❌ (Correctly shows disconnected!)
```
```
App Start → isHubConnected = false ❌
→ Attempt connection
→ Connection succeeds
→ Update status: true ✅ (Correctly shows connected!)
```
---
## 📝 Code Changes Summary
| File | Lines Changed | Description |
|------|---------------|-------------|
| `screen_config_view_model.dart` | 207 | Changed `isHubConnected` initial value from `true` to `false` |
| `queuing_view_model.dart` | 35-43 | Added initial status reset in `initializeQueueingVM()` |
| `queuing_view_model.dart` | 45-82 | Updated `startHubConnection()` to set status based on result |
---
## 🎯 Impact
### User Experience
- ✅ **More accurate status display**: Users see the real connection state
- ✅ **Better troubleshooting**: Clear indication when connection fails
- ✅ **No false positives**: Won't show "Connected" when it's actually disconnected
### Developer Experience
- ✅ **Easier debugging**: Logs now show accurate connection status
- ✅ **Clear state management**: Status updates are explicit and traceable
- ✅ **Consistent behavior**: All connection paths update status correctly
### Web Platform
- ✅ **CORS errors visible**: Footer now correctly shows "Disconnected" when CORS blocks connection
- ✅ **Clear user feedback**: Users can see that connection failed
- ✅ **Better error reporting**: Status matches logs and actual state
---
## ✅ Verification Checklist
- [x] Initial state is `false` (disconnected)
- [x] Status updates to `true` on successful connection
- [x] Status updates to `false` on failed connection
- [x] Status updates to `false` on connection error
- [x] Logging added for status changes
- [x] No compiler errors
- [x] Existing functionality preserved
---
**Date:** August 2, 2026
**Status:** ✅ FIXED
**Files Modified:** 2
**Impact:** High (affects all platforms)

@ -1,111 +0,0 @@
# IP Persistence Fix - Page Reload Issue
## Problem
When the web page reloads (browser refresh), the device IP was not being retrieved from localStorage, showing as empty string `""` instead.
## Root Causes Identified
1. **No immediate localStorage check on startup** - The app was entering detection loop before checking if IP was already saved
2. **Potential localStorage access errors** - No error handling for localStorage operations
3. **Race conditions** - localStorage might not be checked before other detection methods
## Fixes Applied
### 1. **screen_config_view_model.dart**
- ✅ Added **immediate localStorage check** at the start of `waitForIPAndInitializeConfigVM()`
- Checks localStorage BEFORE entering the detection loop
- If IP found, initializes immediately and returns
- Logs when IP is found vs not found
- ✅ Added **try-catch** around all localStorage operations
- ✅ Better error logging for localStorage access failures
### 2. **connectivity_service.dart**
- ✅ Added **comprehensive logging** in `getCurrentScreenIP()`
- Logs storedIp value, type, and all conditions checked
- Shows exactly why localStorage check passed or failed
- ✅ Added **try-catch** around localStorage read/write operations
- ✅ Better error messages when localStorage access fails
### 3. **splash_screen.dart**
- ✅ Enhanced **logging in `_reloadWithIp()`**
- Logs before and after save
- Verifies saved value matches input
- Shows localStorage keys
- Warns if verification fails
- ✅ Added **try-catch** around localStorage operations
### 4. **ip_config_dialog.dart**
- ✅ Added **try-catch** in `_submitIp()`
- ✅ Enhanced **verification logging**
- Confirms save operation
- Warns if verification fails
## Flow After Fix
### On Page Reload (Browser Refresh):
1. **App starts fresh** (all Dart state cleared, localStorage persists)
2. **SplashScreen loads** → `_loadData()` called
3. **waitForIPAndInitializeConfigVM() called**
4. **🆕 IMMEDIATE localStorage check** (NEW!)
```dart
if (kIsWeb && currentScreenIP == "") {
final storedIp = html.window.localStorage['device_ip'];
if (storedIp != null && storedIp.isNotEmpty && storedIp != 'null') {
currentScreenIP = storedIp;
initializeScreenConfigVM();
return; // ✅ IP found, initialize immediately!
}
}
```
5. If IP found → Initialize immediately ✅
6. If not found → Enter detection loop (URL param → localStorage → auto-detect)
### On IP Configuration:
1. User enters IP in dialog
2. **Saved to localStorage** with verification
3. **Set in viewModel**
4. Re-initialize with saved IP
## Benefits
- ✅ **Instant load** on page reload (no detection delay if IP already saved)
- ✅ **Better error handling** (won't fail silently)
- ✅ **Comprehensive logging** (easy to debug if issues persist)
- ✅ **Multiple safeguards** (checks localStorage in multiple places)
## Testing Checklist
1. ✅ Configure IP via dialog → Reload page → Should load instantly with saved IP
2. ✅ Configure IP via URL `?ip=X.X.X.X` → Reload without URL param → Should load with saved IP
3. ✅ Clear localStorage → Reload → Should show IP config dialog
4. ✅ Check browser console for detailed logs showing localStorage access
## Console Logs to Look For
### Success (IP Found):
```
✅ IP retrieved from localStorage on startup: 10.71.50.53
💾 Saved IP to localStorage: 10.71.50.53
```
### Failure (IP Not Found):
```
⚠️ No IP found in localStorage on startup (storedIp: "null")
🔍 Checking localStorage for saved IP...
⚠️ localStorage check failed - storedIp: "null"
```
### Verification:
```
💾 Saved IP to localStorage in splash: 10.71.50.53
Verify: saved == ip: true
localStorage keys: [device_ip, ...]
```
## Files Modified
1. `/lib/view_models/screen_config_view_model.dart`
2. `/lib/services/connectivity_service.dart`
3. `/lib/views/splash_screen/splash_screen.dart`
4. `/lib/views/common_widgets/ip_config_dialog.dart`
---
**Date:** August 2, 2026
**Status:** ✅ FIXED - Ready for testing

File diff suppressed because it is too large Load Diff

@ -3,26 +3,6 @@
HMG_QLine is a Flutter-based application designed for efficient patient management and queue handling in healthcare environments. It features multiple screens for patient display, robust logging, and
background service integration to ensure reliability and uptime.
## 📚 Documentation
**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
@ -98,124 +78,4 @@ Pull requests are welcome. For major changes, please open an issue first to disc
## License
This project is licensed under the MIT License.
# Queuing System Shell Scripts
This repository contains a set of shell scripts for managing Android devices running the HMG QLine application in a multi-device hospital environment. The scripts automate deployment, logging, restarting, and uninstallation tasks for efficient device management.
## Features
- **Deploy APKs:** Install and launch specific versions of the QLine app on multiple devices.
- **Restart App:** Force-stop and relaunch the QLine app on all connected devices.
- **Uninstall App:** Remove the QLine app from all connected devices.
- **Extract Logs:** Pull log files from devices for troubleshooting and analysis.
- **Device Connection:** Automatically connect to devices over Wi-Fi using IP addresses.
## Folder Structure
```
queuing_system/
├── apks/ # APK files for different QLine app versions
├── logs/ # Data logs from devices
├── connect_devices.sh # Script to connect to devices
├── deploy_qline.sh # Script to deploy APKs
├── device_ips.txt # List of device IP addresses
├── extract_logs.sh # Script to extract logs from devices
├── restart_qline.sh # Script to restart the app on devices
├── uninstall_qline.sh # Script to uninstall the app
```
## Getting Started
1. **Clone the repository:**
```bash
git clone <repo-url>
cd queuing_system
```
2. **Configure device IPs:**
- Add device IPs to a file (e.g., `device_ips_all.txt`), one per line.
3. **Prepare APKs:**
- Place APK files in the `apks/` folder, named as per the versioning scheme.
4. **Run scripts:**
- Make scripts executable: `chmod +x *.sh`
- Example: Deploy APKs
```bash
./deploy_qline.sh
```
- Example: Extract logs from a device
```bash
sh extract_logs.sh -ip <device_ip:port>
```
## Script Commands (One by One)
Use the commands below with the appropriate IP list file.
### Connect Devices
```bash
sh connect_devices.sh -ipfile device_ips_all.txt
```
### Deploy QLine APK
```bash
sh deploy_qline.sh -ipfile device_ips_all.txt -version 9.2
```
**Deploy to Single IP:**
```bash
echo "10.70.6.143" > temp_ip.txt
sh deploy_qline.sh -ipfile temp_ip.txt -version 9.2
rm temp_ip.txt
```
### Restart QLine App
```bash
sh restart_qline.sh -ipfile device_ips_all.txt
```
### Uninstall QLine App
```bash
sh uninstall_qline.sh -ipfile device_ips_all.txt
```
### Extract Logs (Single Device)
```bash
sh extract_logs.sh -ip <device_ip:port>
```
### Extract Logs (All Devices)
```bash
sh extract_all_logs.sh -ipfile device_ips_all.txt
```
### Clear Logs (Single Device)
```bash
sh clear_logs.sh -ip <device_ip:port>
```
### Clear Logs (All Devices)
```bash
sh clear_logs_all.sh -ipfile device_ips_all.txt
```
### Kill QLine App (All Devices)
```bash
sh kill_qline.sh -ipfile device_ips_all.txt
```
## Requirements
- macOS (tested)
- Android SDK Platform Tools (adb)
- Wi-Fi connection to devices
## Notes
- Ensure all devices are on the same Wi-Fi network (`Cloud Solutions`).
- APKs and logs are ignored from version control via `.gitignore`.
- Scripts are designed for hospital multi-floor deployments but can be adapted for other environments.
## License
MIT License
This project is licensed under the MIT License.

@ -1,155 +0,0 @@
# Web Deployment - Important Notes
## ⚠️ CORS Configuration Required for Web
### Current Status
- ✅ **Mobile App**: Works perfectly
- ⚠️ **Web App**: Requires server CORS configuration
### The Issue
The web browser blocks connections to `https://ms.hmg.com/nscapi` due to CORS (Cross-Origin Resource Sharing) policy.
**Error you'll see:**
```
ClientException: Failed to fetch, uri=https://ms.hmg.com/nscapi/PatientCallingHub/negotiate
```
### What This Means
- The web app **cannot connect to SignalR hub** without CORS headers
- The API works fine (mobile proves this)
- This is a **server configuration issue**, not a code issue
---
## 🔧 Quick Fix Guide
### For Backend/Server Team:
The SignalR hub needs to allow cross-origin requests from the web app.
**Add this to your ASP.NET Core SignalR server:**
```csharp
// In Startup.cs or Program.cs
services.AddCors(options =>
{
options.AddPolicy("AllowQLineWeb", builder =>
{
builder.WithOrigins(
"http://localhost:8080", // Flutter dev
"http://10.71.50.53:8080", // Device IP
"https://your-domain.com" // Production domain
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // Required for SignalR
});
});
// Enable CORS
app.UseCors("AllowQLineWeb");
```
---
## 📖 Detailed Solutions
See **[CORS_ISSUE_SOLUTION.md](./CORS_ISSUE_SOLUTION.md)** for:
- Complete server configuration examples
- Alternative solutions (reverse proxy, same-origin deployment)
- Testing methods
- Troubleshooting guide
---
## 🚀 Deployment Options
### Option 1: Fix CORS (Recommended)
Configure the backend server to allow web requests. See CORS_ISSUE_SOLUTION.md.
### Option 2: Same-Origin Deployment
Deploy web app on the same domain as the API:
- API: `https://ms.hmg.com/nscapi`
- Web: `https://ms.hmg.com/qline` ← No CORS issues!
### Option 3: Use Reverse Proxy
Set up a reverse proxy (nginx, Apache) to route both API and web through same origin.
---
## 🧪 Testing
### Before deploying web app, test CORS:
**1. Open browser console on your web app**
**2. Run this test:**
```javascript
fetch('https://ms.hmg.com/nscapi/PatientCallingHub/negotiate?IPAddress=10.71.50.53')
.then(r => console.log('✅ CORS working!', r))
.catch(e => console.error('❌ CORS blocked:', e));
```
**3. If you see "CORS blocked":**
- Server needs CORS configuration
- Contact backend team with CORS_ISSUE_SOLUTION.md
**4. If you see "CORS working":**
- You're ready to deploy! 🎉
---
## 📝 Checklist Before Web Deployment
- [ ] Backend server has CORS configured for your web domain
- [ ] CORS allows credentials (`Access-Control-Allow-Credentials: true`)
- [ ] CORS allows required headers
- [ ] SignalR hub endpoint has CORS applied
- [ ] Test CORS from browser console (see above)
- [ ] Verify SignalR connection works in browser dev tools
---
## 🐛 Troubleshooting
### "Failed to fetch" error
**Cause:** CORS not configured
**Fix:** Configure CORS on server (see CORS_ISSUE_SOLUTION.md)
### "Hub Status: Disconnected" in footer
**Cause:** Cannot connect to SignalR hub
**Fix:** Check browser console for errors, likely CORS
### Mobile works but web doesn't
**Cause:** CORS only affects browsers
**Fix:** This confirms it's CORS - configure server
---
## 📞 Support
**For Server/Backend Team:**
- See: CORS_ISSUE_SOLUTION.md
- Section: "Solution 1: Configure CORS on Server"
**For Frontend/Web Deployment:**
- Verify CORS is configured before deploying
- Test using the browser console method above
---
## ✅ What's Already Working
- ✅ IP detection and persistence (localStorage)
- ✅ IP configuration dialog
- ✅ IP retrieval on page reload
- ✅ SignalR connection logic (works on mobile)
- ✅ All UI components
- ✅ Offline detection and reconnection logic
**Only missing:** Server CORS configuration for web browsers
---
**Last Updated:** August 2, 2026

@ -45,5 +45,4 @@ flutter {
dependencies {
implementation 'androidx.lifecycle:lifecycle-service:2.8.7'
implementation 'androidx.core:core-ktx:1.13.1'
}

@ -5,12 +5,6 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 12+ (API 31+) exact alarm permissions -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- Android 14+ (API 34+) - USE_EXACT_ALARM is for apps that need exact alarms as core functionality -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<!-- Foreground service types for Android 14+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<queries>
<intent>
@ -30,30 +24,14 @@
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- Receiver for scheduled restart alarm -->
<receiver
android:name=".RestartAlarmReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.example.hmg_qline.SCHEDULED_RESTART" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<service
android:name=".BootForegroundService"
android:exported="true"
android:foregroundServiceType="specialUse">
<!-- Android 14+ requires property declaration for FOREGROUND_SERVICE_TYPE_SPECIAL_USE -->
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="App auto-start and scheduled restart" />
</service>
android:exported="true" />
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

@ -1,225 +0,0 @@
package com.example.hmg_qline.hmg_qline
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.util.Log
import java.util.Calendar
/**
* Utility class for scheduling app restart alarms.
* Handles Android version-specific alarm scheduling with proper backward compatibility.
*
* Android Version Compatibility:
* - Android 14+ (API 34): Uses USE_EXACT_ALARM or SCHEDULE_EXACT_ALARM with permission check
* - Android 12-13 (API 31-33): Uses SCHEDULE_EXACT_ALARM with permission check
* - Android 6-11 (API 23-30): Uses setExactAndAllowWhileIdle
* - Android < 6 (API < 23): Uses setExact
*/
object AlarmScheduler {
private const val TAG = "AlarmScheduler"
private const val RESTART_ALARM_REQUEST_CODE = 1001
/**
* Schedule a daily restart alarm at the specified time.
*
* @param context Application context
* @param hour Hour of day (0-23), default is 0 (midnight)
* @param minute Minute (0-59), default is 15
*/
fun scheduleRestartAlarm(context: Context, hour: Int = 0, minute: Int = 15) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, RestartAlarmReceiver::class.java).apply {
action = RestartAlarmReceiver.ACTION_SCHEDULED_RESTART
}
val pendingIntent = PendingIntent.getBroadcast(
context,
RESTART_ALARM_REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Calculate next alarm time
val calendar = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, minute)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
// If the time has already passed today, schedule for tomorrow
if (timeInMillis <= System.currentTimeMillis()) {
add(Calendar.DAY_OF_YEAR, 1)
}
}
// Cancel any existing alarm first
alarmManager.cancel(pendingIntent)
Log.d(TAG, "Scheduling restart alarm for: ${calendar.time}")
Log.d(TAG, "Android SDK Version: ${Build.VERSION.SDK_INT}")
// Schedule based on Android version
when {
// Android 14+ (API 34+)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> {
scheduleForAndroid14Plus(context, alarmManager, pendingIntent, calendar.timeInMillis)
}
// Android 12-13 (API 31-33)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
scheduleForAndroid12To13(context, alarmManager, pendingIntent, calendar.timeInMillis)
}
// Android 6-11 (API 23-30)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
Log.d(TAG, "Alarm scheduled using setExactAndAllowWhileIdle (API 23-30)")
}
// Android < 6 (API < 23)
else -> {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
Log.d(TAG, "Alarm scheduled using setExact (API < 23)")
}
}
}
/**
* Schedule alarm for Android 14+ (API 34+)
* Android 14 requires special handling for exact alarms.
*/
private fun scheduleForAndroid14Plus(
context: Context,
alarmManager: AlarmManager,
pendingIntent: PendingIntent,
triggerTime: Long
) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
Log.d(TAG, "Alarm scheduled using setExactAndAllowWhileIdle (API 34+)")
} else {
// Fallback to inexact alarm if permission not granted
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
Log.w(TAG, "Exact alarm permission not granted, using setAndAllowWhileIdle (API 34+)")
}
}
} catch (e: SecurityException) {
Log.e(TAG, "SecurityException scheduling alarm: ${e.message}")
// Fallback to inexact alarm
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
}
}
/**
* Schedule alarm for Android 12-13 (API 31-33)
*/
private fun scheduleForAndroid12To13(
context: Context,
alarmManager: AlarmManager,
pendingIntent: PendingIntent,
triggerTime: Long
) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
Log.d(TAG, "Alarm scheduled using setExactAndAllowWhileIdle (API 31-33)")
} else {
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
Log.w(TAG, "Exact alarm permission not granted, using setAndAllowWhileIdle (API 31-33)")
}
}
} catch (e: SecurityException) {
Log.e(TAG, "SecurityException scheduling alarm: ${e.message}")
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
}
}
/**
* Cancel the scheduled restart alarm.
*/
fun cancelRestartAlarm(context: Context) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, RestartAlarmReceiver::class.java).apply {
action = RestartAlarmReceiver.ACTION_SCHEDULED_RESTART
}
val pendingIntent = PendingIntent.getBroadcast(
context,
RESTART_ALARM_REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
Log.d(TAG, "Restart alarm cancelled")
}
/**
* Check if the app can schedule exact alarms.
* Returns true for Android < 12 (always allowed) or if permission is granted on Android 12+.
*/
fun canScheduleExactAlarms(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.canScheduleExactAlarms()
} else {
true // Always allowed on older versions
}
}
/**
* Open system settings to request exact alarm permission (Android 12+).
*/
fun requestExactAlarmPermission(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
} catch (e: Exception) {
Log.e(TAG, "Error opening exact alarm settings: ${e.message}")
}
}
}
}

@ -4,128 +4,46 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
/**
* Foreground service that launches the app after device boot or scheduled restart.
* Compatible with Android 14+ (API 34+) and older versions.
*
* Android 14+ requires:
* - Explicit foreground service type in manifest and code
* - FOREGROUND_SERVICE_SPECIAL_USE permission
*/
class BootForegroundService : LifecycleService() {
companion object {
private const val TAG = "BootForegroundService"
private const val CHANNEL_ID = "boot_service_channel"
private const val NOTIFICATION_ID = 1
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Service created - Android SDK: ${Build.VERSION.SDK_INT}")
startForegroundServiceCompat()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val source = intent?.getStringExtra("source") ?: "unknown"
Log.d(TAG, "Service started from source: $source")
// Launch the main activity
launchMainActivity(source)
// Stop the service after launching the app
stopSelf()
return START_NOT_STICKY
startForegroundService()
}
/**
* Start foreground service with Android version compatibility.
* Android 14+ requires explicit foreground service type.
*/
private fun startForegroundServiceCompat() {
private fun startForegroundService() {
val channelId = "boot_service_channel"
createNotificationChannel()
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
val notification: Notification = NotificationCompat.Builder(this, channelId)
.setContentTitle("QLine App")
.setContentText("Starting QLine...")
.setContentText("Monitoring QLine activity...")
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setAutoCancel(true)
.build()
// Use ServiceCompat for Android 14+ compatibility
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// Android 14+ (API 34+) requires explicit foreground service type
ServiceCompat.startForeground(
this,
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
)
Log.d(TAG, "Foreground service started with SPECIAL_USE type (API 34+)")
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10-13 (API 29-33)
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE)
Log.d(TAG, "Foreground service started with NONE type (API 29-33)")
} else {
// Android 8-9 (API 26-28)
startForeground(NOTIFICATION_ID, notification)
Log.d(TAG, "Foreground service started (API 26-28)")
}
}
/**
* Launch the main activity.
*/
private fun launchMainActivity(source: String) {
try {
Log.d(TAG, "Launching MainActivity from source: $source")
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra("launched_from_boot", source == "boot_completed")
putExtra("launched_from_scheduled_restart", source == "scheduled_restart")
}
startActivity(intent)
startForeground(1, notification)
Log.d(TAG, "MainActivity launched successfully")
} catch (e: Exception) {
Log.e(TAG, "Error launching MainActivity: ${e.message}")
// Only launch MainActivity if this service is started by the system (e.g. on boot)
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
startActivity(intent)
stopSelf() // Stop the service after initialization
}
/**
* Create notification channel for Android 8.0+ (API 26+).
*/
private fun createNotificationChannel() {
val channelId = "boot_service_channel"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
channelId,
"Boot Service Channel",
NotificationManager.IMPORTANCE_LOW // Use LOW to avoid sound/vibration
).apply {
description = "Used to start QLine app after device boot"
setShowBadge(false)
}
NotificationManager.IMPORTANCE_HIGH
)
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(channel)
Log.d(TAG, "Notification channel created")
}
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Service destroyed")
}
}

@ -6,58 +6,17 @@ import android.content.Intent
import android.os.Build
import android.util.Log
/**
* BroadcastReceiver that handles device boot events.
* Starts the app automatically after device boot and schedules daily restart alarm.
* Compatible with Android 14+ (API 34+) and older versions.
*/
class BootBroadcastReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "BootBroadcastReceiver"
}
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Received intent: ${intent.action}")
Log.d("BootReceiver", "Received intent: ${intent.action}")
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
intent.action == "android.intent.action.QUICKBOOT_POWERON" ||
intent.action == "com.htc.intent.action.QUICKBOOT_POWERON"
) {
Log.d(TAG, "Boot completed detected - Android SDK: ${Build.VERSION.SDK_INT}")
// Schedule the daily restart alarm first
scheduleRestartAlarm(context)
// Then start the foreground service to launch the app
startAppViaForegroundService(context)
}
}
/**
* Schedule daily restart alarm at 00:15.
* This ensures the alarm is set even if the app wasn't running before reboot.
*/
private fun scheduleRestartAlarm(context: Context) {
try {
Log.d(TAG, "Scheduling daily restart alarm after boot")
AlarmScheduler.scheduleRestartAlarm(context, 0, 15) // 00:15 (12:15 AM)
Log.d(TAG, "Daily restart alarm scheduled successfully")
} catch (e: Exception) {
Log.e(TAG, "Error scheduling restart alarm after boot: ${e.message}")
}
}
/**
* Start the app via foreground service.
* Uses different approach based on Android version for compatibility.
*/
private fun startAppViaForegroundService(context: Context) {
try {
Log.d(TAG, "Starting BootForegroundService")
val serviceIntent = Intent(context, BootForegroundService::class.java).apply {
putExtra("source", "boot_completed")
}
Log.d("BootReceiver", "Starting BootForegroundService.")
val serviceIntent = Intent(context, BootForegroundService::class.java)
// Use foreground service for Android 8.0+ (API 26+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -65,30 +24,6 @@ class BootBroadcastReceiver : BroadcastReceiver() {
} else {
context.startService(serviceIntent)
}
Log.d(TAG, "BootForegroundService started successfully")
} catch (e: Exception) {
Log.e(TAG, "Error starting foreground service: ${e.message}")
// Fallback: try direct activity launch
tryDirectActivityLaunch(context)
}
}
/**
* Fallback method to launch activity directly if service fails.
*/
private fun tryDirectActivityLaunch(context: Context) {
try {
Log.d(TAG, "Attempting direct activity launch as fallback")
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra("launched_from_boot", true)
}
context.startActivity(launchIntent)
Log.d(TAG, "Direct activity launch successful")
} catch (fallbackError: Exception) {
Log.e(TAG, "Direct activity launch also failed: ${fallbackError.message}")
}
}
}

@ -1,8 +1,5 @@
package com.example.hmg_qline.hmg_qline
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Handler
@ -17,169 +14,112 @@ import java.io.File
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.hmg_qline/foreground"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Keep screen on at all times (AC-powered kiosk display)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
// Disable sleep/screensaver for kiosk mode
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
// Log if app was restarted
if (intent.getBooleanExtra("restarted", false)) {
Log.d("MainActivity", "App restarted successfully")
}
// Log if launched from boot
if (intent.getBooleanExtra("launched_from_boot", false)) {
Log.d("MainActivity", "App launched from boot")
// Give system time to settle after boot
Thread.sleep(2000)
}
companion object {
private const val TAG = "MainActivity"
Log.d("MainActivity", "MainActivity created - Kiosk display mode active")
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
Log.d(TAG, "MethodChannel call received: ${call.method}")
Log.d("MainActivity", "MethodChannel call received: ${call.method}")
when (call.method) {
"reopenApp" -> {
Log.d(TAG, "reopenApp called, bringing app to foreground")
Log.d("MainActivity", "reopenApp called, bringing app to foreground")
moveTaskToBack(false)
result.success("App brought to foreground")
}
"restartApp" -> {
Log.d(TAG, "Restarting application")
Log.d("MainActivity", "Restarting application")
restartApplication()
result.success("App restart initiated")
}
"restartDevice" -> {
Log.d(TAG, "Attempting device restart")
Log.d("MainActivity", "Attempting device restart")
restartDevice(result)
}
"runShellScript" -> {
Log.d(TAG, "Executing shell restart command")
Log.d("MainActivity", "Executing shell restart command")
executeShellRestart(result)
}
"clearAudioCache" -> {
Log.d(TAG, "Clearing audio cache")
Log.d("MainActivity", "Clearing audio cache")
clearAudioResources()
result.success("Audio cache cleared")
}
"clearAllResources" -> {
Log.d(TAG, "Clearing all native resources")
Log.d("MainActivity", "Clearing all native resources")
clearAllNativeResources()
result.success("All resources cleared")
}
// === NEW: Alarm Scheduling Methods for Android 14+ compatibility ===
"scheduleRestartAlarm" -> {
val hour = call.argument<Int>("hour") ?: 0
val minute = call.argument<Int>("minute") ?: 15
Log.d(TAG, "Scheduling restart alarm for $hour:$minute")
scheduleRestartAlarm(hour, minute)
result.success("Restart alarm scheduled for $hour:$minute")
}
"cancelRestartAlarm" -> {
Log.d(TAG, "Cancelling restart alarm")
AlarmScheduler.cancelRestartAlarm(this)
result.success("Restart alarm cancelled")
}
"canScheduleExactAlarms" -> {
val canSchedule = AlarmScheduler.canScheduleExactAlarms(this)
Log.d(TAG, "Can schedule exact alarms: $canSchedule")
result.success(canSchedule)
}
"requestExactAlarmPermission" -> {
Log.d(TAG, "Requesting exact alarm permission")
AlarmScheduler.requestExactAlarmPermission(this)
result.success("Permission request initiated")
}
else -> {
Log.w(TAG, "Method not implemented: ${call.method}")
Log.w("MainActivity", "Method not implemented: ${call.method}")
result.notImplemented()
}
}
}
}
/**
* Schedule daily restart alarm at specified time.
* Compatible with Android 14+ and older versions.
*/
private fun scheduleRestartAlarm(hour: Int, minute: Int) {
try {
AlarmScheduler.scheduleRestartAlarm(this, hour, minute)
Log.d(TAG, "Restart alarm scheduled successfully for $hour:$minute")
} catch (e: Exception) {
Log.e(TAG, "Error scheduling restart alarm: ${e.message}")
}
}
private fun restartApplication() {
try {
Log.d(TAG, "Initiating app restart")
// Get the launch intent
val intent = packageManager.getLaunchIntentForPackage(packageName)
if (intent != null) {
// Configure intent for clean restart
intent.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra("restarted", true)
}
Log.d("MainActivity", "Initiating app restart")
// Use AlarmManager for reliable restart (works better on Android 14+)
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
// Schedule restart in 500ms
alarmManager.set(
AlarmManager.RTC,
System.currentTimeMillis() + 500,
pendingIntent
)
Log.d(TAG, "Restart scheduled via AlarmManager")
// Now safely exit the app
Handler(Looper.getMainLooper()).postDelayed({
finishAffinity()
android.os.Process.killProcess(android.os.Process.myPid())
}, 100)
// Clear resources before restart
clearAllNativeResources()
} else {
Log.e(TAG, "Could not create restart intent")
}
} catch (e: Exception) {
Log.e(TAG, "Error during restart: ${e.message}")
// Fallback: try simple restart
fallbackRestart()
}
}
/**
* Fallback restart method if AlarmManager approach fails
*/
private fun fallbackRestart() {
try {
Log.d(TAG, "Attempting fallback restart")
// Create restart intent
val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
putExtra("restarted", true)
}
if (intent != null) {
startActivity(intent)
finishAffinity()
Runtime.getRuntime().exit(0)
// Use a shorter delay for faster restart
Handler(Looper.getMainLooper()).postDelayed({
startActivity(intent)
finishAffinity()
// Remove exitProcess() call if present
// android.os.Process.killProcess(android.os.Process.myPid())
}, 100) // Reduced delay
Log.d("MainActivity", "App restart initiated")
} else {
Log.e("MainActivity", "Could not create restart intent")
}
} catch (e: Exception) {
Log.e(TAG, "Fallback restart also failed: ${e.message}")
Log.e("MainActivity", "Error during restart: ${e.message}")
// Fallback - don't exit, just log the error
}
}
@ -320,59 +260,15 @@ class MainActivity : FlutterActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Keep screen on at all times (AC-powered kiosk display)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
// Disable sleep/screensaver for kiosk mode
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
// Log if app was restarted
if (intent.getBooleanExtra("restarted", false)) {
Log.d(TAG, "App restarted successfully")
}
// Log if launched from boot
if (intent.getBooleanExtra("launched_from_boot", false)) {
Log.d(TAG, "App launched from boot")
// Give system time to settle after boot
Thread.sleep(2000)
}
Log.d(TAG, "MainActivity created - Kiosk display mode active")
// Schedule daily restart alarm at 00:15 (12:15 AM)
// This ensures the alarm is always set when app starts
initializeRestartAlarm()
}
/**
* Initialize the daily restart alarm.
* Called on app start to ensure alarm is always scheduled.
*/
private fun initializeRestartAlarm() {
try {
Log.d(TAG, "Initializing daily restart alarm")
AlarmScheduler.scheduleRestartAlarm(this, 0, 15) // 00:15 (12:15 AM)
Log.d(TAG, "Daily restart alarm initialized for 00:15")
} catch (e: Exception) {
Log.e(TAG, "Error initializing restart alarm: ${e.message}")
}
}
override fun onResume() {
super.onResume()
Log.d(TAG, "Activity resumed")
Log.d("MainActivity", "Activity resumed")
}
override fun onPause() {
super.onPause()
Log.d(TAG, "Activity paused - cleaning up resources")
Log.d("MainActivity", "Activity paused - cleaning up resources")
// Light cleanup when app goes to background
System.gc()
@ -380,7 +276,7 @@ class MainActivity : FlutterActivity() {
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Activity destroyed")
Log.d("MainActivity", "Activity destroyed")
// Final cleanup
clearAllNativeResources()

@ -1,82 +0,0 @@
package com.example.hmg_qline.hmg_qline
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
/**
* BroadcastReceiver for handling scheduled app restart alarms.
* This is triggered by AlarmManager at the scheduled time (e.g., 12:15 AM).
* Compatible with Android 14+ (API 34+) and older versions.
*/
class RestartAlarmReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "RestartAlarmReceiver"
const val ACTION_SCHEDULED_RESTART = "com.example.hmg_qline.SCHEDULED_RESTART"
}
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Received intent: ${intent.action}")
when (intent.action) {
ACTION_SCHEDULED_RESTART -> {
Log.d(TAG, "Scheduled restart triggered")
launchApp(context)
// Re-schedule the alarm for the next day
rescheduleAlarm(context)
}
Intent.ACTION_BOOT_COMPLETED,
"android.intent.action.QUICKBOOT_POWERON",
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
Log.d(TAG, "Boot completed - re-scheduling daily restart alarm")
rescheduleAlarm(context)
}
}
}
private fun launchApp(context: Context) {
try {
Log.d(TAG, "Launching app via foreground service")
val serviceIntent = Intent(context, BootForegroundService::class.java).apply {
putExtra("source", "scheduled_restart")
}
// Use foreground service for Android 8.0+ (API 26+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
Log.d(TAG, "App launch initiated successfully")
} catch (e: Exception) {
Log.e(TAG, "Error launching app: ${e.message}")
// Fallback: try direct activity launch
try {
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
context.startActivity(launchIntent)
} catch (fallbackError: Exception) {
Log.e(TAG, "Fallback launch also failed: ${fallbackError.message}")
}
}
}
private fun rescheduleAlarm(context: Context) {
try {
// Re-schedule for the next day at 00:15
AlarmScheduler.scheduleRestartAlarm(context, 0, 15)
Log.d(TAG, "Alarm rescheduled for next day at 00:15")
} catch (e: Exception) {
Log.e(TAG, "Error rescheduling alarm: ${e.message}")
}
}
}

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '2.1.0'
ext.kotlin_version = '1.9.10'
repositories {
google()
mavenCentral()

@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.7.0" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}

@ -1,44 +0,0 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

@ -2,12 +2,11 @@ import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hmg_qline/services/logger_service.dart';
import 'package:hmg_qline/utilities/api_exception.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:hmg_qline/utilities/api_exception.dart';
typedef FactoryConstructor<U> = U Function(dynamic);
@ -37,9 +36,6 @@ class ApiClientImp implements ApiClient {
var response = await postJsonForResponse(url, jsonObject, token: token, queryParameters: queryParameters, headers: headers0, retryTimes: retryTimes);
try {
if (!kReleaseMode) {
log("responseBody:${response.body}");
}
if (!kReleaseMode) {
log("statusCode:${response.statusCode}");
}
@ -102,7 +98,6 @@ class ApiClientImp implements ApiClient {
log(jsonDecode(response.body).toString());
// loggerService.logInfo(jsonDecode(response.body).toString());
}
if (response.statusCode >= 200 && response.statusCode < 500) {
var jsonData = jsonDecode(response.body);
if (jsonData["StatusMessage"] != null && jsonData["StatusMessage"] == "Unauthorized user attempt to access API") {

@ -8,11 +8,11 @@ bool useTestIP = false;
bool isNeedToBreakVoiceForArabic = true;
bool isSpeechCompleted = true;
bool isAndroid14 = false;
class AppStrings {
static String timeRemainingText = "Time Remaining";
static String namazTimeText = "Namaz Time";
static String poweredBy = "Powered By";
static String appName = "QLine";
static String fontNamePoppins = "Poppins";
@ -174,12 +174,34 @@ class AppConstants {
// Maximum log file size in bytes before rotation/clearing. Default 2 MB.
static int maxLogFileSizeBytes = 2 * 1024 * 1024;
// Project IDs for different branches
static const int projectIdTakhasusiMain = 16;
static const int projectIdSuwaidi = 17;
static const int projectIdKhobar = 60;
static const int projectIdTakhasusiWomen = 140;
// Takhasusi Main Branch IPs - These devices use getTurnsByOrientationForOlderVersions()
static const List<String> takhasusiMainBranchIps = [
'10.70.194.87',
'10.70.6.139',
'10.70.6.141',
'10.70.6.142',
'10.70.6.114',
'10.70.6.143',
'10.70.6.49',
'10.70.6.56',
'10.70.6.144',
'10.70.6.158',
'10.70.6.119',
'10.70.6.156',
'10.70.6.109',
'10.70.6.102',
'10.70.6.107',
'10.70.6.137',
'10.70.6.93',
'10.70.6.147',
'10.70.6.148',
'10.70.6.98',
'10.70.6.112',
'10.70.6.96',
'10.70.6.145',
'10.70.6.91',
'10.70.6.157',
];
}
class ApiConstants {
@ -187,7 +209,7 @@ class ApiConstants {
static String baseUrlUat = 'https://ms.hmg.com/nscapi'; // UAT
static String baseUrlDev = 'https://ms.hmg.com/nscapi2'; // DEV
static String baseUrl = baseUrlUat;
static String baseUrl = baseUrlLive;
// static String baseUrl = baseUrlDev;
static String baseUrlHub = '$baseUrl/PatientCallingHub';
static String baseUrlApi = '$baseUrl/api';
@ -246,7 +268,6 @@ class MockJsonRepo {
id: 189805,
patientID: 4292695,
queueNo: 'W-T-4',
laBQGroupID: 0,
mobileNo: '0598544522',
patientEmail: 'munira.ali@hotmail.com',
preferredLang: 2,
@ -258,8 +279,8 @@ class MockJsonRepo {
editedOn: DateTime.parse('2025-08-18 15:09:03.633'),
createdOn: DateTime.parse('2025-08-18 15:06:07.363'),
callTypeEnum: CallTypeEnum.doctor,
queueNoM: 'FMC W-T-4',
callNoStr: 'FMC W-T-4',
queueNoM: 'W-T-4',
callNoStr: 'W_T-4',
isQueue: false,
isToneReq: false,
isVoiceReq: false,
@ -281,9 +302,6 @@ class MockJsonRepo {
queueNoText: 'رقم الانتظار',
callForText: 'التوجه الى',
);
}
// RAW DATA:

@ -1,19 +1,17 @@
import 'dart:async';
import 'dart:developer';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:hmg_qline/view_models/screen_config_view_model.dart';
import 'package:provider/provider.dart';
import 'package:hmg_qline/config/dependency_injection.dart';
import 'package:hmg_qline/config/routes.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/services/crash_handler_service.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/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;
import 'package:hmg_qline/services/crash_handler_service.dart';
void main() {
runZonedGuarded(() async {
@ -22,13 +20,7 @@ void main() {
await AppDependencies.addDependencies();
// initialize crash handler after DI setup
getIt.get<CrashHandlerService>().setupGlobalErrorHandlers();
// Platform-specific wake lock
if (kIsWeb) {
await _initializeWeb();
} else {
await WakelockPlus.enable();
}
await WakelockPlus.enable();
} catch (e) {
log('Initialization error: $e');
}
@ -43,21 +35,6 @@ 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});
@ -76,16 +53,11 @@ class MyApp extends StatelessWidget {
log("Constraints: maxWidth=${constraints.maxWidth}, maxHeight=${constraints.maxHeight}");
log("Is Portrait (OrientationBuilder): ${orientation == Orientation.portrait}");
log("Is Landscape (OrientationBuilder): ${orientation == Orientation.landscape}");
log("Device Pixel Ratio: ${MediaQuery.of(context).devicePixelRatio}");
log("Physical Size: width=${constraints.maxWidth * MediaQuery.of(context).devicePixelRatio}, height=${constraints.maxHeight * MediaQuery.of(context).devicePixelRatio}");
log("SizeConfig - realWidth: ${SizeConfig.realScreenWidth}, realHeight: ${SizeConfig.realScreenHeight}");
log("SizeConfig - isPortrait: ${SizeConfig.isPortrait}, screenWidth: ${SizeConfig.screenWidth}, screenHeight: ${SizeConfig.screenHeight}");
log("=====================================");
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
// if (!isAndroid14) {
// SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// }
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
return MultiProvider(
providers: [
ChangeNotifierProvider<ScreenConfigViewModel>(

@ -1,5 +1,3 @@
import 'dart:developer';
class GenericRespModel {
GenericRespModel({
this.data,
@ -14,7 +12,6 @@ class GenericRespModel {
String? message;
factory GenericRespModel.fromJson(Map<String, dynamic> json) {
log("jsonjsonjosn: $json");
if (json.containsKey('StatusMessage')) {
if ((json['StatusMessage'] as String).contains('Internal server error')) {
// Utils.showToast("${json['StatusMessage']}");

@ -1,5 +1,4 @@
import 'dart:ui';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/models/kiosk_language_config_model.dart';
import 'package:hmg_qline/models/kiosk_queue_model.dart';
@ -70,8 +69,6 @@ class GlobalConfigurationsModel {
bool isWeatherReq = false;
bool isPrayerTimeReq = false;
bool isRssFeedReq = false;
bool globalClinicPrefixReq = false;
bool clinicPrefixReq = true;
QTypeEnum qTypeEnum = QTypeEnum.appointment;
ScreenTypeEnum screenTypeEnum = ScreenTypeEnum.waitingAreaScreen;
int? projectID;
@ -82,6 +79,8 @@ class GlobalConfigurationsModel {
List<KioskLanguageConfigModel>? kioskLanguageConfigList;
bool isFromTakhasusiMain = false;
bool globalClinicPrefixReq = false;
bool clinicPrefixReq = true;
String vitalSignTextEng = "Vital Sign";
String doctorTextEng = "Doctor";
@ -173,8 +172,6 @@ class GlobalConfigurationsModel {
this.isWeatherReq = false,
this.isPrayerTimeReq = false,
this.isRssFeedReq = false,
this.globalClinicPrefixReq = false,
this.clinicPrefixReq = true,
this.qTypeEnum = QTypeEnum.appointment,
this.screenTypeEnum = ScreenTypeEnum.waitingAreaScreen,
this.projectID,
@ -184,6 +181,8 @@ class GlobalConfigurationsModel {
this.kioskQueueList,
this.kioskLanguageConfigList,
this.isFromTakhasusiMain = false,
this.globalClinicPrefixReq = false,
this.clinicPrefixReq = true,
this.vitalSignTextEng = "Vital Sign",
this.doctorTextEng = "Doctor",
this.procedureTextEng = "Procedure",
@ -269,8 +268,6 @@ class GlobalConfigurationsModel {
isWeatherReq = json['isWeatherReq'] ?? false;
isPrayerTimeReq = json['isPrayerTimeReq'] ?? false;
isRssFeedReq = json['isRssFeedReq'] ?? false;
globalClinicPrefixReq = json['globalClinicPrefixReq'] ?? false;
clinicPrefixReq = json['clinicPrefixReq'] ?? true;
qTypeEnum = ((json['qType'] ?? qType) as int).toQTypeEnum();
screenTypeEnum = ((json['screenType'] ?? screenType) as int).toScreenTypeEnum();
projectID = json['projectID'] ?? 0;

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:hmg_qline/api/api_client.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/models/generic_response_model.dart';
@ -49,28 +47,15 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
ApiConstants.commonConfigGet,
params,
);
List<GlobalConfigurationsModel> globalConfigurationsModel =
List.generate(genericModel.data.length, (index) => GlobalConfigurationsModel.fromJson(json: genericModel.data[index]));
List<GlobalConfigurationsModel> globalConfigurationsModel = List.generate(genericModel.data.length, (index) => GlobalConfigurationsModel.fromJson(json: genericModel.data[index]));
if (globalConfigurationsModel.isNotEmpty) {
loggerService.logToFile(
message: globalConfigurationsModel.toString(),
type: LogTypeEnum.data,
source: "getGlobalScreenConfigurations-> screen_details_repo.dart");
loggerService.logToFile(message: globalConfigurationsModel.toString(), type: LogTypeEnum.data, source: "getGlobalScreenConfigurations-> screen_details_repo.dart");
return globalConfigurationsModel.first;
}
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;
}
@ -120,8 +105,7 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
);
genericRespModel.data = KioskPatientTicket.fromJson(genericRespModel.data);
loggerService.logToFile(
message: genericRespModel.toString(), source: "createTicketFromKiosk-> screen_details_repo.dart", type: LogTypeEnum.data);
loggerService.logToFile(message: genericRespModel.toString(), source: "createTicketFromKiosk-> screen_details_repo.dart", type: LogTypeEnum.data);
return genericRespModel;
} catch (e) {
@ -158,11 +142,9 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
body,
);
List<WeathersWidgetModel> weathersWidgetModel =
List.generate(genericRespModel.data.length, (index) => WeathersWidgetModel.fromJson(genericRespModel.data[index]));
List<WeathersWidgetModel> weathersWidgetModel = List.generate(genericRespModel.data.length, (index) => WeathersWidgetModel.fromJson(genericRespModel.data[index]));
if (weathersWidgetModel.isNotEmpty) {
loggerService.logToFile(
message: weathersWidgetModel.toString(), source: "getWeatherDetailsByCity-> screen_details_repo.dart", type: LogTypeEnum.data);
loggerService.logToFile(message: weathersWidgetModel.toString(), source: "getWeatherDetailsByCity-> screen_details_repo.dart", type: LogTypeEnum.data);
return weathersWidgetModel.first;
}
return constantWeathersWidgetModel;
@ -184,11 +166,9 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
body,
);
List<PrayersWidgetModel> prayersWidgetModel =
List.generate(genericRespModel.data.length, (index) => PrayersWidgetModel.fromJson(genericRespModel.data[index]));
List<PrayersWidgetModel> prayersWidgetModel = List.generate(genericRespModel.data.length, (index) => PrayersWidgetModel.fromJson(genericRespModel.data[index]));
if (prayersWidgetModel.isNotEmpty) {
loggerService.logToFile(
message: prayersWidgetModel.toString(), source: "getPrayerDetailsByLatLong-> screen_details_repo.dart", type: LogTypeEnum.data);
loggerService.logToFile(message: prayersWidgetModel.toString(), source: "getPrayerDetailsByLatLong-> screen_details_repo.dart", type: LogTypeEnum.data);
return prayersWidgetModel.first;
}
@ -213,8 +193,7 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
List<RssFeedModel> rssFeedModel = List.generate(genericRespModel.data.length, (index) => RssFeedModel.fromJson(genericRespModel.data[index]));
if (rssFeedModel.isNotEmpty) {
loggerService.logToFile(
message: rssFeedModel.toString(), source: "getRssFeedDetailsByLanguageID-> screen_details_repo.dart", type: LogTypeEnum.data);
loggerService.logToFile(message: rssFeedModel.toString(), source: "getRssFeedDetailsByLanguageID-> screen_details_repo.dart", type: LogTypeEnum.data);
return rssFeedModel.first;
}
@ -250,8 +229,7 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
}
@override
Future<GenericRespModel?> acknowledgeTicketForAppointment(
{required int ticketId, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
Future<GenericRespModel?> acknowledgeTicketForAppointment({required int ticketId, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
try {
var params = {
"id": ticketId.toString(),

@ -1,12 +1,10 @@
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({
@ -24,7 +22,6 @@ 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;
@ -40,34 +37,19 @@ 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;
}
// 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 = 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();
connection!.serverTimeoutInMilliseconds = 120000; // 2 minutes
connection!.keepAliveIntervalInMilliseconds = 15000; // 15 seconds keep-alive
@ -82,46 +64,6 @@ 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
@ -152,20 +94,6 @@ 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;
}
}
@ -180,7 +108,7 @@ class SignalrRepoImp implements SignalrRepo {
@override
closeConnection() async {
if (connection != null && (connection!.state == HubConnectionState.connected || connection!.state == HubConnectionState.connecting)) {
if (connection!.state == HubConnectionState.connected || connection!.state == HubConnectionState.connecting) {
await connection!.stop();
}
}

@ -1,6 +1,4 @@
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});
@ -26,8 +24,6 @@ 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,19 +2,6 @@ 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({
@ -55,128 +42,22 @@ 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 = "";
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;
dev.log('✅ IP from WiFi (wlan0): $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;
}
}
} 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;
if (interface.name == wlan) {
for (var address in interface.addresses) {
currentDeviceIp = address.address;
}
}
dev.log('⏱️ IP detection timeout or failed');
return null;
} catch (e) {
dev.log('❌ Auto-detection error: $e');
return null;
}
return currentDeviceIp;
}
}

@ -1,5 +1,4 @@
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';
@ -60,19 +59,10 @@ 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()) {
@ -103,6 +93,7 @@ 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';
@ -111,15 +102,6 @@ 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,14 +1,12 @@
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';
import 'package:logger/logger.dart';
abstract class TextToSpeechService {
Future<void> speechText({
@ -17,10 +15,9 @@ abstract class TextToSpeechService {
bool isMute = false,
});
void listenToTextToSpeechEvents({required Function() onVoiceCompleted});
// Future<void> speechTextTest(TicketData ticket);
// Test function to speak any text
Future<void> testSpeak(String text, {LanguageEnum language = LanguageEnum.english});
void listenToTextToSpeechEvents({required Function() onVoiceCompleted});
}
class TextToSpeechServiceImp implements TextToSpeechService {
@ -33,19 +30,128 @@ class TextToSpeechServiceImp implements TextToSpeechService {
double pitch = 0.6;
Map<String, String> arabicVoice = {"name": "ar-xa-x-ard-local", "locale": "ar"};
@override
// Future<void> speechTextTest(TicketData ticket) async {
// const ttsGoogleEngine = 'com.google.android.tts';
// LanguageEnum langEnum = ticket.voiceLanguageEnum;
// List engines = await textToSpeechInstance.getEngines;
// if (engines.contains(ttsGoogleEngine)) {
// await textToSpeechInstance.setEngine(ttsGoogleEngine);
// }
//
// textToSpeechInstance.setVolume(1.0);
//
// // final voices = await textToSpeechInstance.getVoices;
// // log ("voices:: $voices");
//
// await textToSpeechInstance.setVoice(arabicVoice);
//
// if (langEnum == LanguageEnum.arabic) {
// try {
// await textToSpeechInstance.setLanguage(LanguageEnum.arabic.enumToString());
// } catch (e) {
// log("error setting language english: ${e.toString()}");
// }
// } else if (langEnum == LanguageEnum.english) {
// try {
// await textToSpeechInstance.setLanguage(LanguageEnum.english.enumToString());
// } catch (e) {
// log("error setting language english: ${e.toString()}");
// }
// }
// String preVoice = ticket.ticketNoText;
// String postVoice = ticket.postVoiceText;
// if (preVoice.isNotEmpty) {
// preVoice = '$preVoice..';
// }
// String ticketNo = ticket.queueNo!.trim().toString();
//
// log("areLanguagesInstalled: ${await textToSpeechInstance.areLanguagesInstalled(["en", "ar"])}");
//
// log("lang: $langEnum");
// log("preVoice: $preVoice");
// log("postVoice: $postVoice");
// log("ticketNo: $ticketNo");
//
// String patientAlpha = "";
// String patientNumeric = "";
// String clinicName = "";
//
// bool isClinicNameAdded = (ticket.queueNo != ticket.callNoStr);
//
// if (isClinicNameAdded) {
// var queueNo = "";
// var clinic = ticketNo.split(" ");
// if (clinic.length > 1) {
// clinicName = clinic[0];
// queueNo = clinic[1];
// } else {
// queueNo = ticketNo;
// }
//
// var queueNoArray = queueNo.split("-");
// if (queueNoArray.length > 2) {
// patientAlpha = "${queueNoArray[0]} .. ${queueNoArray[1]}";
// patientNumeric = queueNoArray[2];
// } else {
// patientAlpha = queueNoArray[0];
// patientNumeric = queueNoArray[1];
// }
// } else {
// var queueNoArray = ticketNo.split("-");
// if (queueNoArray.length > 2) {
// patientAlpha = "${queueNoArray[0]} .. ${queueNoArray[1]}";
// patientNumeric = queueNoArray[2];
// } else {
// patientAlpha = queueNoArray[0];
// patientNumeric = queueNoArray[1];
// }
// }
//
// patientAlpha = patientAlpha.split('').join(' .. ');
// String roomNo = "";
//
// log("I will now all this:{ $preVoice .. $clinicName .. $patientAlpha .. $patientNumeric .. $postVoice $roomNo } ");
//
// if (langEnum == LanguageEnum.english) {
// await textToSpeechInstance.speak("$preVoice .. $clinicName .. $patientAlpha .. $patientNumeric .. $postVoice $roomNo");
// return;
// }
//
// if (isNeedToBreakVoiceForArabic) {
// await textToSpeechInstance.awaitSpeakCompletion(true);
//
// isSpeechCompleted = false;
// if (preVoice.isNotEmpty) {
// await textToSpeechInstance.speak("$preVoice ");
// }
// try {
// await textToSpeechInstance.setLanguage(LanguageEnum.english.enumToString());
// } catch (e) {
// log("error setting language english: ${e.toString()}");
// }
// await textToSpeechInstance.speak("$patientAlpha .. $patientNumeric");
//
// try {
// await textToSpeechInstance.setLanguage(langEnum.enumToString());
// } catch (e) {
// log("error setting language langEnum: ${e.toString()}");
// }
//
// await textToSpeechInstance.speak("$postVoice $roomNo").whenComplete(() {
// isSpeechCompleted = true;
// });
// } else {
// await textToSpeechInstance.speak("$preVoice .. $clinicName .. $patientAlpha .. $patientNumeric .. $postVoice $roomNo");
// }
// }
@override
Future<void> speechText({
required TicketDetailsModel ticket,
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;
@ -63,16 +169,7 @@ class TextToSpeechServiceImp implements TextToSpeechService {
textToSpeechInstance.setVolume(1.0);
}
if (isAndroid14) {
if (langEnum == LanguageEnum.arabic) {
textToSpeechInstance.setSpeechRate(0.5);
} else {
textToSpeechInstance.setSpeechRate(0.4);
}
textToSpeechInstance.setPitch(0.9);
} else {
textToSpeechInstance.setSpeechRate(0.4);
}
textToSpeechInstance.setSpeechRate(0.4);
if (langEnum == LanguageEnum.arabic) {
try {
await textToSpeechInstance.setLanguage(LanguageEnum.arabic.enumToString());
@ -94,7 +191,7 @@ class TextToSpeechServiceImp implements TextToSpeechService {
postVoice = ticket.ticketModel!.postVoiceText;
}
String roomNo = '';
if (globalConfigurationsModel.qTypeEnum != QTypeEnum.appointment && ticket.ticketModel!.roomNo.isNotEmpty) {
if (globalConfigurationsModel.qTypeEnum != QTypeEnum.appointment && ticket.ticketModel!.roomNo != null && ticket.ticketModel!.roomNo!.isNotEmpty) {
roomNo = ".. ${ticket.ticketModel!.roomNo.toString()}";
}
@ -102,7 +199,7 @@ class TextToSpeechServiceImp implements TextToSpeechService {
preVoice = '$preVoice..';
}
String ticketNo = ticket.ticketModel!.queueNo.trim().toString();
String ticketNo = ticket.ticketModel!.queueNo!.trim().toString();
log("areLanguagesInstalled: ${await textToSpeechInstance.areLanguagesInstalled(["en-US", "ar-SA"])}");
log("lang: $langEnum");
@ -200,107 +297,4 @@ 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');
}
}
}
}

@ -217,9 +217,9 @@ extension XCallType on CallTypeEnum {
}
Widget getIconByCallType(double height, QTypeEnum qType, {double? width, BoxFit fit = BoxFit.contain}) {
// if (this == CallTypeEnum.vitalSign) {
// return const SizedBox.shrink();
// }
if (this == CallTypeEnum.vitalSign) {
return const SizedBox.shrink();
}
String iconPath = "";
if (this == CallTypeEnum.vitalSign) {

@ -1,8 +1,6 @@
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();
@ -14,19 +12,6 @@ abstract class NativeMethodChannelService {
Future<void> clearAllResources();
Future<void> smartRestart({bool forceRestart = false, bool cleanupFirst = true});
/// Schedule daily restart alarm at specified time.
/// Works on Android 14+ and older versions.
Future<void> scheduleRestartAlarm({int hour = 0, int minute = 15});
/// Cancel the scheduled restart alarm.
Future<void> cancelRestartAlarm();
/// Check if the app can schedule exact alarms (Android 12+).
Future<bool> canScheduleExactAlarms();
/// Request permission to schedule exact alarms (Android 12+).
Future<void> requestExactAlarmPermission();
}
class NativeMethodChannelServiceImp implements NativeMethodChannelService {
@ -91,14 +76,6 @@ 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));
@ -115,87 +92,4 @@ class NativeMethodChannelServiceImp implements NativeMethodChannelService {
loggerService.logError("Primary restart failed, trying fallback methods: $primaryError");
}
}
// === NEW: Alarm Scheduling Methods for Android 14+ compatibility ===
/// Schedule daily restart alarm at specified time.
/// Default is 00:15 (12:15 AM).
/// Works on Android 14+ and older versions.
@override
Future<void> scheduleRestartAlarm({int hour = 0, int minute = 15}) async {
try {
loggerService.logInfo("Scheduling restart alarm for $hour:$minute");
// First check if we can schedule exact alarms
final canSchedule = await canScheduleExactAlarms();
if (!canSchedule) {
loggerService.logInfo("Exact alarm permission not granted. Requesting permission...");
await requestExactAlarmPermission();
}
await _platform.invokeMethod('scheduleRestartAlarm', {
'hour': hour,
'minute': minute,
});
loggerService.logInfo("Restart alarm scheduled successfully for $hour:$minute");
} catch (e) {
loggerService.logError("Error scheduling restart alarm: $e");
loggerService.logToFile(
message: "Error scheduling restart alarm: $e",
source: "scheduleRestartAlarm -> native_method_handler.dart",
type: LogTypeEnum.error,
);
}
}
/// Cancel the scheduled restart alarm.
@override
Future<void> cancelRestartAlarm() async {
try {
loggerService.logInfo("Cancelling restart alarm");
await _platform.invokeMethod('cancelRestartAlarm');
loggerService.logInfo("Restart alarm cancelled successfully");
} catch (e) {
loggerService.logError("Error cancelling restart alarm: $e");
loggerService.logToFile(
message: "Error cancelling restart alarm: $e",
source: "cancelRestartAlarm -> native_method_handler.dart",
type: LogTypeEnum.error,
);
}
}
/// Check if the app can schedule exact alarms.
/// 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");
return result ?? false;
} catch (e) {
loggerService.logError("Error checking exact alarm permission: $e");
return false;
}
}
/// Request permission to schedule exact alarms (Android 12+).
/// Opens system settings for the user to grant permission.
@override
Future<void> requestExactAlarmPermission() async {
try {
loggerService.logInfo("Requesting exact alarm permission");
await _platform.invokeMethod('requestExactAlarmPermission');
loggerService.logInfo("Exact alarm permission request initiated");
} catch (e) {
loggerService.logError("Error requesting exact alarm permission: $e");
loggerService.logToFile(
message: "Error requesting exact alarm permission: $e",
source: "requestExactAlarmPermission -> native_method_handler.dart",
type: LogTypeEnum.error,
);
}
}
}

@ -34,11 +34,6 @@ 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();
@ -50,7 +45,7 @@ class QueuingViewModel extends ChangeNotifier {
Future<bool?> startHubConnection() async {
try {
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
final result = await signalrRepo.startHubConnection(
return await signalrRepo.startHubConnection(
deviceIp: screenConfigViewModel.currentScreenIP,
onHubTicketCall: onHubTicketCall,
onHubConfigCall: onHubConfigCall,
@ -58,30 +53,7 @@ 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;
}
@ -122,8 +94,8 @@ class QueuingViewModel extends ChangeNotifier {
loggerService.logToFile(message: response.toString(), source: "onHubTicketCall -> queueing_view_model.dart ", type: LogTypeEnum.data);
log("onHubTicketCall: $response");
log("isCallingInProgress: $isCallingInProgress");
log("isCallingInProgress: $isCallingInProgress");
if (response != null && response.isNotEmpty) {
TicketDetailsModel ticketDetailsModel = TicketDetailsModel.fromJson(response.first as Map<String, dynamic>);
addNewTicket(ticketDetailsModel);
@ -243,8 +215,7 @@ class QueuingViewModel extends ChangeNotifier {
callTypeEnum: ticketData.callTypeEnum,
);
} else {
screenConfigViewModel.acknowledgeTicket(
ticketQueueID: ticketData.id ?? 0,
screenConfigViewModel.acknowledgeTicket(ticketQueueID: ticketData.id ?? 0,
ipAddress: screenConfigViewModel.currentScreenIP,
);
}

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hmg_qline/config/dependency_injection.dart';
@ -23,9 +22,8 @@ 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:mobile_scanner/mobile_scanner.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.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 {
@ -50,36 +48,12 @@ 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,
);
@ -91,50 +65,14 @@ class ScreenConfigViewModel extends ChangeNotifier {
loggerService.logError("Failed to enable wakelock on resume: $e");
}
// 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
// Verify connections are still alive
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,
);
@ -143,56 +81,19 @@ 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 {
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));
await Future.delayed(const Duration(seconds: 2));
}
}
}
@ -204,7 +105,7 @@ class ScreenConfigViewModel extends ChangeNotifier {
notifyListeners();
}
bool isHubConnected = false; // Start as disconnected until connection succeeds
bool isHubConnected = true;
updateIsHubConnected(bool value) {
isHubConnected = value;
@ -238,33 +139,11 @@ 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();
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');
}
}
}
log("currentScreenIP: $currentScreenIP");
}
}
@ -282,15 +161,10 @@ class ScreenConfigViewModel extends ChangeNotifier {
);
}
GlobalConfigurationsModel globalConfigurationsModel =
GlobalConfigurationsModel();
GlobalConfigurationsModel globalConfigurationsModel = GlobalConfigurationsModel();
Future<void> getGlobalConfigurationsByIP() async {
GlobalConfigurationsModel? response = await screenDetailsRepo
.getGlobalScreenConfigurations(ipAddress: currentScreenIP);
loggerService.logError("response was: $response");
GlobalConfigurationsModel? response = await screenDetailsRepo.getGlobalScreenConfigurations(ipAddress: currentScreenIP);
if (response == null) {
loggerService.logError("response was: $response");
@ -304,28 +178,24 @@ class ScreenConfigViewModel extends ChangeNotifier {
notifyListeners();
}
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
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 current IP
if (value is GlobalConfigurationsModel) {
// Use projectID-based detection (projectID = 16 for Takhasusi Main)
// IPs that belong to Takhasusi main - using AppConstants
try {
value.isFromTakhasusiMain =
value.projectID == AppConstants.projectIdTakhasusiMain;
value.isFromTakhasusiMain = AppConstants.takhasusiMainBranchIps.contains(currentScreenIP);
// Debug logging for Takhasusi Main detection
log("=== TAKHASUSI MAIN CHECK (ProjectID-based) ===");
log("=== TAKHASUSI MAIN CHECK ===");
log("Current Screen IP: $currentScreenIP");
log("Project ID: ${value.projectID}");
log("Is Takhasusi Main (projectID == 16): ${value.isFromTakhasusiMain}");
log("Is Takhasusi Main: ${value.isFromTakhasusiMain}");
log("Takhasusi Main IPs count: ${AppConstants.takhasusiMainBranchIps.length}");
if (value.isFromTakhasusiMain) {
log("✅ This device will use getTurnsByOrientationForOlderVersions()");
} else {
log("✅ This device will use getTurnsByOrientationForNewVersions()");
log("✅ This device will use getTurnsByOrientation()");
}
log("==========================================");
log("============================");
} catch (_) {
value.isFromTakhasusiMain = false;
}
@ -356,15 +226,13 @@ 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;
@ -378,14 +246,9 @@ 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) {
@ -401,13 +264,8 @@ 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) {
@ -428,8 +286,7 @@ 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();
}
@ -438,8 +295,7 @@ 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}";
@ -447,8 +303,7 @@ 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}";
@ -457,8 +312,7 @@ 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}";
@ -467,10 +321,8 @@ 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}";
@ -478,8 +330,7 @@ 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}";
@ -509,9 +360,7 @@ 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;
@ -525,7 +374,7 @@ class ScreenConfigViewModel extends ChangeNotifier {
DateTime now = DateTime.now();
log("counterValue: $counter");
if (globalConfigurationsModel.id == null || state == ViewState.error) {
if (globalConfigurationsModel.id == null) {
await getGlobalConfigurationsByIP();
}
@ -539,17 +388,8 @@ 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;
}
}
@ -575,8 +415,7 @@ 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,
);
@ -584,8 +423,7 @@ 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,
);
@ -594,47 +432,20 @@ 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>();
try {
await queuingViewModel.stopHubConnection();
} catch (e) {
log("Stop connection error during recovery (may be expected): $e");
}
await queuingViewModel.stopHubConnection();
await Future.delayed(const Duration(seconds: 2));
await queuingViewModel.startHubConnection();
}
@ -649,8 +460,7 @@ 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,
);
@ -659,14 +469,12 @@ 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,
);
@ -678,11 +486,9 @@ class ScreenConfigViewModel extends ChangeNotifier {
@override
void dispose() {
_visibilitySubscription?.cancel();
_midnightTimer?.cancel();
_healthCheckTimer?.cancel();
patientIdController.dispose();
mobileScannerController?.dispose();
super.dispose();
}
@ -701,17 +507,13 @@ class ScreenConfigViewModel extends ChangeNotifier {
Future<void> getLastTimeLogsClearedFromCache() async {
lastTimeLogsCleared = await cacheService.getLastTimeLogsCleared();
if (lastTimeLogsCleared == null) {
await cacheService
.setLastTimeLogsCleared(
lastTimeCleared: DateTime.now().millisecondsSinceEpoch)
.whenComplete(() => lastTimeLogsCleared = DateTime.now());
await cacheService.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;
@ -739,8 +541,7 @@ 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,
@ -760,8 +561,7 @@ 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");
@ -772,11 +572,9 @@ 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,
@ -793,8 +591,7 @@ 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,
@ -808,23 +605,17 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
}
Future<void> acknowledgeTicketForAppointmentOnly(
{required int ticketQueueID,
required String ipAddress,
required CallTypeEnum callTypeEnum}) async {
GenericRespModel? response =
await screenDetailsRepo.acknowledgeTicketForAppointment(
Future<void> acknowledgeTicketForAppointmentOnly({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}");
}
}
@ -836,52 +627,73 @@ 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 *************************
MobileScannerController? mobileScannerController;
Barcode? qrCodeResult;
QRViewController? qrViewController;
bool isGeneratingTicket = false;
void initializeMobileScanner() {
mobileScannerController = MobileScannerController(
facing: kIsWeb ? CameraFacing.front : CameraFacing.back,
detectionSpeed: DetectionSpeed.normal,
);
Future<void> flipCamera() async {
await qrViewController!.flipCamera();
}
void onQRDetected(BarcodeCapture barcodes) async {
if (isGeneratingTicket) return;
void onQRViewCreated({
required QRViewController controller,
required VoidCallback onSuccess,
required VoidCallback onFailure,
}) async {
qrViewController = controller;
final barcode = barcodes.barcodes.firstOrNull;
if (barcode == null || barcode.rawValue == null) 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();
}
loggerService.logInfo(
"QR Found: ${barcode.rawValue} and isGeneratingTicket: $isGeneratingTicket");
Future<void> validateAndGenerateTicketFromQR({required var data, required VoidCallback onSuccess, required VoidCallback onFailure}) async {
if (data == null) return;
qrCodeResult = data;
notifyListeners();
final patientId = int.tryParse(barcode.rawValue!);
final code = qrCodeResult!.code;
int? patientId = int.tryParse(code ?? '');
if (patientId != null && patientId > 0) {
isGeneratingTicket = true;
await generateTicketForQueue(
kioskQueueModel: currentSelectedKioskQueueModel,
patientId: patientId,
);
await Future.delayed(const Duration(seconds: 2))
.whenComplete(() => isGeneratingTicket = false);
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();
}
}
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",
@ -895,12 +707,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
if (!isHubConnected) {
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
try {
await queuingViewModel.startHubConnection();
// syncHubConnectionState will update isHubConnected
} catch (e) {
loggerService.logError("Failed to start hub connection in sync: $e");
}
bool? status = await queuingViewModel.startHubConnection();
// syncHubConnectionState will update isHubConnected
} else {
log("Hub is Connected!");
}

@ -1,9 +1,6 @@
import 'dart:async'; // Add this import
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:hmg_qline/models/ticket_model.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/app_general_widgets.dart';
import 'package:hmg_qline/views/common_widgets/date_display_widget.dart';
@ -312,16 +309,11 @@ class _AppFooterState extends State<AppFooter> {
Padding(
padding: EdgeInsets.only(top: SizeConfig.getHeightMultiplier()! * 0.1),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
InkWell(
onTap: () {
// context.read<QueuingViewModel>().addNewTicket(TicketDetailsModel(ticketModel: MockJsonRepo.ticket));
},
child: AppText(
AppStrings.poweredBy,
fontSize: SizeConfig.getWidthMultiplier()! * 1.5,
fontWeight: FontWeight.w400,
color: AppColors.darkGreyTextColor,
),
AppText(
AppStrings.poweredBy,
fontSize: SizeConfig.getWidthMultiplier()! * 1.5,
fontWeight: FontWeight.w400,
color: AppColors.darkGreyTextColor,
),
AppText(
"v${screenConfigVM.currentScreenIP.replaceAll(".", "-")}(${AppConstants.currentBuildVersion})",

@ -1,14 +1,19 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hmg_qline/config/dependency_injection.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/models/global_config_model.dart';
import 'package:hmg_qline/utilities/enums.dart';
import 'package:hmg_qline/utilities/native_method_handler.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/app_general_widgets.dart';
import 'package:hmg_qline/views/view_helpers/size_config.dart';
import 'package:provider/provider.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/views/common_widgets/app_texts_widget.dart';
import 'package:hmg_qline/views/view_helpers/size_config.dart';
class AppHeader extends StatelessWidget implements PreferredSizeWidget {
const AppHeader({super.key});
@ -29,10 +34,8 @@ class AppHeader extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () async {
final nativeMethodChannelService = getIt.get<NativeMethodChannelService>();
await nativeMethodChannelService.smartRestart(forceRestart: true, cleanupFirst: true);
onTap: () {
// getIt.get<QueuingViewModel>().triggerOOM();
},
child: engArabicTextWithSeparatorWidget(

@ -1,245 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:universal_html/html.dart' as html;
import 'package:hmg_qline/constants/app_constants.dart';
/// Dialog for configuring device IP address on web platform
class IpConfigDialog extends StatefulWidget {
final Function(String) onIpSubmitted;
const IpConfigDialog({
super.key,
required this.onIpSubmitted,
});
@override
State<IpConfigDialog> createState() => _IpConfigDialogState();
}
class _IpConfigDialogState extends State<IpConfigDialog> {
final TextEditingController _ipController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
@override
void initState() {
super.initState();
// Try to get IP from localStorage
final storedIp = html.window.localStorage['device_ip'];
if (storedIp != null && storedIp.isNotEmpty && storedIp != 'WEB_DEFAULT') {
_ipController.text = storedIp;
}
}
@override
void dispose() {
_ipController.dispose();
super.dispose();
}
void _submitIp() {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
final ip = _ipController.text.trim();
// Save to localStorage
try {
html.window.localStorage['device_ip'] = ip;
// Verify it was saved
final saved = html.window.localStorage['device_ip'];
print('💾 Saved IP to localStorage: $ip');
print('✅ Verified in localStorage: $saved');
print(' Match: ${saved == ip}');
if (saved != ip) {
print('⚠️ WARNING: localStorage save verification failed!');
print(' Expected: $ip');
print(' Got: $saved');
}
} catch (e) {
print('❌ Error saving to localStorage: $e');
}
// Notify parent
widget.onIpSubmitted(ip);
}
}
String? _validateIp(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter an IP address';
}
// Basic IP format validation (supports xxx.xxx.xxx.xxx format)
final ipPattern = RegExp(r'^(\d{1,3}\.){3}\d{1,3}$');
if (!ipPattern.hasMatch(value.trim())) {
return 'Invalid IP format (e.g., 12.4.5.1)';
}
return null;
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 500),
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Icon(
Icons.settings_ethernet,
color: AppColors.blueColor,
size: 32,
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Device IP Configuration',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: AppColors.greyColor,
),
),
),
],
),
const SizedBox(height: 24),
// Instructions
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.blueColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.blueColor.withValues(alpha: 0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Enter the device IP address to configure this kiosk.',
style: TextStyle(
fontSize: 14,
color: AppColors.greyColor,
),
),
const SizedBox(height: 8),
Text(
'Alternatively, you can add ?ip=YOUR_IP to the URL:',
style: TextStyle(
fontSize: 12,
color: AppColors.greyColor.withValues(alpha: 0.7),
),
),
const SizedBox(height: 4),
SelectableText(
'Example: ${html.window.location.origin}/?ip=12.4.5.1',
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: AppColors.blueColor,
),
),
],
),
),
const SizedBox(height: 24),
// IP Input Field
TextFormField(
controller: _ipController,
validator: _validateIp,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9.]')),
],
decoration: InputDecoration(
labelText: 'IP Address',
hintText: '12.4.5.1',
prefixIcon: Icon(Icons.router, color: AppColors.blueColor),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: AppColors.blueColor, width: 2),
),
),
onFieldSubmitted: (_) => _submitIp(),
),
const SizedBox(height: 24),
// Buttons
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isLoading ? null : () {
// Use test IP
_ipController.text = AppConstants.testIP;
_submitIp();
},
child: Text(
'Use Test IP',
style: TextStyle(color: AppColors.greyColor),
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _isLoading ? null : _submitIp,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.blueColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
'Connect',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
),
);
}
}

@ -4,31 +4,18 @@ 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:mobile_scanner/mobile_scanner.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
class KioskQrScannerScreen extends StatefulWidget {
const KioskQrScannerScreen({super.key});
class KioskQrScannerScreen extends StatelessWidget {
KioskQrScannerScreen({super.key});
@override
State<KioskQrScannerScreen> createState() => _KioskQrScannerScreenState();
}
class _KioskQrScannerScreenState extends State<KioskQrScannerScreen> {
@override
void initState() {
super.initState();
// Initialize scanner
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ScreenConfigViewModel>().initializeMobileScanner();
});
}
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
@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(
@ -37,19 +24,22 @@ class _KioskQrScannerScreenState extends State<KioskQrScannerScreen> {
children: <Widget>[
Expanded(
flex: 9,
child: MobileScanner(
controller: screenConfigViewModel.mobileScannerController,
onDetect: screenConfigViewModel.onQRDetected,
overlay: CustomPaint(
painter: ScannerOverlayPainter(
child: QRView(
key: qrKey,
overlay: QrScannerOverlayShape(
borderColor: AppColors.redColor,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: scanArea,
),
),
),
onQRViewCreated: (controller) {
screenConfigViewModel.onQRViewCreated(
controller: controller,
onSuccess: () => context.popScreen(),
onFailure: () {},
);
}),
),
],
),
@ -58,7 +48,7 @@ class _KioskQrScannerScreenState extends State<KioskQrScannerScreen> {
left: 0,
right: 0,
child: Container(
color: Colors.black.withValues(alpha: 0.4),
color: Colors.black.withOpacity(0.4),
padding: const EdgeInsets.all(16),
child: Align(
alignment: Alignment.centerLeft,
@ -88,7 +78,7 @@ class _KioskQrScannerScreenState extends State<KioskQrScannerScreen> {
),
),
GestureDetector(
onTap: () => screenConfigViewModel.mobileScannerController?.switchCamera(),
onTap: () => screenConfigViewModel.flipCamera(),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -114,8 +104,16 @@ class _KioskQrScannerScreenState extends State<KioskQrScannerScreen> {
builder: (BuildContext context, ScreenConfigViewModel screenConfigViewModel, _) {
String text = "";
if (screenConfigViewModel.isGeneratingTicket) {
text = "Generating Ticket...";
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.";
}
} else {
text = "Please scan your Patient QR from Sulaiman Al Habib Mobile App";
}
@ -142,108 +140,3 @@ class _KioskQrScannerScreenState extends State<KioskQrScannerScreen> {
);
}
}
/// 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;
}

@ -58,14 +58,14 @@ class PriorityTickets extends StatelessWidget {
return [_buildPrimaryTicket(context, tickets[0], isFullWidth: true)];
case 2:
return [
Expanded(child: _buildPrimaryTicket(context, tickets[0], isFullWidth: true)),
Expanded(child: _buildPrimaryTicket(context, tickets[0], isHalf: true)),
globalConfigurationsModel.isFromTakhasusiMain ? SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.2) : SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.5),
Expanded(child: _buildSecondaryTicket(context, tickets[1], isHalf: true)),
];
case 3:
case 4:
return [
Expanded(child: _buildPrimaryTicket(context, tickets[0], isFullWidth: true)),
Expanded(child: _buildPrimaryTicket(context, tickets[0], isHalf: true)),
globalConfigurationsModel.isFromTakhasusiMain ? SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.2) : SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.5),
Expanded(child: _buildOtherTicketsRow(context, noOfTicketsToSkipFromStart: 1, isHalf: true)),
];
@ -108,6 +108,7 @@ class PriorityTickets extends StatelessWidget {
roomNo: ticket.ticketModel?.roomNo ?? '',
roomText: _getRoomText(),
roomTextAr: _getRoomTextAr(),
isClinicAdded: false,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.none,
textDirection: globalConfigurationsModel.textDirection,
screenTypeEnum: globalConfigurationsModel.screenTypeEnum,
@ -115,32 +116,30 @@ class PriorityTickets extends StatelessWidget {
globalConfigurationsModel: globalConfigurationsModel,
);
log("isFullWidth: ${isFullWidth} and _buildPrimaryTicket");
if (isFullWidth) {
if (isHalf) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.6, // 60% of screen width
minHeight: SizeConfig.getHeightMultiplier() * 2, // Minimum height
maxWidth: MediaQuery.of(context).size.width * 0.7, // 60% of screen width
minHeight: SizeConfig.getHeightMultiplier() * 2.2, // Minimum height
),
child: Transform.scale(
scale: _getTicketScale() + 0.2,
scale: _getTicketScale(),
child: primaryCallingCard,
),
),
);
}
if (isHalf) {
if (isFullWidth) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.6, // 60% of screen width
minHeight: SizeConfig.getHeightMultiplier() * 2.2, // Minimum height
minHeight: SizeConfig.getHeightMultiplier() * 2, // Minimum height
),
child: Transform.scale(
scale: _getTicketScale(),
scale: _getTicketScale() + 0.2,
child: primaryCallingCard,
),
),
@ -158,6 +157,7 @@ class PriorityTickets extends StatelessWidget {
roomNo: ticket.ticketModel?.roomNo ?? '',
roomText: _getRoomText(),
roomTextAr: _getRoomTextAr(),
isClinicAdded: false,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.none,
textDirection: globalConfigurationsModel.textDirection,
screenTypeEnum: globalConfigurationsModel.screenTypeEnum,
@ -196,7 +196,7 @@ class PriorityTickets extends StatelessWidget {
return globalConfigurationsModel.screenTypeEnum == ScreenTypeEnum.roomLevelScreen
? 1.5
: globalConfigurationsModel.isFromTakhasusiMain
? 0.7
? 0.8
: 1.2;
}

@ -141,18 +141,15 @@ class PriorityTicketsWithSideSection extends StatelessWidget {
}
final List<Widget> children = [
Expanded(flex: 5, child: PriorityTickets(globalConfigurationsModel: globalConfigurationsModel, tickets: priorityTickets)),
// if (screenOrientationEnum == ScreenOrientationEnum.portraitUp || screenOrientationEnum == ScreenOrientationEnum.portraitDown) ...[
// SizedBox(width: SizeConfig.getWidthMultiplier() * 2),
// ],
// if (screenOrientationEnum == ScreenOrientationEnum.landscapeLeft || screenOrientationEnum == ScreenOrientationEnum.landscapeRight) ...[
// SizedBox(height: SizeConfig.getHeightMultiplier() * 0.2),
// ],
if (screenOrientationEnum == ScreenOrientationEnum.portraitUp || screenOrientationEnum == ScreenOrientationEnum.portraitDown) ...[
SizedBox(width: SizeConfig.getWidthMultiplier() * 2),
],
if (screenOrientationEnum == ScreenOrientationEnum.landscapeLeft || screenOrientationEnum == ScreenOrientationEnum.landscapeRight) ...[
SizedBox(height: SizeConfig.getHeightMultiplier() * 0.2),
],
if (showThankyouMessage) ...[
showThankyouWidget(),
] else ...[
// if (globalConfigurationsModel.isFromTakhasusiMain) ...[
// SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.1),
// ],
Expanded(
flex: !globalConfigurationsModel.isRssFeedReq && (screenOrientationEnum == ScreenOrientationEnum.landscapeLeft || screenOrientationEnum == ScreenOrientationEnum.landscapeRight) ? 6 : 4,
// flex: !globalConfigurationsModel.isRssFeedReq && (screenOrientationEnum == ScreenOrientationEnum.landscapeLeft || screenOrientationEnum == ScreenOrientationEnum.landscapeRight) ? 6 : 4,
@ -171,7 +168,7 @@ class PriorityTicketsWithSideSection extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 4,
flex: 3,
child: engArabicTextWithSeparatorWidget(
englishText: globalConfigurationsModel.queueNoTextEng ?? "",
arabicText: globalConfigurationsModel.queueNoTextArb ?? "",

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:blinking_text/blinking_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
@ -16,6 +14,7 @@ class QueueItemCallingCard extends StatelessWidget {
final String roomNo;
final bool blink;
final double scale;
final bool isClinicAdded;
final bool isGradientRequired;
final bool isBorderRequired;
final TextDirection textDirection;
@ -30,6 +29,7 @@ class QueueItemCallingCard extends StatelessWidget {
const QueueItemCallingCard({
super.key,
required this.isClinicAdded,
required this.ticketNo,
required this.roomNo,
required this.scale,
@ -47,11 +47,16 @@ class QueueItemCallingCard extends StatelessWidget {
this.blink = false,
});
bool shouldReduceSize(String ticketNo) {
// Use regex to check if ticket starts with exactly 3 letters followed by " W-"
final hasClinicPrefix = RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo);
return hasClinicPrefix;
String getFormattedTicket(String ticketNo, bool isClinicAdded) {
if (isClinicAdded) {
var formattedString = ticketNo.split(" ");
if (formattedString.length > 1) {
return "${formattedString[0]} ${formattedString[1]}";
} else {
return ticketNo;
}
}
return ticketNo;
}
@override
@ -104,8 +109,8 @@ class QueueItemCallingCard extends StatelessWidget {
top: SizeConfig.getHeightMultiplier() * 0.25,
),
child: AppText(
ticketNo,
fontSize: shouldReduceSize(ticketNo) ? SizeConfig.getWidthMultiplier() * 6 : SizeConfig.getWidthMultiplier() * 7.4,
getFormattedTicket(ticketNo, isClinicAdded),
fontSize: SizeConfig.getWidthMultiplier() * 7.4,
letterSpacing: -1,
fontHeight: 0.5,
color: AppColors.greyTextColor,

@ -11,6 +11,7 @@ import 'package:hmg_qline/views/view_helpers/size_config.dart';
class QueueItemNormalCard extends StatelessWidget {
final String ticketNo;
final String roomNo;
final bool isClinicAdded;
final TextDirection textDirection;
final String roomText;
final String roomTextAr;
@ -24,6 +25,7 @@ class QueueItemNormalCard extends StatelessWidget {
const QueueItemNormalCard({
super.key,
required this.isClinicAdded,
required this.ticketNo,
required this.roomNo,
required this.textDirection,
@ -38,11 +40,16 @@ class QueueItemNormalCard extends StatelessWidget {
this.width,
});
bool shouldReduceSize(String ticketNo) {
// Use regex to check if ticket starts with exactly 3 letters followed by " W-"
final hasClinicPrefix = RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo);
return hasClinicPrefix;
String getFormattedTicket(String ticketNo, bool isClinicAdded) {
if (isClinicAdded) {
var formattedString = ticketNo.split(" ");
if (formattedString.length > 1) {
return "${formattedString[0]} ${formattedString[1]}";
} else {
return ticketNo;
}
}
return ticketNo;
}
@override
@ -69,8 +76,8 @@ class QueueItemNormalCard extends StatelessWidget {
flex: 3,
child: Center(
child: AppText(
ticketNo,
fontSize: shouldReduceSize(ticketNo) ? SizeConfig.getWidthMultiplier() * 2.8 : SizeConfig.getWidthMultiplier() * 5,
getFormattedTicket(ticketNo, isClinicAdded),
fontSize: SizeConfig.getWidthMultiplier() * 5,
letterSpacing: -1,
fontHeight: 0.5,
color: AppColors.greyTextColor,
@ -81,13 +88,10 @@ class QueueItemNormalCard extends StatelessWidget {
),
Flexible(
flex: 3,
child: Padding(
padding: EdgeInsets.all(SizeConfig.getHeightMultiplier() * 0.07),
child: Row(
children: [
callTypeEnum.getIconByCallType(SizeConfig.getHeightMultiplier() * 0.35, qTypeEnum),
],
),
child: Row(
children: [
callTypeEnum.getIconByCallType(SizeConfig.getHeightMultiplier() * 0.35, qTypeEnum),
],
),
),
Flexible(

@ -29,8 +29,7 @@ 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(),
@ -47,16 +46,9 @@ 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 - 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
// // TODO: FOR TEST
// queuingViewModel.textToSpeechService.speechText(
// globalConfigurationsModel: screenConfigViewModel.globalConfigurationsModel,
// ticket: queuingViewModel.currentTickets.first,
@ -82,9 +74,7 @@ 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;
}
@ -92,8 +82,7 @@ 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,
@ -104,25 +93,16 @@ 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: [
@ -131,20 +111,13 @@ 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()
],
@ -154,12 +127,9 @@ 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,
);
}
@ -177,15 +147,32 @@ 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: [
// if (screenConfigVM.globalConfigurationsModel.screenTypeEnum != ScreenTypeEnum.roomLevelScreen) ...[
// Expanded(
// flex: getFlexForScreenTypes(screenConfigVM),
// child: Padding(
// padding: const EdgeInsets.symmetric(horizontal: 20),
// child: Row(
// children: [
// if (screenConfigVM.globalConfigurationsModel.isWeatherReq) ...[
// getWeatherWidget(screenConfigVM),
// ],
// const SizedBox(width: 20),
// if (screenConfigVM.globalConfigurationsModel.isPrayerTimeReq) ...[
// getPrayerWidget(screenConfigVM),
// ],
// ],
// ),
// ),
// )
// ],
Expanded(
flex: 10,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeConfig.getWidthMultiplier()! * 4),
padding: EdgeInsets.symmetric(horizontal: SizeConfig.getWidthMultiplier()! * 4),
child: dataContent(context: context),
),
),
@ -197,21 +184,18 @@ 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,161 +1,45 @@
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 StatefulWidget {
class SplashScreen extends StatelessWidget {
const SplashScreen({super.key});
@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;
}
}
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');
}
}
Future<void> _loadData(BuildContext context) async {
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();
});
final queuingViewModel = context.read<QueuingViewModel>();
await screenConfigViewModel.waitForIPAndInitializeConfigVM();
await queuingViewModel.initializeQueueingVM();
}
static Future<void>? _initializationFuture;
@override
Widget build(BuildContext context) {
_initializationFuture ??= _loadData(context);
return FutureBuilder<void>(
future: _initializationFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
return const Scaffold(
body: Center(
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),
),
],
),
child: CircularProgressIndicator(),
),
);
}
// 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(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'),
),
],
),
),
padding: const EdgeInsets.all(12),
child: Text('${snapshot.stackTrace}'),
), // Error message
),
);
}
@ -163,83 +47,20 @@ class _SplashScreenState extends State<SplashScreen> {
// If the data is loaded successfully
if (snapshot.connectionState == ConnectionState.done) {
Future.delayed(const Duration(seconds: 1)).whenComplete(() {
if (mounted) {
if (context.read<ScreenConfigViewModel>().currentScreenTypeEnum ==
ScreenTypeEnum.kioskScreen) {
context.navigateReplaceTo(AppRoutes.kioskMainScreen);
} else {
context.navigateReplaceTo(AppRoutes.mainQueueScreen);
}
if (context.read<ScreenConfigViewModel>().currentScreenTypeEnum == ScreenTypeEnum.kioskScreen) {
context.navigateReplaceTo(AppRoutes.kioskMainScreen);
} else {
context.navigateReplaceTo(AppRoutes.mainQueueScreen);
}
});
}
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
child: CircularProgressIndicator(), // Loading indicator
),
);
},
);
}
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,15 +45,7 @@ packages:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
@ -97,14 +89,6 @@ 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:
@ -232,14 +216,6 @@ 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:
@ -264,14 +240,6 @@ 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:
@ -356,34 +324,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.19"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.13.0"
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "1b60b8f9d4ce0cb0e7d7bc223c955d083a0737bee66fa1fcfe5de48225e0d5b3"
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "3.5.7"
version: "1.16.0"
nested:
dependency: transitive
description:
@ -512,6 +472,14 @@ 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:
@ -657,10 +625,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.6"
tuple:
dependency: transitive
description:
@ -677,22 +645,6 @@ 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:
@ -806,5 +758,5 @@ packages:
source: hosted
version: "1.0.1"
sdks:
dart: ">=3.10.0-0 <4.0.0"
dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.24.0"

@ -43,7 +43,6 @@ 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
@ -52,14 +51,11 @@ dependencies:
marquee: ^2.2.3
logger: ^2.4.0
fluttertoast: ^8.2.8
mobile_scanner: ^3.5.2 # Web-compatible QR scanner (replaces qr_code_scanner_plus)
qr_code_scanner_plus: ^2.0.10+1
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

@ -1,29 +0,0 @@
#!/usr/bin/env bash
# Parse command line arguments
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ip)
DEVICE_IP="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
if [[ -z "$DEVICE_IP" ]]; then
echo "Usage: sh clear_logs.sh -ip <device_ip:port>"
exit 1
fi
LOG_PATH="/storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs"
# Remove all files in the logs directory on the device
adb -s $DEVICE_IP shell rm -rf "$LOG_PATH/*"
echo "✅ Logs cleared on $DEVICE_IP"

@ -1,86 +0,0 @@
#!/usr/bin/env bash
# ===== Config =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_PATH="/storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs"
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh clear_logs_all.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh clear_logs_all.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Wi-Fi Check =====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect to Devices and Clear Logs =====
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}"
echo "🔌 Connecting to $cleanIP..."
output=$(adb connect "$cleanIP:5555" 2>&1)
if [[ "$output" == *connected* ]]; then
echo "✅ Connected to $cleanIP"
else
echo "❌ Failed to connect to $cleanIP: $output"
continue
fi
echo "🧹 Clearing logs on $cleanIP..."
adb -s "$cleanIP:5555" shell rm -rf "$LOG_PATH/*"
if [[ $? -eq 0 ]]; then
echo "✅ Logs cleared on $cleanIP"
else
echo "❌ Failed to clear logs on $cleanIP"
fi
done
echo "✅ Done! Logs cleared on all devices."

@ -1,111 +0,0 @@
#!/usr/bin/env bash
# ===== Config =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_NAME="com.example.hmg_qline.hmg_qline/.MainActivity"
LOG_DIR="$SCRIPT_DIR/logs"
TIMEOUT_SECONDS=5
run_with_timeout() {
perl -e 'alarm $ARGV[0]; exec @ARGV[1..$#ARGV]' "$TIMEOUT_SECONDS" "$@"
}
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh connect_devices.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh connect_devices.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Wi-Fi Check =====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# Create log dir
mkdir -p "$LOG_DIR"
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect to Devices =====
success_ips=()
failed_ips=()
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}" # strip port if exists
device="$cleanIP:5555"
echo "🔌 Connecting to $cleanIP..."
output=$(run_with_timeout adb connect "$device" 2>&1)
status=$?
if [[ $status -ne 0 || "$output" != *connected* ]]; then
echo "❌ Failed to connect to $cleanIP: $output"
failed_ips+=("$cleanIP")
else
echo "✅ Connected to $cleanIP"
success_ips+=("$cleanIP")
fi
done
echo "✅ Connection complete."
echo "\n====== Summary ======"
echo "✅ Success (${#success_ips[@]}):"
if [[ ${#success_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${success_ips[@]}"
else
echo " - None"
fi
echo "❌ Failed (${#failed_ips[@]}):"
if [[ ${#failed_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${failed_ips[@]}"
else
echo " - None"
fi

@ -1,206 +0,0 @@
#!/usr/bin/env bash
# ===== Config =====
APP_NAME="com.example.hmg_qline.hmg_qline/.MainActivity"
TIMEOUT_SECONDS=5
run_with_timeout() {
perl -e 'alarm $ARGV[0]; exec @ARGV[1..$#ARGV]' "$TIMEOUT_SECONDS" "$@"
}
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
-version)
APP_VERSION="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh deploy_qline.sh -ipfile <device_ips.txt> -version <app_version>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh deploy_qline.sh -ipfile <device_ips.txt> -version <app_version>"
exit 1
fi
if [[ -z "$APP_VERSION" ]]; then
echo "Usage: sh deploy_qline.sh -ipfile <device_ips.txt> -version <app_version>"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APK_PATH="$SCRIPT_DIR/apks/qline_opd_v${APP_VERSION}_prod.apk"
# ===== Wi-Fi Check =====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# ===== Disconnect All Devices =====
echo "🔌 Disconnecting all devices..."
adb disconnect
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect and Deploy =====
success_ips=()
failed_ips=()
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}" # strip port if exists
device="$cleanIP:5555"
echo "🔌 Connecting to $cleanIP..."
output=$(run_with_timeout adb connect "$device" 2>&1)
status=$?
if [[ $status -ne 0 || "$output" != *connected* ]]; then
echo "❌ Failed to connect to $cleanIP: $output"
failed_ips+=("$cleanIP (connect)")
continue
fi
echo "📱 Processing $device..."
echo "📦 Installing APK..."
if ! adb -s "$device" install -r "$APK_PATH"; then
echo "❌ Install failed on $cleanIP"
failed_ips+=("$cleanIP (install)")
continue
fi
echo "🚀 Launching app..."
if ! adb -s "$device" shell am start -n "$APP_NAME"; then
echo "❌ Launch failed on $cleanIP"
failed_ips+=("$cleanIP (launch)")
continue
fi
success_ips+=("$cleanIP")
done
echo "✅ Deployment complete."
echo "\n====== Summary ======"
echo "✅ Success (${#success_ips[@]}):"
if [[ ${#success_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${success_ips[@]}"
else
echo " - None"
fi
echo "❌ Failed (${#failed_ips[@]}):"
if [[ ${#failed_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${failed_ips[@]}"
else
echo " - None"
fi
# Calling ==> ./deploy_qline.sh -ipfile device_ips_all.txt -version 9.2
# /Users/zik/Library/Android/sdk/platform-tools
# *********************** WOMEN HEALTH (Riyadh) ***********************
# 1st Floor (OBG)
# 10.71.50.45 - v9
# 10.71.50.50 - v9
# 10.71.50.57 - v9
# 10.71.50.52 - v9
# 2nd Floor (OBG)
# 10.71.50.46 - v9
# 10.71.50.47 - v9
# 10.71.50.48 - v9
# 10.71.50.49 - v9
# 6th Floor (IVF)
# 10.71.50.53 - v9.3 qline_opd_v9.3_bg_fix_prod
# 10.71.50.56 - v9
# *********************** HMG-KHOBAR ***********************
# 10.90.82.236 - v8.9
# *********************** TAKHASSUSI ***********************
# (G-FLOOR) ORTHOPEDIC
# 10.70.194.105 - v9.2
# 10.70.194.87 - v9.2
# *********************** SUWAIDI ***********************
# 10.80.31.37 - v9.3 (SWD Oncology & Family Med WA)
# 10.80.30.220 - v9 (SWD Oncology & Family Med WA)
# 10.80.31.67 - v9 (SWD Internal Med & Other Clinics WA)
# 10.80.31.80 - v9 (SWD Internal Med & Other Clinics WA)
# 10.80.31.100 - v9.3 (SWD PEDIATRIC & Other WA) -> qline_opd_v9.3_android_14.apk
# 10.80.31.103 - v9.3 (SWD PEDIATRIC & Other WA) -> qline_opd_v9.3_android_14.apk
# 10.80.31.104 - v9.3 (SWD OB-GYN & Other Clinics Waiting Area) -> qline_opd_v9.3_android_14.apk
# 10.80.30.234 - v9.3 (SWD OB-GYN & Other Clinics Waiting Area) -> qline_opd_v9.3_android_14.apk
# Deploy version 9 to all devices
# sh deploy_qline.sh -ipfile device_ips_all.txt -version 9
# Deploy to Khobar location only
# sh deploy_qline.sh -ipfile device_ips_khobar.txt -version 8.9
# Deploy to Takhassusi hospital
# sh deploy_qline.sh -ipfile device_ips_tak.txt -version 8.9
# Deploy to Women's Health center
# sh deploy_qline.sh -ipfile device_ips_tak_women.txt -version 9

@ -1,59 +0,0 @@
10.70.194.105
10.70.194.87
10.70.6.139
10.70.6.141
10.70.6.142
10.70.6.114
10.70.6.143
10.70.6.49
10.70.6.56
10.70.6.144
10.70.6.158
10.70.6.119
10.70.6.156
10.70.6.109
10.70.6.102
10.70.6.107
10.70.6.137
10.70.6.93
10.70.6.147
10.70.6.148
10.70.6.98
10.70.6.112
10.70.6.96
10.70.6.145
10.70.6.91
10.90.82.236
10.71.50.45
10.71.50.46
10.71.50.47
10.71.50.48
10.71.50.49
10.71.50.50
10.71.50.52
10.71.50.53
10.71.50.56
10.71.50.57
10.80.31.37
10.80.30.220
10.80.31.67
10.80.31.80
10.80.31.103
10.80.31.100
10.80.31.104
10.80.30.234
10.80.30.227
10.80.32.31
10.80.35.165
10.80.35.212
10.80.35.218
10.80.31.28
10.80.35.211
10.80.36.111
10.80.32.16
10.80.35.219
10.80.36.114
10.80.36.135
10.80.36.138
10.80.36.132
10.80.36.139

@ -1,33 +0,0 @@
10.80.31.37
10.80.30.220
10.80.31.67
10.80.31.80
10.80.31.103
10.80.31.100
10.80.31.104
10.80.30.234
10.80.30.227
10.80.32.31
10.80.35.165
10.80.35.212
10.80.35.218
10.80.31.28
10.80.35.211
10.80.36.111
10.80.32.16
10.80.35.219
10.80.36.114
10.80.36.135
10.80.36.138
10.80.36.132
10.80.36.139
10.80.26.74
10.80.26.78
10.80.35.108
10.80.36.145
10.80.20.106
10.80.20.110
10.80.31.148

@ -1,27 +0,0 @@
10.70.6.139
10.70.6.141
10.70.6.142
10.70.6.114
10.70.6.143
10.70.6.49
10.70.6.56
10.70.6.144
10.70.6.158
10.70.6.119
10.70.6.156
10.70.6.109
10.70.6.102
10.70.6.107
10.70.6.137
10.70.6.93
10.70.6.147
10.70.6.148
10.70.6.98
10.70.6.112
10.70.6.96
10.70.6.145
10.70.6.91
10.70.6.157
10.70.6.159
10.70.6.128

@ -1,11 +0,0 @@
10.71.50.45
10.71.50.46
10.71.50.47
10.71.50.48
10.71.50.49
10.71.50.50
10.71.50.52
10.71.50.53
10.71.50.56
10.71.50.57

@ -1,67 +0,0 @@
#!/usr/bin/env bash
# ===== Config =====
LOG_DIR="$(pwd)/logs"
LOG_PATH="/storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs"
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh extract_all_logs.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh extract_all_logs.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Extract Logs from All Devices =====
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}"
echo "🔌 Connecting to $cleanIP..."
output=$(adb connect "$cleanIP:5555" 2>&1)
if [[ "$output" == *connected* ]]; then
echo "✅ Connected to $cleanIP"
else
echo "❌ Failed to connect to $cleanIP: $output"
continue
fi
echo "📥 Extracting logs from $cleanIP..."
adb -s "$cleanIP:5555" shell ls "$LOG_PATH"
adb -s "$cleanIP:5555" pull "$LOG_PATH" "$LOG_DIR"
done
echo "✅ Done! Logs extracted from all devices."

@ -1,70 +0,0 @@
#!/usr/bin/env bash
# ===== Config =====
LOG_DIR="$(pwd)/logs"
LOG_PATH="/storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs"
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ip)
DEVICE_IP="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh extract_logs.sh -ip <device_ip:port>"
exit 1
;;
esac
done
if [[ -z "$DEVICE_IP" ]]; then
echo "Usage: sh extract_logs.sh -ip <device_ip:port>"
exit 1
fi
# ===== Create Local Log Directory =====
mkdir -p "$LOG_DIR"
# ===== Connect to Device =====
cleanIP="${DEVICE_IP%%:*}" # strip port if exists
echo "🔌 Connecting to $cleanIP..."
output=$(adb connect "$cleanIP:5555" 2>&1)
if [[ "$output" == *connected* ]]; then
echo "✅ Connected to $cleanIP"
else
echo "❌ Failed to connect to $cleanIP: $output"
exit 1
fi
# ===== Check if Device is Available =====
if ! adb -s "$DEVICE_IP" get-state >/dev/null 2>&1; then
echo "❌ Device $DEVICE_IP is not available via ADB"
exit 1
fi
# ===== Check if Log Directory Exists on Device =====
echo "📁 Checking if log directory exists on device..."
if ! adb -s "$DEVICE_IP" shell "test -d '$LOG_PATH'" 2>/dev/null; then
echo "❌ Log directory not found on device: $LOG_PATH"
echo "💡 Available directories in Android/data/com.example.hmg_qline.hmg_qline/files/:"
adb -s "$DEVICE_IP" shell "ls -la /storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/" 2>/dev/null || echo "❌ Could not list files directory"
exit 1
fi
# ===== List Log Files =====
echo "📋 Listing log files on device..."
adb -s "$DEVICE_IP" shell "ls -la '$LOG_PATH'"
# ===== Extract Logs =====
echo "📥 Extracting logs from $DEVICE_IP..."
if adb -s "$DEVICE_IP" pull "$LOG_PATH" "$LOG_DIR"; then
echo "✅ Logs extracted successfully from $DEVICE_IP to $LOG_DIR"
else
echo "❌ Failed to extract logs from $DEVICE_IP"
exit 1
fi

@ -1,111 +0,0 @@
#!/usr/bin/env bash
# ===== Config =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_PACKAGE="com.example.hmg_qline.hmg_qline"
TIMEOUT_SECONDS=5
run_with_timeout() {
perl -e 'alarm $ARGV[0]; exec @ARGV[1..$#ARGV]' "$TIMEOUT_SECONDS" "$@"
}
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh kill_qline.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh kill_qline.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Wi-Fi Check =====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect and Kill App =====
success_ips=()
failed_ips=()
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}" # strip port if exists
device="$cleanIP:5555"
echo "🔌 Connecting to $cleanIP..."
output=$(run_with_timeout adb connect "$device" 2>&1)
status=$?
if [[ $status -ne 0 || "$output" != *connected* ]]; then
echo "❌ Failed to connect to $cleanIP: $output"
failed_ips+=("$cleanIP (connect)")
continue
fi
echo "📱 Processing $device..."
echo "❌ Force-stopping app..."
if ! adb -s "$device" shell am force-stop "$APP_PACKAGE"; then
echo "❌ Force-stop failed on $cleanIP"
failed_ips+=("$cleanIP (kill)")
continue
fi
success_ips+=("$cleanIP")
done
echo "✅ Kill operation complete."
echo "\n====== Summary ======"
echo "✅ Success (${#success_ips[@]}):"
if [[ ${#success_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${success_ips[@]}"
else
echo " - None"
fi
echo "❌ Failed (${#failed_ips[@]}):"
if [[ ${#failed_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${failed_ips[@]}"
else
echo " - None"
fi

@ -1,71 +0,0 @@
*********************** WOMEN HEALTH (Riyadh) ***********************
1st Floor (OBG)
10.71.50.45 - v9
10.71.50.50 - v9
10.71.50.57 - v9 v9.3 qline_opd_v9.3_bg_fix_prod
10.71.50.52 - v9
2nd Floor (OBG)
10.71.50.46 - v9
10.71.50.47 - v9
10.71.50.48 - v9
10.71.50.49 - v9
6th Floor (IVF)
10.71.50.53 - v9.3 qline_opd_v9.3_bg_fix_prod
10.71.50.56 - v9
*********************** HMG-KHOBAR ***********************
10.90.82.236 - v8.9
*********************** TAKHASSUSI ***********************
(G-FLOOR) ORTHOPEDIC
10.70.194.105 - v8.9
10.70.194.87 - v8.9
*********************** SUWAIDI ***********************
10.80.31.37 - v9.3 (SWD Oncology & Family Med WA) -> qline_opd_v9.3_android_14.apk
10.80.30.220 - v9 (SWD Oncology & Family Med WA) -> qline_opd_v9.3_android_14.apk
10.80.31.67 - v9 (SWD Internal Med & Other Clinics WA) -> qline_opd_v9.3_android_14.apk
10.80.31.80 - v9 (SWD Internal Med & Other Clinics WA) -> qline_opd_v9.3_android_14.apk
10.80.31.103 - v9.3 (SWD PEDIATRIC & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.31.100 - v9.3 (SWD PEDIATRIC & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.31.104 - v9.3 (SWD OB-GYN & Other Clinics Waiting Area) -> qline_opd_v9.3_android_14.apk
10.80.30.234 - v9.3 (SWD OB-GYN & Other Clinics Waiting Area) -> qline_opd_v9.3_android_14.apk
10.80.30.227 - v9.3 (SWD CARDIOLOGY & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.32.31 - v9.3 (SWD CARDIOLOGY & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.35.165 - v9.3 (SWD Urology & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.35.212 - v9.3 (SWD Urology & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.35.218 - v9.3 (SWD General Surgery & Other Waiting Area) -> qline_opd_v9.3_android_14.apk
10.80.31.28 - v9.3 (SWD General Surgery & Other Waiting Area) -> qline_opd_v9.3_android_14.apk
10.80.35.211 - v9.3 (SWD ENT & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.36.111 - v9.3 (SWD ENT & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.32.16 - v9.3 (SWD PULMONARY and Other WA) -> qline_opd_v9.3_android_14.apk
10.80.35.219 - v9.3 (SWD Ophthalmology & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.36.114 - v9.3 (SWD Ophthalmology & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.36.135 - v9.3 (SWD Dermatology / Wound Care / Plastic Surgery & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.36.138 - v9.3 (SWD Dermatology / Wound Care / Plastic Surgery & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.36.132 - v9.3 (SWD Gastroenterology / NEUROLOGY / DIABETIC EDUCATOR / GP Departments WA ) -> qline_opd_v9.3_android_14.apk
10.80.36.139 - v9.3 (SWD Gastroenterology / NEUROLOGY / DIABETIC EDUCATOR / GP Departments WA ) -> qline_opd_v9.3_android_14.apk
10.80.26.78 - v9.3 (SWD ORTHOPEDIC / SPINAL SURGERY / NEURO SURGERY/ PAIN CLINIC / Departments) -> qline_opd_v9.3_android_14.apk
10.80.26.74 - v9.3 (SWD ORTHOPEDIC / SPINAL SURGERY / NEURO SURGERY/ PAIN CLINIC / Departments) -> qline_opd_v9.3_android_14.apk
10.80.35.108 - v9.3 SWD Ophthalmology & Other WA -> qline_opd_v9.3_android_14.apk
10.80.36.145 - v9.3 SWD Dental & Other WA -> qline_opd_v9.3_android_14.apk
10.80.20.110 - v9.3 SWD Physiotherapy Male WA -> qline_opd_v9.3_android_14.apk
10.80.20.106 - v9.3 SWD Physiotherapy Female WA -> qline_opd_v9.3_android_14.apk
10.80.31.148 - v9.3 SWD Oncology & Family Med WA -> qline_opd_v9.3_android_14.apk

@ -1,99 +0,0 @@
#!/usr/bin/env bash
# ===== Config =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_NAME="com.example.hmg_qline.hmg_qline/.MainActivity"
LOG_DIR="$SCRIPT_DIR/logs"
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh restart_qline.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh restart_qline.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Wi-Fi Check ====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# Create log dir
mkdir -p "$LOG_DIR"
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect to Devices =====
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}" # strip port if exists
echo "🔌 Connecting to $cleanIP..."
output=$(adb connect "$cleanIP:5555" 2>&1)
if [[ "$output" == *connected* ]]; then
echo "✅ Connected to $cleanIP"
else
echo "❌ Failed to connect to $cleanIP: $output"
fi
done
# ===== Get Connected Devices =====
devices=$(adb devices | awk 'NR>1 && $2=="device" {print $1}')
for device in $devices; do
echo "📱 Processing $device..."
echo "❌ Closing app if running..."
adb -s "$device" shell am force-stop "${APP_NAME%%/*}"
echo "🚀 Launching app..."
adb -s "$device" shell am start -n "$APP_NAME"
log_file="$LOG_DIR/logs_${device//[^a-zA-Z0-9]/_}.txt"
echo "📝 Logging to $log_file"
adb -s "$device" logcat -s flutter > "$log_file" &
done
echo "✅ Done! Logs are saved to $LOG_DIR"

@ -1,98 +0,0 @@
#!/usr/bin/env bash
# ===== Config =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_PACKAGE="com.example.hmg_qline.hmg_qline"
LOG_DIR="$SCRIPT_DIR/logs"
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh uninstall_qline.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh uninstall_qline.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Wi-Fi Check ====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# Create log dir
mkdir -p "$LOG_DIR"
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect to Devices =====
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}" # strip port if exists
echo "🔌 Connecting to $cleanIP..."
output=$(adb connect "$cleanIP:5555" 2>&1)
if [[ "$output" == *connected* ]]; then
echo "✅ Connected to $cleanIP"
else
echo "❌ Failed to connect to $cleanIP: $output"
fi
done
# ===== Get Connected Devices =====
devices=$(adb devices | awk 'NR>1 && $2=="device" {print $1}')
for device in $devices; do
echo "📱 Processing $device..."
echo "❌ Uninstalling app..."
adb -s "$device" uninstall "$APP_PACKAGE"
if [[ $? -eq 0 ]]; then
echo "✅ Successfully uninstalled $APP_PACKAGE from $device"
else
echo "❌ Failed to uninstall $APP_PACKAGE from $device"
fi
done
echo "✅ Done! Uninstallation process completed."

@ -1,70 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMG QLine</title>
<script>
// Auto-detect device IP using WebRTC - polling version
window.deviceIPResult = null;
window.deviceIPDetecting = false;
window.startIPDetection = function() {
if (window.deviceIPDetecting) return;
window.deviceIPDetecting = true;
window.deviceIPResult = null;
try {
const rtc = new RTCPeerConnection({iceServers: []});
let resolved = false;
rtc.createDataChannel('');
rtc.createOffer().then(offer => rtc.setLocalDescription(offer));
rtc.onicecandidate = function(evt) {
if (resolved) return;
if (!evt || !evt.candidate) return;
const candidate = evt.candidate.candidate;
const ipRegex = /([0-9]{1,3}(\.[0-9]{1,3}){3})/;
const match = ipRegex.exec(candidate);
if (match) {
const ip = match[1];
// Ignore loopback and link-local
if (!ip.startsWith('127.') && !ip.startsWith('169.254.')) {
resolved = true;
rtc.close();
window.deviceIPResult = ip;
window.deviceIPDetecting = false;
}
}
};
// Timeout after 3 seconds
setTimeout(() => {
if (!resolved) {
resolved = true;
rtc.close();
window.deviceIPResult = 'TIMEOUT';
window.deviceIPDetecting = false;
}
}, 3000);
} catch (e) {
console.error('IP detection error:', e);
window.deviceIPResult = 'ERROR';
window.deviceIPDetecting = false;
}
};
</script>
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
Loading…
Cancel
Save