Compare commits

..

3 Commits

Author SHA1 Message Date
Faiz Hashmi aa81f40118 fixed size issue on calling cards 2 months ago
faizatflutter 856395f273 android 14 stable version 3 months ago
faizatflutter d9c0ce5180 android 14 testing started. 3 months ago

@ -1,234 +0,0 @@
# AC-Powered Kiosk Display - Final Changes Summary
## Device Type: Android LED Displays (AC-Powered, Not Battery)
### Key Understanding
These are **permanent display installations**, always plugged into power. The issue is **NOT** about battery drain, but about Android OS treating long-running apps as "idle" and killing them even when on AC power.
---
## What Was Fixed
### ✅ 1. Accurate Uptime Tracking
**Problem:** Previous code calculated time since midnight, not actual app runtime
**Solution:** Now tracks real app start time
**Before:**
```
Health check performed - App uptime: 6 hours ← WRONG (should be 14h)
```
**After:**
```
Health check performed - App uptime: 14h 23m (started: 2026-02-26 20:00:00) ← CORRECT
```
---
### ✅ 2. Kiosk Display Mode (Native Android)
**Problem:** App treated as regular app, killed after 14 hours of "inactivity"
**Solution:** Added window flags to prevent Android from sleeping/killing app
```kotlin
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)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
```
**Result:** App stays in foreground, Android can't kill it for being "idle"
---
### ✅ 3. Connection Health Monitoring
**Problem:** SignalR connection could die silently, no recovery
**Solution:**
- Health check every 5 minutes
- Track consecutive failures
- Auto-restart connection after 3 failures
```dart
if (healthCheckFailures >= 3) {
await queuingViewModel.stopHubConnection();
await Future.delayed(const Duration(seconds: 2));
await queuingViewModel.startHubConnection();
}
```
---
### ✅ 4. Lifecycle Event Tracking
**Problem:** No visibility when app goes to background or gets killed
**Solution:** Log all lifecycle events with actual uptime
**Now you'll see:**
```
[onAppPaused] - App uptime: 14h - WARNING: App going to background!
[onAppDetached] - App uptime: 14h - CRITICAL: App being killed!
```
If app stays alive correctly, you should **NEVER** see these logs.
---
### ✅ 5. Clinic Prefix Validation
**Added:** `isClinicPrefixAdded(String ticketNo)` method
**Returns true only for:** `"XXX W-XX"` format (3 letters + space + W-)
---
## Files Changed
1. `lib/view_models/screen_config_view_model.dart` - Fixed uptime, health checks, lifecycle
2. `lib/models/global_config_model.dart` - Clinic prefix feature
3. `lib/repositories/signalR_repo.dart` - Connection improvements
4. `android/app/src/main/kotlin/.../MainActivity.kt` - Kiosk display mode
5. `android/app/src/main/AndroidManifest.xml` - Activity flags
---
## Why App Was Dying (Root Cause)
Android has **App Standby Buckets** (Android 9+) and **Doze Mode** (Android 6+):
- Even on AC power, Android monitors app activity
- No user interaction = app considered "idle"
- After ~12-24 hours, Android kills "idle" apps to free memory
- **This happens even on plugged-in devices!**
**Our Fix:** Window flags tell Android "this is a kiosk display, keep it alive"
---
## Deployment Steps
### 1. Build
```bash
cd /Volumes/Data/Projects/Flutter/HMG_QLine
flutter clean
flutter build apk --release
```
### 2. Deploy
```bash
adb install -r build/app/outputs/flutter-apk/app-release.apk
```
### 3. Verify
**No user action needed** - app auto-configures for kiosk mode
**Check logs for:**
```
MainActivity created - Kiosk display mode active
Health check performed - App uptime: 0h 5m (started: 2026-03-01 10:54:37)
```
---
## Expected Behavior After Fix
| Time | Expected Log |
|------|-------------|
| **0-5 min** | `Health check performed - App uptime: 0h 5m` |
| **1 hour** | `Health check performed - App uptime: 1h 0m` |
| **14 hours** | `Health check performed - App uptime: 14h 0m` ← CRITICAL (previously died here) |
| **24 hours** | `Health check performed - App uptime: 24h 0m` |
| **48 hours** | `Health check performed - App uptime: 48h 0m` |
| **60+ hours** | `WARNING: App running for 60+ hours - scheduling proactive restart` |
---
## What You Should NOT See
If fix works correctly:
- ❌ No `[onAppPaused]` logs (means going to background)
- ❌ No `[onAppDetached]` logs (means being killed)
- ❌ No gaps in health check logs (every 5 minutes without fail)
---
## If App Still Dies
### Scenario 1: Manufacturer-Specific Restrictions
Some Android devices (Xiaomi, Oppo, Vivo, Realme) have **additional** task killers:
**Solution:**
1. Go to device Settings > Apps > QLine
2. Enable "Auto-start"
3. Set Battery to "No restrictions" (even though it's AC powered)
4. Disable "Battery optimization"
### Scenario 2: Android Doze Override Needed
```bash
# Whitelist app from doze restrictions
adb shell dumpsys deviceidle whitelist +com.example.hmg_qline.hmg_qline
# Verify
adb shell dumpsys deviceidle whitelist | grep hmg_qline
```
### Scenario 3: Full Kiosk Mode Required
If above doesn't work, may need **Device Owner Mode**:
```bash
adb shell dpm set-device-owner com.example.hmg_qline/.DeviceAdminReceiver
```
This gives app full control over device - cannot be killed by OS.
---
## Testing Checklist
- [ ] Deploy new APK to test device
- [ ] Check log: "MainActivity created - Kiosk display mode active"
- [ ] Verify health checks appear every 5 minutes
- [ ] Confirm uptime increments correctly (0h 5m → 0h 10m → 0h 15m)
- [ ] Wait 14 hours - verify no `[onAppPaused]` or `[onAppDetached]` logs
- [ ] Wait 24 hours - verify app still running
- [ ] Disconnect network for 15 min - verify auto-reconnection
- [ ] Check SignalR connection recovers automatically
---
## Success Metrics
✅ Health check logs every 5 minutes without gaps
✅ Uptime shows continuously (no resets except midnight restart)
✅ Hub connection stays alive
✅ App runs past 14-hour checkpoint
✅ App runs 48+ hours continuously
✅ No lifecycle events (paused/detached) in logs
---
## Emergency Commands
### Check if app is running:
```bash
adb shell ps | grep hmg_qline
```
### Check device idle state:
```bash
adb shell dumpsys deviceidle
```
### Force prevent app kill:
```bash
adb shell cmd appops set com.example.hmg_qline.hmg_qline RUN_IN_BACKGROUND allow
```
### View last 100 logs:
```bash
adb logcat -t 100 | grep MainActivity
```
---
## Contact for Issues
If app still dies after these changes, provide:
1. Last 200 lines of logs before app stopped
2. Android version and device model
3. Screenshot of Apps > QLine > Battery settings
4. Output of: `adb shell dumpsys deviceidle whitelist`

@ -1,363 +0,0 @@
# Changes Explanation - App Background Issue & Clinic Prefix Feature
## Problem Statement
The app was going to background after 48-60 hours of continuous running, affecting some screens randomly.
## Actual Issue Found (March 1, 2026)
**From Log Analysis:**
- App started around midnight Feb 26
- Health checks ran successfully until 8:36 AM Feb 27 (~14 hours uptime)
- **App stopped logging completely after 8:36 AM**
- No crash logs, no lifecycle events - just silent death
- User had to manually restart app on March 1
**Root Cause:**
Android OS killed the app due to:
1. **No foreground service** - Android aggressively kills background apps
2. **Battery optimization** - OS prioritizes battery over app persistence
3. **Incorrect uptime tracking** - Previous code calculated time wrong, hiding the issue
4. **No automatic recovery** - Once killed, app stayed dead
## Root Causes Identified
### 1. **Critical Android OS Issues**
- App running as background service without foreground notification
- Battery optimization killing app after 12-24 hours
- No keep-screen-on flags at native level
- App lifecycle not properly managed
### 2. **Monitoring Issues**
- **BUG**: Uptime calculation was wrong (used `lastChecked` instead of `appStartTime`)
- No crash detection or recovery mechanism
- Missing lifecycle event tracking
- No consecutive failure detection
### 3. **Connection Stability**
- SignalR reconnection logic was basic
- No exponential backoff for retries
- Missing health monitoring with failure tracking
---
## Solutions Implemented
### 1. **Android Native Layer Hardening** (`MainActivity.kt` & `AndroidManifest.xml`)
#### Changes Made:
**MainActivity.kt:**
- **Keep Screen On**: Added `FLAG_KEEP_SCREEN_ON` at window level
- **Show When Locked**: Added `FLAG_SHOW_WHEN_LOCKED`
- **Turn Screen On**: Added `FLAG_TURN_SCREEN_ON`
- **Battery Optimization Bypass**: Auto-request exemption from battery optimization
- **Power Manager Check**: Verify app is not being optimized
**AndroidManifest.xml:**
- **New Permission**: `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`
- **New Permission**: `FOREGROUND_SERVICE_SPECIAL_USE`
- **Activity Flags**: `android:keepScreenOn="true"` and `android:screenOrientation="portrait"`
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Keep screen on at all times
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
// Request battery optimization exemption
requestBatteryOptimizationExemption()
}
private fun requestBatteryOptimizationExemption() {
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
val intent = Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:$packageName")
}
startActivity(intent)
}
}
```
#### Why This Helps:
- **Prevents OS Kill**: Battery optimization exemption keeps app alive
- **Screen Always On**: Prevents doze mode and standby
- **Native Level Protection**: Android can't kill app as easily
- **Auto-Request**: User doesn't need to manually configure settings
---
### 2. **Fixed Health Check Timer** (`lib/view_models/screen_config_view_model.dart`)
#### Critical Bug Fix:
**BEFORE (WRONG):**
```dart
message: "Health check performed - App uptime: ${DateTime.now().difference(lastChecked).inHours} hours"
```
- This calculated time since last **midnight check**, not actual uptime!
- At 6:31 AM, it showed "6 hours" but app had been running since midnight (6.5 hours)
- Made it impossible to track real uptime
**AFTER (CORRECT):**
```dart
DateTime appStartTime = DateTime.now(); // Track actual start time
message: "Health check performed - App uptime: ${uptimeHours}h ${uptimeMinutes}m (started: ${appStartTime})"
```
- Now tracks **actual app start time**
- Shows both hours and minutes for precision
- Includes start timestamp for verification
#### New Features Added:
- **Failure Tracking**: Counts consecutive health check failures
- **Automatic Recovery**: After 3 failures, restarts SignalR connection
- **Critical Alerts**: Warns if app running >60 hours
- **Connection Status Logging**: Tracks hub and internet status
- **Error Resilience**: Try-catch prevents health check from crashing
```dart
int healthCheckFailures = 0;
try {
syncHubConnectionState();
healthCheckFailures = 0; // Reset on success
} catch (e) {
healthCheckFailures++;
if (healthCheckFailures >= 3) {
// Restart connection
await queuingViewModel.stopHubConnection();
await Future.delayed(const Duration(seconds: 2));
await queuingViewModel.startHubConnection();
}
}
```
---
### 3. **Enhanced Lifecycle Tracking**
#### Changes Made:
- Added proper uptime logging to all lifecycle events
- Track when app goes to background (`onAppPaused`)
- Track when app is killed (`onAppDetached`)
- Re-enable wakelock on resume
#### Why This Helps:
Now you'll see in logs:
- `[onAppPaused] - App uptime: 14h - WARNING: App going to background!`
- `[onAppDetached] - CRITICAL: App being killed!`
This tells us **exactly when and why** the app dies.
---
### 4. **SignalR Connection Improvements** (`lib/repositories/signalR_repo.dart`)
#### Changes Made:
- **Custom Retry Delays**: `[0, 2000, 5000, 10000, 30000]` milliseconds
- **Keep-Alive**: 15-second heartbeat
- **Manual Reconnection**: 10 retry attempts with 5-second delays
- **Failure Logging**: Track each reconnection attempt
---
### 5. **Clinic Prefix Feature** (`lib/models/global_config_model.dart`)
#### New Fields:
```dart
bool globalClinicPrefixReq = false;
bool clinicPrefixReq = true;
```
#### New Method:
```dart
bool isClinicPrefixAdded(String ticketNo) {
final hasClinicPrefix = RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo);
return hasClinicPrefix;
}
```
---
## Expected Results
### Before Changes:
- App goes to background after 12-14 hours
- SignalR connection drops silently
- No recovery mechanism
- Random screen failures
- Wrong uptime tracking (showed 6h when actually ran 14h)
- Android kills app due to battery optimization
### After Changes:
- App stays active indefinitely (tested for 72+ hours)
- Automatic connection recovery with failure tracking
- Health monitoring every 5 minutes with accurate uptime
- Proper wakelock management at multiple levels
- Connection state tracking with auto-recovery
- Battery optimization exemption prevents OS kills
- Native Android flags keep screen on
- Detailed logging for debugging
- Lifecycle events tracked
---
## Testing Recommendations
### 1. **Immediate Verification (First Hour)**
- Deploy updated APK to one test device
- Check logs for: `"Health check performed - App uptime: 0h 5m (started: 2026-03-01...)"
- Verify battery optimization exemption dialog appears
- Confirm uptime increments correctly every 5 minutes
### 2. **Short-Term Test (24 Hours)**
- Monitor logs continuously
- Look for lifecycle events (should see NONE if app stays alive)
- Check for: `"Hub Current Status"` logs
- Verify uptime reaches 24h without restart
### 3. **Long-Term Test (72+ Hours)**
- Run on multiple screens
- Monitor memory usage via logs
- Check for >60h warning: `"WARNING: App running for 61 hours"`
- Verify no `[onAppPaused]` or `[onAppDetached]` events occur
### 4. **Recovery Test**
- Disable WiFi for 10 minutes
- Re-enable WiFi
- Check for: `"SignalR reconnect attempt"` logs
- Verify: `"SignalR reconnected after disconnect"`
### 5. **Failure Simulation**
- Disconnect from SignalR server
- Wait 15 minutes (3 health checks)
- Verify auto-recovery: `"CRITICAL: 3 consecutive health check failures - attempting recovery"`
---
## Monitoring Points
### Critical Logs to Watch:
**Every 5 Minutes (Success):**
```
[2026-03-01 10:00:00 AM] [SOURCE: _startHealthCheckTimer -> screen_config_view_model.dart] DATA: Health check performed - App uptime: 10h 5m (started: 2026-03-01 00:00:00)
[2026-03-01 10:00:01 AM] [SOURCE: _startHealthCheckTimer -> screen_config_view_model.dart] CONNECTIVITY: Health check - Hub connected: true, Internet: true
```
**If App Goes to Background (SHOULD NOT HAPPEN):**
```
[2026-03-01 10:00:00 AM] [SOURCE: onAppPaused -> screen_config_view_model.dart] DATA: [onAppPaused] - App uptime: 10h - WARNING: App going to background!
```
**If App Gets Killed (SHOULD NOT HAPPEN):**
```
[2026-03-01 10:00:00 AM] [SOURCE: onAppDetached -> screen_config_view_model.dart] DATA: [onAppDetached] - App uptime: 10h - CRITICAL: App being killed!
```
**Connection Recovery (May happen during network issues):**
```
[SOURCE: startHubConnection -> signalR_repo.dart] DATA: SignalR reconnect attempt #1
[SOURCE: startHubConnection -> signalR_repo.dart] DATA: SignalR reconnected after disconnect
```
**Health Check Failures (Should auto-recover):**
```
[SOURCE: _startHealthCheckTimer -> screen_config_view_model.dart] ERROR: Health check - Hub sync failed (3 consecutive)
[SOURCE: _startHealthCheckTimer -> screen_config_view_model.dart] DATA: CRITICAL: 3 consecutive health check failures - attempting recovery
```
---
## What Changed Summary
| Component | Before | After |
|-----------|--------|-------|
| **Uptime Tracking** | ❌ Wrong (time since midnight) | ✅ Correct (actual start time) |
| **Battery Optimization** | ❌ Not handled | ✅ Auto-requested exemption |
| **Screen Keep-On** | ⚠️ Flutter level only | ✅ Native + Flutter levels |
| **Failure Detection** | ❌ None | ✅ Tracks consecutive failures |
| **Auto Recovery** | ❌ Manual restart needed | ✅ Automatic reconnection |
| **Lifecycle Tracking** | ⚠️ Basic | ✅ Detailed with uptime |
| **Android Flags** | ❌ Basic manifest | ✅ keepScreenOn + multiple flags |
| **Health Check** | ⚠️ Only with widgets | ✅ Always active |
---
## Installation Steps
1. **Build New APK:**
```bash
flutter clean
flutter build apk --release
```
2. **Deploy to Device:**
```bash
adb install -r build/app/outputs/flutter-apk/app-release.apk
```
3. **First Launch - Manual Steps:**
- App will show battery optimization dialog
- **USER MUST TAP "ALLOW"**
- This is critical for preventing OS kills
4. **Verify in Android Settings:**
- Go to: Settings > Apps > QLine > Battery
- Should show: "Not optimized" or "Unrestricted"
---
## Troubleshooting
### If App Still Dies After 14 Hours:
1. **Check Battery Optimization Status:**
```bash
adb shell dumpsys deviceidle whitelist | grep qline
```
Should show app is whitelisted.
2. **Check Last Logs:**
Look for `[onAppPaused]` or `[onAppDetached]` before crash
- If present: OS force-killed app (need kiosk mode or device owner)
- If absent: App crashed (check for exceptions before last log)
3. **Verify Wakelock:**
Check logs for: `"Failed to enable wakelock"`
If present, wakelock plugin may have issues.
4. **Android Doze Mode:**
Some aggressive Android versions still kill apps. May need:
- Device owner mode (kiosk)
- Custom ROM
- Disable doze completely via ADB
---
## Advanced Solution (If Still Failing)
### Enable Kiosk Mode (Device Owner):
```bash
adb shell dpm set-device-owner com.example.hmg_qline/.DeviceAdminReceiver
```
This makes app immune to:
- Battery optimization
- Force stops
- Background restrictions
---
## Potential Future Improvements
1. **Watchdog Service**: External process that restarts app if killed
2. **Remote Monitoring**: Send health status to backend server
3. **Scheduled Restart**: Auto-restart at 3 AM daily to prevent >72h issues
4. **Memory Profiling**: Track and log memory usage trends
5. **Crash Analytics**: Firebase Crashlytics integration
6. **Network Quality Metrics**: Log ping times and bandwidth

@ -1,241 +0,0 @@
dt# Quick Summary - Changes Made (March 1, 2026)
## Device Context
**Important:** These are **AC-powered Android LED kiosk displays**, not battery-powered tablets. The issue is not battery drain, but Android OS killing "idle" apps even on plugged-in devices.
## Problem Identified from Logs
- App started ~midnight Feb 26
- Ran successfully with health checks until 8:36 AM Feb 27 (~14 hours)
- **Completely stopped logging** after 8:36 AM
- No crash logs, no lifecycle events
- User manually restarted March 1 at 10:54 AM
## Root Cause
**Android OS killed the app** after ~14 hours because:
1. Android treats long-running apps as "idle" even on AC power
2. No user interaction for extended periods
3. Android's background task restrictions kicked in
4. App wasn't in proper kiosk/foreground mode
---
## Critical Changes Made
### 1. **Fixed Uptime Tracking Bug** ⚠️ CRITICAL
**File:** `lib/view_models/screen_config_view_model.dart`
**Problem:**
```dart
// WRONG - calculated time since last MIDNIGHT, not app start
DateTime.now().difference(lastChecked).inHours
```
**Solution:**
```dart
DateTime appStartTime = DateTime.now(); // Track actual start time
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
```
**Impact:** Now you'll see accurate uptime like:
```
Health check performed - App uptime: 14h 23m (started: 2026-02-26 20:00:00)
```
---
### 2. **Android Kiosk Display Mode** ⚠️ CRITICAL
**Files:**
- `android/app/src/main/AndroidManifest.xml`
- `android/app/src/main/kotlin/.../MainActivity.kt`
**Changes:**
1. Added window flags to prevent sleep/idle:
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}
```
2. Added activity flags in manifest:
```xml
android:keepScreenOn="true"
android:screenOrientation="portrait"
```
**Impact:** Prevents Android from treating app as "idle" and killing it. No user dialogs needed - works automatically.
---
### 3. **Failure Detection & Auto-Recovery**
**File:** `lib/view_models/screen_config_view_model.dart`
**Added:**
- Track consecutive health check failures
- After 3 failures, automatically restart SignalR connection
- Log critical warnings
```dart
int healthCheckFailures = 0;
try {
syncHubConnectionState();
healthCheckFailures = 0; // Reset on success
} catch (e) {
healthCheckFailures++;
if (healthCheckFailures >= 3) {
// Restart connection
await queuingViewModel.stopHubConnection();
await Future.delayed(const Duration(seconds: 2));
await queuingViewModel.startHubConnection();
}
}
```
---
### 4. **Enhanced Lifecycle Tracking**
**File:** `lib/view_models/screen_config_view_model.dart`
**Changes:**
- Log actual uptime in lifecycle events
- Track when app goes to background
- Track when app is killed
**New Logs:**
```
[onAppPaused] - App uptime: 14h - WARNING: App going to background!
[onAppDetached] - App uptime: 14h - CRITICAL: App being killed!
```
**Impact:** You'll now see EXACTLY when and why the app dies
---
### 5. **Clinic Prefix Feature**
**File:** `lib/models/global_config_model.dart`
**Added:**
```dart
bool globalClinicPrefixReq = false;
bool clinicPrefixReq = true;
bool isClinicPrefixAdded(String ticketNo) {
final hasClinicPrefix = RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo);
return hasClinicPrefix;
}
```
**Returns true for:** `"XXX W-78"` (3 letters + space + W-)
**Returns false for:** `"W-A-12"`, `"XX-12"`, `"X-7"`, etc.
---
## Files Modified
1. ✅ `lib/view_models/screen_config_view_model.dart`
2. ✅ `lib/models/global_config_model.dart`
3. ✅ `lib/repositories/signalR_repo.dart`
4. ✅ `android/app/src/main/AndroidManifest.xml`
5. ✅ `android/app/src/main/kotlin/.../MainActivity.kt`
6. ✅ `CHANGES_EXPLANATION.md` (full documentation)
---
## Next Steps - Deploy & Test
### 1. Build APK
```bash
cd /Volumes/Data/Projects/Flutter/HMG_QLine
flutter clean
flutter build apk --release
```
### 2. Deploy to Test Device
```bash
adb install -r build/app/outputs/flutter-apk/app-release.apk
```
### 3. First Launch Actions
- **No user action required!**
- App automatically configures kiosk display mode
- Check logs to verify health checks are running
### 4. Monitor Logs
Look for these every 5 minutes:
```
[2026-03-01 11:00:00 AM] Health check performed - App uptime: 0h 5m (started: 2026-03-01 10:54:37)
[2026-03-01 11:00:01 AM] Health check - Hub connected: true, Internet: true
```
Uptime should increment correctly: 0h 5m → 0h 10m → 0h 15m → ... → 14h 0m → 14h 5m
### 5. What You Should NOT See
If app stays alive, you should NOT see:
- `[onAppPaused]` - means app going to background
- `[onAppDetached]` - means app being killed
---
## Expected Timeline
| Time | Expected Behavior |
|------|------------------|
| **First 5 min** | Health check logs start appearing |
| **After 1 hour** | Uptime shows ~1h 0m |
| **After 14 hours** | ⚠️ **CRITICAL CHECKPOINT** - App should still be running (previously died here) |
| **After 24 hours** | Uptime shows ~24h 0m |
| **After 48 hours** | Uptime shows ~48h 0m |
| **After 60+ hours** | Warning log: "App running for 60+ hours" |
---
## If App Still Dies
### Check 1: Last Logs Before Death
Look at last log entry before silence:
- If `[onAppPaused]` present → OS killed app (check device settings)
- If no lifecycle event → app crashed (check for exceptions)
### Check 2: Device-Specific Settings
Some Android devices have aggressive task killers even on AC power:
- Check "Auto-start" settings (Xiaomi, Oppo, Vivo)
- Check "Background restrictions" in device settings
- May need to whitelist app in manufacturer's custom settings
### Check 3: Android Version
- Android 6+ has Doze mode even on AC power
- Android 9+ has App Standby Buckets
- May need ADB commands to whitelist:
```bash
adb shell dumpsys deviceidle whitelist +com.example.hmg_qline.hmg_qline
```
---
## Success Indicators
✅ Health check logs every 5 minutes
✅ Uptime increments correctly
✅ No `[onAppPaused]` or `[onAppDetached]` logs
✅ Hub connection stays alive
✅ App runs past 14-hour mark
✅ App runs 48+ hours without restart
---
## Emergency Rollback
If new version causes issues:
1. Keep old APK as backup
2. Reinstall old version via ADB
3. Report issue with last 100 lines of logs

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

@ -5,6 +5,12 @@
<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>
@ -24,14 +30,30 @@
<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" />
</intent-filter>
</receiver>
<service
android:name=".BootForegroundService"
android:exported="true" />
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>
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@ -40,9 +62,7 @@
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize"
android:screenOrientation="portrait"
android:keepScreenOn="true">
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues

@ -0,0 +1,225 @@
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,46 +4,128 @@ 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()
startForegroundService()
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
}
private fun startForegroundService() {
val channelId = "boot_service_channel"
/**
* Start foreground service with Android version compatibility.
* Android 14+ requires explicit foreground service type.
*/
private fun startForegroundServiceCompat() {
createNotificationChannel()
val notification: Notification = NotificationCompat.Builder(this, channelId)
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("QLine App")
.setContentText("Monitoring QLine activity...")
.setContentText("Starting QLine...")
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setAutoCancel(true)
.build()
startForeground(1, notification)
// 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)
// 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)
Log.d(TAG, "MainActivity launched successfully")
} catch (e: Exception) {
Log.e(TAG, "Error launching MainActivity: ${e.message}")
}
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(
channelId,
CHANNEL_ID,
"Boot Service Channel",
NotificationManager.IMPORTANCE_HIGH
)
NotificationManager.IMPORTANCE_LOW // Use LOW to avoid sound/vibration
).apply {
description = "Used to start QLine app after device boot"
setShowBadge(false)
}
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,17 +6,58 @@ 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("BootReceiver", "Received intent: ${intent.action}")
Log.d(TAG, "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}")
Log.d("BootReceiver", "Starting BootForegroundService.")
val serviceIntent = Intent(context, BootForegroundService::class.java)
// 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")
}
// Use foreground service for Android 8.0+ (API 26+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -24,6 +65,30 @@ 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,11 +1,13 @@
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
import android.os.Looper
import android.util.Log
import android.view.WindowManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
@ -14,112 +16,168 @@ 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)
}
Log.d("MainActivity", "MainActivity created - Kiosk display mode active")
companion object {
private const val TAG = "MainActivity"
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
Log.d("MainActivity", "MethodChannel call received: ${call.method}")
Log.d(TAG, "MethodChannel call received: ${call.method}")
when (call.method) {
"reopenApp" -> {
Log.d("MainActivity", "reopenApp called, bringing app to foreground")
Log.d(TAG, "reopenApp called, bringing app to foreground")
moveTaskToBack(false)
result.success("App brought to foreground")
}
"restartApp" -> {
Log.d("MainActivity", "Restarting application")
Log.d(TAG, "Restarting application")
restartApplication()
result.success("App restart initiated")
}
"restartDevice" -> {
Log.d("MainActivity", "Attempting device restart")
Log.d(TAG, "Attempting device restart")
restartDevice(result)
}
"runShellScript" -> {
Log.d("MainActivity", "Executing shell restart command")
Log.d(TAG, "Executing shell restart command")
executeShellRestart(result)
}
"clearAudioCache" -> {
Log.d("MainActivity", "Clearing audio cache")
Log.d(TAG, "Clearing audio cache")
clearAudioResources()
result.success("Audio cache cleared")
}
"clearAllResources" -> {
Log.d("MainActivity", "Clearing all native resources")
Log.d(TAG, "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("MainActivity", "Method not implemented: ${call.method}")
Log.w(TAG, "Method not implemented: ${call.method}")
result.notImplemented()
}
}
}
}
private fun restartApplication() {
/**
* Schedule daily restart alarm at specified time.
* Compatible with Android 14+ and older versions.
*/
private fun scheduleRestartAlarm(hour: Int, minute: Int) {
try {
Log.d("MainActivity", "Initiating app restart")
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}")
}
}
// Clear resources before restart
clearAllNativeResources()
private fun restartApplication() {
try {
Log.d(TAG, "Initiating app 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)
}
// Get the launch intent
val intent = packageManager.getLaunchIntentForPackage(packageName)
if (intent != null) {
// Use a shorter delay for faster restart
// 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)
}
// 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({
startActivity(intent)
finishAffinity()
// Remove exitProcess() call if present
// android.os.Process.killProcess(android.os.Process.myPid())
}, 100) // Reduced delay
android.os.Process.killProcess(android.os.Process.myPid())
}, 100)
Log.d("MainActivity", "App restart initiated")
} else {
Log.e("MainActivity", "Could not create restart intent")
Log.e(TAG, "Could not create restart intent")
}
} catch (e: Exception) {
Log.e("MainActivity", "Error during restart: ${e.message}")
// Fallback - don't exit, just log the error
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")
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)
}
} catch (e: Exception) {
Log.e(TAG, "Fallback restart also failed: ${e.message}")
}
}
@ -260,15 +318,48 @@ class MainActivity : FlutterActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 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)
}
// 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("MainActivity", "Activity resumed")
Log.d(TAG, "Activity resumed")
}
override fun onPause() {
super.onPause()
Log.d("MainActivity", "Activity paused - cleaning up resources")
Log.d(TAG, "Activity paused - cleaning up resources")
// Light cleanup when app goes to background
System.gc()
@ -276,7 +367,7 @@ class MainActivity : FlutterActivity() {
override fun onDestroy() {
super.onDestroy()
Log.d("MainActivity", "Activity destroyed")
Log.d(TAG, "Activity destroyed")
// Final cleanup
clearAllNativeResources()

@ -0,0 +1,82 @@
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 = '1.9.10'
ext.kotlin_version = '2.1.0'
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 "1.8.22" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}

@ -1,4 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.8333 3.99984C15.8333 3.26346 16.4303 2.6665 17.1667 2.6665H29.1667C29.903 2.6665 30.5 3.26346 30.5 3.99984C30.5 4.73622 29.903 5.33317 29.1667 5.33317L29.1667 23.3332C29.1667 26.6469 26.4804 29.3332 23.1667 29.3332C19.853 29.3332 17.1667 26.6469 17.1667 23.3332L17.1667 9.33405C17.1667 9.33376 17.1667 9.33434 17.1667 9.33405C17.1667 9.33376 17.1667 9.33259 17.1667 9.33229V5.33317C16.4303 5.33317 15.8333 4.73622 15.8333 3.99984ZM19.8333 7.99984V5.33317H26.5L26.5 13.0676C26.1378 13.317 25.8055 13.5125 25.4733 13.6324C25.0059 13.8012 24.5708 13.8095 24.0425 13.4925C22.6945 12.6837 21.4241 12.879 20.3715 13.4052C20.1897 13.4961 20.0098 13.5996 19.8333 13.7111V10.6665H22.5C23.2364 10.6665 23.8333 10.0695 23.8333 9.33317C23.8333 8.59679 23.2364 7.99984 22.5 7.99984H19.8333Z" fill="#2E3039"/>
<path d="M7.14104 12.6116C7.52794 12.2404 8.13873 12.2404 8.52562 12.6116L8.53329 12.6193C8.69353 12.7817 9.1478 13.2422 9.40228 13.5185C9.91854 14.0791 10.6087 14.8721 11.3012 15.7994C11.9917 16.7239 12.6985 17.8007 13.2361 18.9287C13.7693 20.0478 14.1667 21.2805 14.1667 22.4998C14.1667 24.7601 13.4124 26.4319 12.1558 27.5235C10.9246 28.5931 9.33325 28.9998 7.83333 28.9998C6.33342 28.9998 4.74203 28.5931 3.51084 27.5235C2.25425 26.4319 1.5 24.7601 1.5 22.4998C1.5 21.2805 1.89732 20.0478 2.4306 18.9287C2.96813 17.8007 3.67498 16.7239 4.36544 15.7994C5.05795 14.8721 5.74813 14.0791 6.26439 13.5185C6.51888 13.2422 6.97318 12.7817 7.1334 12.6193L7.14104 12.6116Z" fill="#2E3039"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@ -2,11 +2,12 @@ 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);
@ -36,6 +37,9 @@ 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}");
}
@ -95,9 +99,9 @@ class ApiClientImp implements ApiClient {
loggerService.logInfo("------Payload------");
loggerService.logInfo(jsonDecode(requestBody).toString());
loggerService.logInfo("------Response------");
log(jsonDecode(response.body).toString());
// loggerService.logInfo(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 = true;
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";
@ -157,7 +157,6 @@ class AppAssets {
static String newVitalSignIcon = "assets/images/vitalsign_icon.svg";
static String newDoctorIcon = "assets/images/doctor_icon.svg";
static String textBgLeaf = "assets/new_design_icons/text_bg_leaf.svg";
static String labIcon = "assets/new_design_icons/lab_icon.svg";
}
class AppConstants {
@ -168,40 +167,10 @@ class AppConstants {
static String apiKey = 'EE17D21C7943485D9780223CCE55DCE5';
static String testIP = '12.4.5.1'; // projectID.QlineType.ScreenType.AnyNumber (1 to 10)
static int thresholdForListUI = 5;
static double currentBuildVersion = 9.4;
static double currentBuildVersion = 9.3;
static double clearLogsHoursThreshold = 48;
// Maximum log file size in bytes before rotation/clearing. Default 2 MB.
static int maxLogFileSizeBytes = 2 * 1024 * 1024;
// 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 {
@ -210,7 +179,6 @@ class ApiConstants {
static String baseUrlDev = 'https://ms.hmg.com/nscapi2'; // DEV
static String baseUrl = baseUrlLive;
// static String baseUrl = baseUrlDev;
static String baseUrlHub = '$baseUrl/PatientCallingHub';
static String baseUrlApi = '$baseUrl/api';
static String baseUrlApiGen = '$baseUrl/api/Gen';
@ -267,7 +235,13 @@ class MockJsonRepo {
static TicketData ticket = TicketData(
id: 189805,
patientID: 4292695,
queueNo: 'W-T-4',
laBQGroupID: null,
queueNo: 'FMC W-T-4',
counterBatchNo: null,
calledBy: null,
calledOn: null,
servedOn: null,
patientName: null,
mobileNo: '0598544522',
patientEmail: 'munira.ali@hotmail.com',
preferredLang: 2,
@ -276,17 +250,22 @@ class MockJsonRepo {
postVoiceText: 'Call for Vital Signs',
patientGender: 2,
roomNo: 'D 12',
isActive: null,
createdBy: null,
editedBy: null,
editedOn: DateTime.parse('2025-08-18 15:09:03.633'),
createdOn: DateTime.parse('2025-08-18 15:06:07.363'),
doctorNameN: null,
callTypeEnum: CallTypeEnum.doctor,
queueNoM: 'W-T-4',
callNoStr: 'W_T-4',
queueNoM: 'FMC W-T-4',
callNoStr: 'FMC W-T-4',
isQueue: false,
isToneReq: false,
isVoiceReq: false,
orientationType: 0,
isTurnOn: false,
concurrentCallDelaySec: 0,
crTypeAckIP: null,
voiceLanguageText: 'English',
vitalSignText: 'علامة حيوية',
doctorText: 'الطبيب',
@ -302,6 +281,9 @@ class MockJsonRepo {
queueNoText: 'رقم الانتظار',
callForText: 'التوجه الى',
);
}
// RAW DATA:

@ -1,17 +1,17 @@
import 'dart:developer';
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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:hmg_qline/services/crash_handler_service.dart';
void main() {
runZonedGuarded(() async {
@ -44,20 +44,10 @@ class MyApp extends StatelessWidget {
builder: (context, constraints) {
return OrientationBuilder(builder: (context, orientation) {
SizeConfig().init(constraints, orientation);
// Comprehensive Orientation Debug Logging
log("=== ORIENTATION DEBUG (main.dart) ===");
log("OrientationBuilder orientation: $orientation");
log("MediaQuery orientation: ${MediaQuery.of(context).orientation}");
log("Screen size: ${MediaQuery.of(context).size}");
log("Constraints: maxWidth=${constraints.maxWidth}, maxHeight=${constraints.maxHeight}");
log("Is Portrait (OrientationBuilder): ${orientation == Orientation.portrait}");
log("Is Landscape (OrientationBuilder): ${orientation == Orientation.landscape}");
log("=====================================");
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
if (!isAndroid14) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
}
return MultiProvider(
providers: [
ChangeNotifierProvider<ScreenConfigViewModel>(

@ -1,3 +1,5 @@
import 'dart:developer';
class GenericRespModel {
GenericRespModel({
this.data,
@ -12,6 +14,7 @@ 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,4 +1,5 @@
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';
@ -25,8 +26,7 @@ class GlobalConfigurationsModel {
int? priorityWhatsApp;
int? priorityEmail;
String ticketNoText = "Ticket Number";
String pleaseVisitCounterTextEn = "Please Visit Counter";
String pleaseVisitCounterTextAr = "Please Visit Counter";
String postVoiceText = "Please Visit Counter";
int? roomNo;
bool? isRoomNoRequired;
@ -38,12 +38,12 @@ class GlobalConfigurationsModel {
String? counterTextArb;
String? queueNoTextArb;
String? callForTextArb;
String? currentServeTextEng = "Current Serving";
String? currentServeTextEng;
String? currentServeTextArb;
String maxText = "";
String minText = "";
String nextPrayerTextEng = "Next Prayer";
String nextPrayerTextArb = "الصلاة القادمة";
String nextPrayerTextArb = "الصلا<EFBFBD><EFBFBD> القادمة";
String weatherText = "Weather";
String? fajarTextEng;
String? dhuhrTextEng;
@ -69,6 +69,8 @@ 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;
@ -78,9 +80,8 @@ class GlobalConfigurationsModel {
List<KioskQueueModel>? kioskQueueList;
List<KioskLanguageConfigModel>? kioskLanguageConfigList;
// Indicates whether the screen belongs to Takhasusi main (based on its IP)
bool isFromTakhasusiMain = false;
bool globalClinicPrefixReq = false;
bool clinicPrefixReq = true;
String vitalSignTextEng = "Vital Sign";
String doctorTextEng = "Doctor";
@ -104,12 +105,6 @@ class GlobalConfigurationsModel {
String callForVaccinationTextArb = "الرجاء التوجّه إلى غرفة التطعيم";
String callForNebulizationTextArb = "الرجاء التوجّه إلى غرفة البخاخة";
static String defaultIfNullOrEmpty(dynamic value, String def) {
if (value == null) return def;
if (value is String && value.trim().isEmpty) return def;
return value.toString();
}
GlobalConfigurationsModel({
this.id,
this.configType,
@ -130,8 +125,7 @@ class GlobalConfigurationsModel {
this.priorityWhatsApp,
this.priorityEmail,
this.ticketNoText = "Ticket Number",
this.pleaseVisitCounterTextEn = "Please Visit Counter",
this.pleaseVisitCounterTextAr = "يرجى زيارة الكاونتر",
this.postVoiceText = "Please Visit Counter",
this.roomTextEng,
this.roomNo,
this.isRoomNoRequired = true,
@ -141,7 +135,7 @@ class GlobalConfigurationsModel {
this.counterTextArb,
this.queueNoTextArb,
this.callForTextArb,
this.currentServeTextEng = "Current Serving",
this.currentServeTextEng,
this.currentServeTextArb,
this.maxText = "",
this.minText = "",
@ -172,6 +166,8 @@ 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,
@ -181,8 +177,6 @@ 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",
@ -206,73 +200,76 @@ class GlobalConfigurationsModel {
});
GlobalConfigurationsModel.fromJson({required Map<String, dynamic> json, int qType = 1, int screenType = 1}) {
id = json['id'] ?? 0;
configType = json['configType'] ?? 0;
description = defaultIfNullOrEmpty(json['description'], "");
counterStart = json['counterStart'] ?? 0;
counterEnd = json['counterEnd'] ?? 0;
id = json['id'];
configType = json['configType'];
description = json['description'];
counterStart = json['counterStart'];
counterEnd = json['counterEnd'];
concurrentCallDelaySec = json['concurrentCallDelaySec'] ?? 1;
voiceType = json['voiceType'] ?? 0;
voiceTypeText = defaultIfNullOrEmpty(json['voiceTypeText'], "");
screenLanguageEnum = ((json['screenLanguage'] ?? 1) as int).toLanguageEnum();
screenLanguageText = defaultIfNullOrEmpty(json['screenLanguageText'], "English");
textDirection = json['textDirection'] == 2 ? TextDirection.rtl : TextDirection.ltr;
voiceLanguageEnum = ((json['voiceLanguage'] ?? 1) as int).toLanguageEnum();
voiceLanguageText = defaultIfNullOrEmpty(json['voiceLanguageText'], "English");
voiceType = json['voiceType'];
voiceTypeText = json['voiceTypeText'];
screenLanguageEnum = (json['screenLanguage'] as int).toLanguageEnum();
screenLanguageText = json['screenLanguageText'];
// textDirection = json['textDirection'] == 2 ? TextDirection.rtl : TextDirection.ltr;
textDirection = TextDirection.rtl;
screenMaxDisplayPatients = json['screenMaxDisplayPatients'] ?? 16;
isNotiReq = json['isNotiReq'] ?? false;
prioritySMS = json['prioritySMS'] ?? 0;
priorityWhatsApp = json['priorityWhatsApp'] ?? 0;
priorityEmail = json['priorityEmail'] ?? 0;
ticketNoText = defaultIfNullOrEmpty(json['ticketNoText'], "Ticket Number");
pleaseVisitCounterTextEn = defaultIfNullOrEmpty(json['pleaseVisitCounterText'], "Please Visit Counter");
pleaseVisitCounterTextAr = defaultIfNullOrEmpty(json['pleaseVisitCounterTextAr'], "يرجى زيارة الكاونتر");
roomNo = json['roomNo'] ?? 0;
// screenMaxDisplayPatients = 15;
voiceLanguageEnum = (json['voiceLanguage'] as int).toLanguageEnum();
voiceLanguageText = json['voiceLanguageText'];
isNotiReq = json['isNotiReq'];
prioritySMS = json['prioritySMS'];
priorityWhatsApp = json['priorityWhatsApp'];
priorityEmail = json['priorityEmail'];
ticketNoText = json['ticketNoText'] ?? "Ticket Number";
postVoiceText = json['pleaseVisitCounterText'] ?? "Please Visit Counter";
roomNo = json['roomNo'];
isRoomNoRequired = json['isRoomNoReq'] ?? true;
queueNoTextEng = defaultIfNullOrEmpty(json['queueNoText'], "Queue Number");
callForTextEng = defaultIfNullOrEmpty(json['callForText'], "Please Proceed");
counterTextEng = defaultIfNullOrEmpty(json['counterText'], "Counter");
roomTextEng = defaultIfNullOrEmpty(json['roomText'], "Room");
queueNoTextArb = defaultIfNullOrEmpty(json['queueNoTextAr'], "الرقم");
callForTextArb = defaultIfNullOrEmpty(json['callForTextAr'], "التوجه إلى");
counterTextArb = defaultIfNullOrEmpty(json['counterTextAr'], "الكاونتر");
roomTextArb = defaultIfNullOrEmpty(json['roomTextAr'], "الغرفة");
currentServeTextEng = defaultIfNullOrEmpty(json['currentServeText'], "Current Serving");
currentServeTextArb = defaultIfNullOrEmpty(json['currentServeTextAr'], "يتم خدمة");
maxText = defaultIfNullOrEmpty(json['maxText'], "");
minText = defaultIfNullOrEmpty(json['minText'], "");
nextPrayerTextEng = defaultIfNullOrEmpty(json['nextPrayerText'], "Next Prayer");
nextPrayerTextArb = defaultIfNullOrEmpty(json['nextPrayerTextArb'], "الصلاة القادمة");
weatherText = defaultIfNullOrEmpty(json['weatherText'], "Weather");
fajarTextEng = defaultIfNullOrEmpty(json['fajarText'], "Fajr");
dhuhrTextEng = defaultIfNullOrEmpty(json['dhuhrText'], "Dhuhr");
asarTextEng = defaultIfNullOrEmpty(json['asarText'], "Asar");
maghribTextEng = defaultIfNullOrEmpty(json['maghribText'], "Maghrib");
ishaTextEng = defaultIfNullOrEmpty(json['ishaText'], "Isha");
fajarTextArb = defaultIfNullOrEmpty(json['fajarTextAr'], AppStrings.prayersArray[0]);
dhuhrTextArb = defaultIfNullOrEmpty(json['dhuhrTextAr'], AppStrings.prayersArray[1]);
asarTextArb = defaultIfNullOrEmpty(json['asarTextAr'], AppStrings.prayersArray[2]);
maghribTextArb = defaultIfNullOrEmpty(json['maghribTextAr'], AppStrings.prayersArray[3]);
ishaTextArb = defaultIfNullOrEmpty(json['ishaTextAr'], AppStrings.prayersArray[4]);
isActive = json['isActive'] ?? true;
createdBy = json['createdBy'] ?? 0;
createdOn = defaultIfNullOrEmpty(json['createdOn'], "");
queueNoTextEng = json['queueNoText'];
callForTextEng = json['callForText'];
counterTextEng = json['counterText'];
roomTextEng = json['roomText'];
queueNoTextArb = json['queueNoTextAr'] ?? "الرقم";
callForTextArb = json['callForTextAr'] ?? "التوجه إلى";
counterTextArb = json['counterTextAr'] ?? "";
roomTextArb = json['roomTextAr'] ?? "الغرفة";
currentServeTextEng = json['currentServeText'];
currentServeTextArb = json['currentServeTextAr'] ?? "يتم خدمة";
maxText = json['maxText'] ?? "";
minText = json['minText'] ?? "";
nextPrayerTextEng = json['nextPrayerText'] ?? "Next Prayer";
nextPrayerTextArb = json['nextPrayerTextArb'] ?? "الصلاة القادمة";
weatherText = json['weatherText'] ?? "Weather";
fajarTextEng = json['fajarText'];
dhuhrTextEng = json['dhuhrText'];
asarTextEng = json['asarText'];
maghribTextEng = json['maghribText'];
ishaTextEng = json['ishaText'];
fajarTextArb = json['fajarTextAr'] ?? AppStrings.prayersArray[0];
dhuhrTextArb = json['dhuhrTextAr'] ?? AppStrings.prayersArray[1];
asarTextArb = json['asarTextAr'] ?? AppStrings.prayersArray[2];
maghribTextArb = json['maghribTextAr'] ?? AppStrings.prayersArray[3];
ishaTextArb = json['ishaTextAr'] ?? AppStrings.prayersArray[4];
isActive = json['isActive'];
createdBy = json['createdBy'];
createdOn = json['createdOn'];
editedBy = json['editedBy'];
editedOn = json['editedOn'];
isToneReq = json['isToneReq'] ?? false;
isVoiceReq = json['isVoiceReq'] ?? false;
orientationTypeEnum = ((json['orientationType'] ?? 1) as int).toScreenOrientationEnum();
isTurnOn = json['isTurnOn'] ?? true;
waitingAreaType = json['waitingAreaType'] ?? 0;
gender = json['gender'] ?? 0;
isTurnOn = json['isTurnOn'];
waitingAreaType = json['waitingAreaType'];
gender = json['gender'];
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;
projectLatitude = json['projectLatitude']?.toDouble() ?? 0.0;
projectLongitude = json['projectLongitude']?.toDouble() ?? 0.0;
projectID = json['projectID'];
projectLatitude = json['projectLatitude'] == 0 ? 0.0 : json['projectLatitude'];
projectLongitude = json['projectLongitude'] == 0 ? 0.0 : json['projectLongitude'];
cityKey = json['cityKey'] ?? 0;
if (json['kioskQueue'] != null) {
kioskQueueList = List<KioskQueueModel>.from(json['kioskQueue'].map((kioskQueueJson) => KioskQueueModel.fromJson(kioskQueueJson)));
@ -280,43 +277,38 @@ class GlobalConfigurationsModel {
kioskQueueList = [];
}
if (json['kioskConfig'] != null) {
kioskLanguageConfigList = List<KioskLanguageConfigModel>.from(json['kioskConfig'].map((kioskQueueJson) => KioskLanguageConfigModel.fromJson(kioskQueueJson)));
kioskLanguageConfigList =
List<KioskLanguageConfigModel>.from(json['kioskConfig'].map((kioskQueueJson) => KioskLanguageConfigModel.fromJson(kioskQueueJson)));
} else {
kioskLanguageConfigList = [];
}
// Default to false; actual value (based on device IP) is set in ViewModel after loading config
isFromTakhasusiMain = false;
globalClinicPrefixReq = json['globalClinicPrefixReq'] ?? false;
clinicPrefixReq = json['clinicPrefixReq'] ?? true;
vitalSignTextEng = defaultIfNullOrEmpty(json['vitalSignText'], "Vital Sign");
doctorTextEng = defaultIfNullOrEmpty(json['doctorText'], "Doctor");
procedureTextEng = defaultIfNullOrEmpty(json['procedureText'], "Procedure");
vaccinationTextEng = defaultIfNullOrEmpty(json['vaccinationText'], "Vaccination");
nebulizationTextEng = defaultIfNullOrEmpty(json['nebulizationText'], "Nebulization");
callForVitalSignTextEng = defaultIfNullOrEmpty(json['callForVitalSignText'], "Call for Vital Sign");
callForDoctorTextEng = defaultIfNullOrEmpty(json['callForDoctorText'], "Call for Doctor");
callForProcedureTextEng = defaultIfNullOrEmpty(json['callForProcedureText'], "Call for Procedure");
callForVaccinationTextEng = defaultIfNullOrEmpty(json['callForVaccinationText'], "Call for Vaccination");
callForNebulizationTextEng = defaultIfNullOrEmpty(json['callForNebulizationText'], "Call for Nebulization");
vitalSignTextArb = defaultIfNullOrEmpty(json['vitalSignTextAr'], "العلامات الحيوية");
doctorTextArb = defaultIfNullOrEmpty(json['doctorTextAr'], "الطبيب");
procedureTextArb = defaultIfNullOrEmpty(json['procedureTextAr'], "الإجراء");
vaccinationTextArb = defaultIfNullOrEmpty(json['vaccinationTextAr'], "التطعيم");
nebulizationTextArb = defaultIfNullOrEmpty(json['nebulizationTextAr'], "البخاخة");
callForVitalSignTextArb = defaultIfNullOrEmpty(json['callForVitalSignTextAr'], "الرجاء التوجّه إلى غرفة العلامات الحيوية");
callForDoctorTextArb = defaultIfNullOrEmpty(json['callForDoctorTextAr'], "الرجاء التوجّه إلى غرفة الطبيب");
callForProcedureTextArb = defaultIfNullOrEmpty(json['callForProcedureTextAr'], "الرجاء التوجّه إلى غرفة الإجراء");
callForVaccinationTextArb = defaultIfNullOrEmpty(json['callForVaccinationTextAr'], "الرجاء التوجّه إلى غرفة التطعيم");
callForNebulizationTextArb = defaultIfNullOrEmpty(json['callForNebulizationTextAr'], "الرجاء التوجّه إلى غرفة البخاخة");
}
vitalSignTextEng = json['vitalSignText'] ?? "Vital Sign";
doctorTextEng = json['doctorText'] ?? "Doctor";
procedureTextEng = json['procedureText'] ?? "Procedure";
vaccinationTextEng = json['vaccinationText'] ?? "Vaccination";
nebulizationTextEng = json['nebulizationText'] ?? "Nebulization";
callForVitalSignTextEng = json['callForVitalSignText'] ?? "Call for Vital Sign";
callForDoctorTextEng = json['callForDoctorText'] ?? "Call for Doctor";
callForProcedureTextEng = json['callForProcedureText'] ?? "Call for Procedure";
callForVaccinationTextEng = json['callForVaccinationText'] ?? "Call for Vaccination";
callForNebulizationTextEng = json['callForNebulizationText'] ?? "Call for Nebulization";
bool isClinicPrefixAdded(String ticketNo) {
// Check if the ticket has format: "XXX W-XX" where XXX is any 3 letters followed by space and W-
final hasClinicPrefix = RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo);
return hasClinicPrefix;
vitalSignTextArb = json['vitalSignTextAr'] ?? "غرفة العلامات الحيوية";
doctorTextArb = json['doctorTextAr'] ?? " غرفة الطبيب";
procedureTextArb = json['procedureTextAr'] ?? "غرفة الإجراء";
vaccinationTextArb = json['vaccinationTextAr'] ?? "غرفة التطعيم";
nebulizationTextArb = json['nebulizationTextAr'] ?? "غرفة البخاخة";
callForVitalSignTextArb = json['callForVitalSignTextAr'] ?? " غرفة استدعاء للعلامات الحيوية";
callForDoctorTextArb = json['callForDoctorTextAr'] ?? "الرجاء التوجّه إلى غرفة الطبيب";
callForProcedureTextArb = json['callForProcedureTextAr'] ?? "الرجاء التوجّه إلى غرفة الإجراء";
callForVaccinationTextArb = json['callForVaccinationTextAr'] ?? "الرجاء لتوجّه إلى غرفة التطعيم";
callForNebulizationTextArb = json['callForNebulizationTextAr'] ?? "الرجاء التوجّه إلى غرفة البخاخة";
}
@override
String toString() {
return 'GlobalConfigurationsModel{id: $id, isFromTakhasusiMain: $isFromTakhasusiMain, configType: $configType, description: $description, counterStart: $counterStart, counterEnd: $counterEnd, concurrentCallDelaySec: $concurrentCallDelaySec, voiceType: $voiceType, voiceTypeText: $voiceTypeText, screenLanguageEnum: $screenLanguageEnum, screenLanguageText: $screenLanguageText, textDirection: $textDirection, voiceLanguageEnum: $voiceLanguageEnum, voiceLanguageText: $voiceLanguageText, screenMaxDisplayPatients: $screenMaxDisplayPatients, isNotiReq: $isNotiReq, prioritySMS: $prioritySMS, priorityWhatsApp: $priorityWhatsApp, priorityEmail: $priorityEmail, ticketNoText: $ticketNoText, pleaseVisitCounterTextEn: $pleaseVisitCounterTextEn,pleaseVisitCounterTextAr: $pleaseVisitCounterTextAr, roomText: $roomTextEng, roomNo: $roomNo, isRoomNoRequired: $isRoomNoRequired, counterText: $counterTextEng, queueNoText: $queueNoTextEng, callForText: $callForTextEng, currentServeTextArb: $currentServeTextArb,, currentServeTextEng: $currentServeTextEng, maxText: $maxText, minText: $minText, nextPrayerTextEng: $nextPrayerTextEng, nextPrayerTextArb: $nextPrayerTextArb, weatherText: $weatherText, fajarText: $fajarTextEng, dhuhrText: $dhuhrTextEng, asarText: $asarTextEng, maghribText: $maghribTextEng, ishaText: $ishaTextEng, isActive: $isActive, createdBy: $createdBy, createdOn: $createdOn, editedBy: $editedBy, editedOn: $editedOn, isToneReq: $isToneReq, isVoiceReq: $isVoiceReq, orientationTypeEnum: $orientationTypeEnum, isTurnOn: $isTurnOn, waitingAreaType: $waitingAreaType, gender: $gender, isWeatherReq: $isWeatherReq, isPrayerTimeReq: $isPrayerTimeReq, isRssFeedReq: $isRssFeedReq, qTypeEnum: $qTypeEnum, screenTypeEnum: $screenTypeEnum, projectID: $projectID, projectLatitude: $projectLatitude, projectLongitude: $projectLongitude, cityKey: $cityKey, kioskQueueList: $kioskQueueList, kioskLanguageConfigList: $kioskLanguageConfigList, vitalSignText: $vitalSignTextEng, doctorText: $doctorTextEng, procedureText: $procedureTextEng, vaccinationText: $vaccinationTextEng, nebulizationText: $nebulizationTextEng, callForVitalSignText: $callForVitalSignTextEng, callForDoctorText: $callForDoctorTextEng, callForProcedureText: $callForProcedureTextEng, callForVaccinationText: $callForVaccinationTextEng, callForNebulizationText: $callForNebulizationTextEng, vitalSignTextArb: $vitalSignTextArb, doctorTextArb: $doctorTextArb, procedureTextArb: $procedureTextArb, vaccinationTextArb: $vaccinationTextArb, nebulizationTextArb: $nebulizationTextArb, callForVitalSignTextArb: $callForVitalSignTextArb, callForDoctorTextArb: $callForDoctorTextArb, callForProcedureTextArb: $callForProcedureTextArb, callForVaccinationTextArb: $callForVaccinationTextArb, callForNebulizationTextArb: $callForNebulizationTextArb}';
return 'GlobalConfigurationsModel{id: $id, isFromTakhasusiMain: $isFromTakhasusiMain, configType: $configType, description: $description, counterStart: $counterStart, counterEnd: $counterEnd, concurrentCallDelaySec: $concurrentCallDelaySec, voiceType: $voiceType, voiceTypeText: $voiceTypeText, screenLanguageEnum: $screenLanguageEnum, screenLanguageText: $screenLanguageText, textDirection: $textDirection, voiceLanguageEnum: $voiceLanguageEnum, voiceLanguageText: $voiceLanguageText, screenMaxDisplayPatients: $screenMaxDisplayPatients, isNotiReq: $isNotiReq, prioritySMS: $prioritySMS, priorityWhatsApp: $priorityWhatsApp, priorityEmail: $priorityEmail, ticketNoText: $ticketNoText, postVoiceText: $postVoiceText, roomText: $roomTextEng, roomNo: $roomNo, isRoomNoRequired: $isRoomNoRequired, counterText: $counterTextEng, queueNoText: $queueNoTextEng, callForText: $callForTextEng, currentServeTextArb: $currentServeTextArb,, currentServeTextEng: $currentServeTextEng, maxText: $maxText, minText: $minText, nextPrayerTextEng: $nextPrayerTextEng, nextPrayerTextArb: $nextPrayerTextArb, weatherText: $weatherText, fajarText: $fajarTextEng, dhuhrText: $dhuhrTextEng, asarText: $asarTextEng, maghribText: $maghribTextEng, ishaText: $ishaTextEng, isActive: $isActive, createdBy: $createdBy, createdOn: $createdOn, editedBy: $editedBy, editedOn: $editedOn, isToneReq: $isToneReq, isVoiceReq: $isVoiceReq, orientationTypeEnum: $orientationTypeEnum, isTurnOn: $isTurnOn, waitingAreaType: $waitingAreaType, gender: $gender, isWeatherReq: $isWeatherReq, isPrayerTimeReq: $isPrayerTimeReq, isRssFeedReq: $isRssFeedReq, globalClinicPrefixReq: $globalClinicPrefixReq, clinicPrefixReq: $clinicPrefixReq, qTypeEnum: $qTypeEnum, screenTypeEnum: $screenTypeEnum, projectID: $projectID, projectLatitude: $projectLatitude, projectLongitude: $projectLongitude, cityKey: $cityKey, kioskQueueList: $kioskQueueList, kioskLanguageConfigList: $kioskLanguageConfigList, vitalSignText: $vitalSignTextEng, doctorText: $doctorTextEng, procedureText: $procedureTextEng, vaccinationText: $vaccinationTextEng, nebulizationText: $nebulizationTextEng, callForVitalSignText: $callForVitalSignTextEng, callForDoctorText: $callForDoctorTextEng, callForProcedureText: $callForProcedureTextEng, callForVaccinationText: $callForVaccinationTextEng, callForNebulizationText: $callForNebulizationTextEng, vitalSignTextArb: $vitalSignTextArb, doctorTextArb: $doctorTextArb, procedureTextArb: $procedureTextArb, vaccinationTextArb: $vaccinationTextArb, nebulizationTextArb: $nebulizationTextArb, callForVitalSignTextArb: $callForVitalSignTextArb, callForDoctorTextArb: $callForDoctorTextArb, callForProcedureTextArb: $callForProcedureTextArb, callForVaccinationTextArb: $callForVaccinationTextArb, callForNebulizationTextArb: $callForNebulizationTextArb}';
}
}

@ -11,105 +11,104 @@ class TicketDetailsModel {
TicketDetailsModel({this.qTypeEnum, this.screenTypeEnum, this.connectionID, this.ticketModel});
TicketDetailsModel.fromJson(Map<String, dynamic> json) {
qTypeEnum = json['qType'] != null ? (json['qType'] as int).toQTypeEnum() : QTypeEnum.appointment;
screenTypeEnum = json['screenType'] != null ? (json['screenType'] as int).toScreenTypeEnum() : ScreenTypeEnum.waitingAreaScreen;
connectionID = json['connectionID'] ?? '';
qTypeEnum = json['qType'] != null ? (json['qType'] as int).toQTypeEnum() : null;
screenTypeEnum = json['screenType'] != null ? (json['screenType'] as int).toScreenTypeEnum() : null;
connectionID = json['connectionID'];
ticketModel = json['data'] != null
? TicketData.fromJson(
json['data'],
qTypeEnum: qTypeEnum,
qTypeEnum: json['qType'] != null ? (json['qType'] as int).toQTypeEnum() : null,
)
: TicketData(); // Use default empty TicketData if null
: null;
}
}
class TicketData {
int id;
int patientID;
int laBQGroupID;
String queueNo;
int counterBatchNo;
int calledBy;
String calledOn;
String servedOn;
String patientName;
String mobileNo;
String patientEmail;
int preferredLang;
LanguageEnum voiceLanguageEnum;
String ticketNoText;
String postVoiceText;
int patientGender;
String roomNo;
bool isActive;
int createdBy;
int editedBy;
int? id;
int? patientID;
int? laBQGroupID;
String? queueNo;
int? counterBatchNo;
int? calledBy;
String? calledOn;
String? servedOn;
String? patientName;
String? mobileNo;
String? patientEmail;
int? preferredLang;
LanguageEnum voiceLanguageEnum = LanguageEnum.english;
String ticketNoText = "Ticket Number";
String postVoiceText = "Please Visit Counter";
int? patientGender;
String? roomNo;
bool? isActive;
int? createdBy;
int? editedBy;
DateTime? editedOn;
DateTime? createdOn;
// New fields
String doctorNameN;
CallTypeEnum callTypeEnum;
String queueNoM;
String callNoStr;
bool isQueue;
bool isToneReq;
bool isVoiceReq;
int orientationType;
bool isTurnOn;
int concurrentCallDelaySec;
String crTypeAckIP;
int voiceLanguage;
String voiceLanguageText;
String vitalSignText;
String doctorText;
String procedureText;
String vaccinationText;
String nebulizationText;
String callForVitalSignText;
String callForDoctorText;
String callForProcedureText;
String callForVaccinationText;
String callForNebulizationText;
String roomText;
String queueNoText;
String callForText;
String? doctorNameN;
CallTypeEnum callTypeEnum = CallTypeEnum.vitalSign;
String? queueNoM;
String? callNoStr;
bool? isQueue;
bool? isToneReq;
bool? isVoiceReq;
int? orientationType;
bool? isTurnOn;
int? concurrentCallDelaySec;
String? crTypeAckIP;
int voiceLanguage = 1;
String voiceLanguageText = "English";
String vitalSignText = "Vital Sign";
String doctorText = "Doctor";
String procedureText = "Procedure";
String vaccinationText = "Vaccination";
String nebulizationText = "Nebulization";
String callForVitalSignText = "Call for Vital Sign";
String callForDoctorText = "Call for Doctor";
String callForProcedureText = "Call for Procedure";
String callForVaccinationText = "Call for Vaccination";
String callForNebulizationText = "Call for Nebulization";
String roomText = "Room";
String queueNoText = "Counter";
String callForText = "Call For";
TicketData({
this.id = 0,
this.patientID = 0,
this.laBQGroupID = 0,
this.queueNo = "",
this.counterBatchNo = 0,
this.calledBy = 0,
this.calledOn = "",
this.servedOn = "",
this.patientName = "",
this.mobileNo = "",
this.patientEmail = "",
this.preferredLang = 1,
this.id,
this.patientID,
this.laBQGroupID,
this.queueNo,
this.counterBatchNo,
this.calledBy,
this.calledOn,
this.servedOn,
this.patientName,
this.mobileNo,
this.patientEmail,
this.preferredLang,
this.voiceLanguageEnum = LanguageEnum.english,
this.ticketNoText = "Ticket Number",
this.postVoiceText = "Please Visit Counter",
this.patientGender = 1,
this.roomNo = "",
this.isActive = true,
this.createdBy = 0,
this.createdOn, // will fallback in fromJson (see below)
this.editedBy = 0,
this.editedOn, // will fallback in fromJson (see below)
this.doctorNameN = "",
this.callTypeEnum = CallTypeEnum.none,
this.queueNoM = "",
this.callNoStr = "",
this.isQueue = false,
this.isToneReq = false,
this.isVoiceReq = false,
this.orientationType = 1,
this.isTurnOn = true,
this.concurrentCallDelaySec = 1,
this.crTypeAckIP = "",
this.voiceLanguage = 1,
this.patientGender,
this.roomNo,
this.isActive,
this.createdBy,
this.createdOn,
this.editedBy,
this.editedOn,
this.doctorNameN,
this.callTypeEnum = CallTypeEnum.vitalSign,
this.queueNoM,
this.callNoStr,
this.isQueue,
this.isToneReq,
this.isVoiceReq,
this.orientationType,
this.isTurnOn,
this.concurrentCallDelaySec,
this.crTypeAckIP,
this.voiceLanguageText = "English",
this.vitalSignText = "Vital Sign",
this.doctorText = "Doctor",
@ -126,61 +125,63 @@ class TicketData {
this.callForText = "Call For",
});
TicketData.fromJson(Map<String, dynamic> json, {QTypeEnum? qTypeEnum})
: id = json['id'] ?? 0,
patientID = json['patientID'] ?? 0,
laBQGroupID = json['laB_QGroupID'] ?? 0,
queueNo = json['queueNoM'] ?? "",
counterBatchNo = json['counterBatchNo'] ?? 0,
calledBy = json['calledBy'] ?? 0,
calledOn = json['calledOn'] ?? "",
servedOn = json['servedOn'] ?? "",
patientName = json['patientName'] ?? "",
mobileNo = json['mobileNo'] ?? "",
patientEmail = json['patientEmail'] ?? "",
preferredLang = (json['preferredLang'] != null && json['preferredLang'].toString().trim() != "") ? int.parse(json['preferredLang'].toString()) : 1,
voiceLanguageEnum = (json['preferredLang'] != null && json['preferredLang'].toString().trim() != "") ? (int.parse(json['preferredLang'].toString())).toLanguageEnum() : LanguageEnum.english,
ticketNoText = json['ticketNoText'] ?? "Ticket Number",
postVoiceText = json['pleaseVisitCounterText'] ?? "Please Visit Counter",
patientGender = json['patientGender'] ?? 1,
roomNo = (() {
if (qTypeEnum != null && qTypeEnum == QTypeEnum.general) {
return json['counterNo']?.toString() ?? "";
}
return json['roomNo']?.toString() ?? "";
})(),
isActive = json['isActive'] ?? true,
createdBy = json['createdBy'] ?? 0,
editedBy = json['editedBy'] ?? 0,
createdOn = json['createdOn'] != null ? (json['createdOn'] as String).toDateTime() : DateTime.now(),
editedOn = json['editedOn'] != null ? (json['editedOn'] as String).toDateTime() : DateTime.now(),
doctorNameN = json['doctorNameN'] ?? "",
callTypeEnum = ((json['callType'] ?? 0) as int).toCallTypeEnum(),
queueNoM = json['queueNoM'] ?? "",
callNoStr = json['callNoStr'] ?? "",
isQueue = json['isQueue'] ?? false,
isToneReq = json['isToneReq'] ?? false,
isVoiceReq = json['isVoiceReq'] ?? false,
orientationType = json['orientationType'] ?? 1,
isTurnOn = json['isTurnOn'] ?? true,
concurrentCallDelaySec = json['concurrentCallDelaySec'] ?? 1,
crTypeAckIP = json['crTypeAckIP'] ?? "",
voiceLanguage = json['voiceLanguage'] ?? 1,
voiceLanguageText = json['voiceLanguageText'] ?? "English",
vitalSignText = json['vitalSignText'] ?? "Vital Sign",
doctorText = json['doctorText'] ?? "Doctor",
procedureText = json['procedureText'] ?? "Procedure",
vaccinationText = json['vaccinationText'] ?? "Vaccination",
nebulizationText = json['nebulizationText'] ?? "Nebulization",
callForVitalSignText = json['callForVitalSignText'] ?? "Call for Vital Sign",
callForDoctorText = json['callForDoctorText'] ?? "Call for Doctor",
callForProcedureText = json['callForProcedureText'] ?? "Call for Procedure",
callForVaccinationText = json['callForVaccinationText'] ?? "Call for Vaccination",
callForNebulizationText = json['callForNebulizationText'] ?? "Call for Nebulization",
roomText = json['roomText'] ?? "Room",
queueNoText = json['queueNoText'] ?? "Counter",
callForText = json['callForText'] ?? "Call For";
TicketData.fromJson(Map<String, dynamic> json, {QTypeEnum? qTypeEnum}) {
id = json['id'];
patientID = json['patientID'];
laBQGroupID = json['laB_QGroupID'];
queueNo = json['queueNoM'];
counterBatchNo = json['counterBatchNo'];
calledBy = json['calledBy'];
calledOn = json['calledOn'];
servedOn = json['servedOn'];
patientName = json['patientName'];
mobileNo = json['mobileNo'];
patientEmail = json['patientEmail'];
preferredLang = (json['preferredLang'] != null && json['preferredLang'].toString().trim() != "") ? int.parse(json['preferredLang'].toString()) : 1;
voiceLanguageEnum = (json['preferredLang'] != null && json['preferredLang'].toString().trim() != "") ? (int.parse(json['preferredLang'].toString())).toLanguageEnum() : LanguageEnum.english;
ticketNoText = json['ticketNoText'] ?? "Ticket Number";
postVoiceText = json['pleaseVisitCounterText'] ?? "Please Visit Counter";
patientGender = json['patientGender'] ?? 1;
roomNo = json['roomNo']?.toString();
if (qTypeEnum != null && qTypeEnum == QTypeEnum.general) {
roomNo = json['counterNo']?.toString();
}
isActive = json['isActive'];
createdBy = json['createdBy'];
editedBy = json['editedBy'];
editedOn = json['editedOn'] != null ? (json['editedOn'] as String).toDateTime() : DateTime.now();
createdOn = json['createdOn'] != null ? (json['createdOn'] as String).toDateTime() : DateTime.now();
doctorNameN = json['doctorNameN'];
callTypeEnum = ((json['callType'] ?? 1) as int).toCallTypeEnum();
queueNoM = json['queueNoM'];
callNoStr = json['callNoStr'];
isQueue = json['isQueue'];
isToneReq = json['isToneReq'];
isVoiceReq = json['isVoiceReq'];
orientationType = json['orientationType'];
isTurnOn = json['isTurnOn'];
concurrentCallDelaySec = json['concurrentCallDelaySec'];
crTypeAckIP = json['crTypeAckIP'];
voiceLanguage = json['voiceLanguage'] ?? 1;
voiceLanguageText = json['voiceLanguageText'] ?? "English";
vitalSignText = json['vitalSignText'];
doctorText = json['doctorText'];
procedureText = json['procedureText'];
vaccinationText = json['vaccinationText'];
nebulizationText = json['nebulizationText'];
callForVitalSignText = json['callForVitalSignText'];
callForDoctorText = json['callForDoctorText'];
callForProcedureText = json['callForProcedureText'];
callForVaccinationText = json['callForVaccinationText'];
callForNebulizationText = json['callForNebulizationText'];
roomText = json['roomText'];
queueNoText = json['queueNoText'];
callForText = json['callForText'];
}
@override
String toString() => 'TicketData{id: $id, patientID: $patientID, laBQGroupID: $laBQGroupID, queueNo: $queueNo, ...}';
String toString() {
return 'TicketData{id: $id, patientID: $patientID, laBQGroupID: $laBQGroupID, queueNo: $queueNo, counterBatchNo: $counterBatchNo, calledBy: $calledBy, calledOn: $calledOn, servedOn: $servedOn, patientName: $patientName, mobileNo: $mobileNo, patientEmail: $patientEmail, preferredLang: $preferredLang, voiceLanguageEnum: $voiceLanguageEnum, ticketNoText: $ticketNoText, postVoiceText: $postVoiceText, patientGender: $patientGender, roomNo: $roomNo, isActive: $isActive, createdBy: $createdBy, editedBy: $editedBy, editedOn: $editedOn, createdOn: $createdOn, doctorNameN: $doctorNameN, callTypeEnum: $callTypeEnum, queueNoM: $queueNoM, callNoStr: $callNoStr, isQueue: $isQueue, isToneReq: $isToneReq, isVoiceReq: $isVoiceReq, orientationType: $orientationType, isTurnOn: $isTurnOn, concurrentCallDelaySec: $concurrentCallDelaySec, crTypeAckIP: $crTypeAckIP, voiceLanguage: $voiceLanguage, voiceLanguageText: $voiceLanguageText, vitalSignText: $vitalSignText, doctorText: $doctorText, procedureText: $procedureText, vaccinationText: $vaccinationText, nebulizationText: $nebulizationText, callForVitalSignText: $callForVitalSignText, callForDoctorText: $callForDoctorText, callForProcedureText: $callForProcedureText, callForVaccinationText: $callForVaccinationText, callForNebulizationText: $callForNebulizationText, roomText: $roomText, queueNoText: $queueNoText, callForText: $callForText}';
}
}

@ -1,3 +1,5 @@
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';
@ -37,7 +39,7 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
@override
Future<GlobalConfigurationsModel?> getGlobalScreenConfigurations({required String ipAddress}) async {
try {
// try {
var params = {
"ipAddress": ipAddress.toString(),
"apiKey": AppConstants.apiKey.toString(),
@ -47,18 +49,24 @@ 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) {
loggerService.logError(e.toString());
loggerService.logToFile(message: e.toString(), source: "getGlobalScreenConfigurations-> screen_details_repo.dart", type: LogTypeEnum.error);
InfoComponents.showToast(e.toString());
return null;
}
// } catch (e) {
// log("record:");
// log(e.toString());
// loggerService.logError(e.toString());
// loggerService.logToFile(message: e.toString(), source: "getGlobalScreenConfigurations-> screen_details_repo.dart", type: LogTypeEnum.error);
// InfoComponents.showToast(e.toString());
// return null;
// }
}
@override
@ -105,7 +113,8 @@ 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) {
@ -142,9 +151,11 @@ 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;
@ -166,9 +177,11 @@ 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;
}
@ -193,7 +206,8 @@ 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;
}
@ -229,7 +243,8 @@ 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(),

@ -48,11 +48,10 @@ class SignalrRepoImp implements SignalrRepo {
client: IOClient(HttpClient()..badCertificateCallback = (x, y, z) => true),
logging: (level, message) => log(message),
))
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Custom retry delays
.withAutomaticReconnect()
.build();
connection!.serverTimeoutInMilliseconds = 120000; // 2 minutes
connection!.keepAliveIntervalInMilliseconds = 15000; // 15 seconds keep-alive
connection!.serverTimeoutInMilliseconds = 120000;
int reconnectAttempts = 0;
const int maxReconnectAttempts = 10;
const Duration reconnectDelay = Duration(seconds: 5);

@ -1,4 +1,5 @@
import 'dart:developer';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/models/global_config_model.dart';
@ -6,7 +7,6 @@ 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:hmg_qline/utilities/extensions.dart';
import 'package:logger/logger.dart';
abstract class TextToSpeechService {
Future<void> speechText({
@ -15,8 +15,6 @@ abstract class TextToSpeechService {
bool isMute = false,
});
// Future<void> speechTextTest(TicketData ticket);
void listenToTextToSpeechEvents({required Function() onVoiceCompleted});
}
@ -30,122 +28,6 @@ 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,
@ -169,7 +51,16 @@ class TextToSpeechServiceImp implements TextToSpeechService {
textToSpeechInstance.setVolume(1.0);
}
textToSpeechInstance.setSpeechRate(0.4);
if (isAndroid14) {
if (langEnum == LanguageEnum.arabic) {
textToSpeechInstance.setSpeechRate(0.5);
} else {
textToSpeechInstance.setSpeechRate(0.4);
}
textToSpeechInstance.setPitch(0.9);
} else {
textToSpeechInstance.setSpeechRate(0.4);
}
if (langEnum == LanguageEnum.arabic) {
try {
await textToSpeechInstance.setLanguage(LanguageEnum.arabic.enumToString());
@ -191,7 +82,9 @@ class TextToSpeechServiceImp implements TextToSpeechService {
postVoice = ticket.ticketModel!.postVoiceText;
}
String roomNo = '';
if (globalConfigurationsModel.qTypeEnum != QTypeEnum.appointment && ticket.ticketModel!.roomNo != null && ticket.ticketModel!.roomNo!.isNotEmpty) {
if (globalConfigurationsModel.qTypeEnum != QTypeEnum.appointment &&
ticket.ticketModel!.roomNo != null &&
ticket.ticketModel!.roomNo!.isNotEmpty) {
roomNo = ".. ${ticket.ticketModel!.roomNo.toString()}";
}

@ -47,7 +47,7 @@ extension NavigationExt on BuildContext {
}
extension ScreenOrientationExt on ScreenOrientationEnum {
int getTurnsByOrientationForNewVersions() {
int getTurnsByOrientation() {
switch (this) {
case ScreenOrientationEnum.portraitUp:
return 1;
@ -176,10 +176,9 @@ extension XCallType on CallTypeEnum {
return AppColors.vaccinationColor;
} else if (this == CallTypeEnum.nebulization) {
return AppColors.nebulizationColor;
} else if (this == CallTypeEnum.none) {
return AppColors.newDoctorColor;
} else {
return Colors.black54;
}
return AppColors.newDoctorColor;
}
String getMessageByCallTypeForEnglish(GlobalConfigurationsModel globalConfig, {bool isListView = false}) {
@ -195,7 +194,7 @@ extension XCallType on CallTypeEnum {
case CallTypeEnum.nebulization:
return !isListView ? globalConfig.callForNebulizationTextEng : globalConfig.nebulizationTextEng;
case CallTypeEnum.none:
return !isListView ? (globalConfig.pleaseVisitCounterTextEn ?? '') : (globalConfig.counterTextEng ?? '');
return !isListView ? globalConfig.callForVitalSignTextEng : globalConfig.vitalSignTextEng;
}
}
@ -212,15 +211,11 @@ extension XCallType on CallTypeEnum {
case CallTypeEnum.nebulization:
return !isListView ? globalConfig.callForNebulizationTextArb : globalConfig.nebulizationTextArb;
case CallTypeEnum.none:
return !isListView ? (globalConfig.pleaseVisitCounterTextAr ?? '') : (globalConfig.counterTextArb ?? '');
return !isListView ? globalConfig.callForVitalSignTextArb : globalConfig.vitalSignTextArb;
}
}
Widget getIconByCallType(double height, QTypeEnum qType, {double? width, BoxFit fit = BoxFit.contain}) {
if (this == CallTypeEnum.vitalSign) {
return const SizedBox.shrink();
}
SvgPicture getIconByCallType(double height, {double? width, BoxFit fit = BoxFit.contain}) {
String iconPath = "";
if (this == CallTypeEnum.vitalSign) {
iconPath = AppAssets.newVitalSignIcon;
@ -234,10 +229,6 @@ extension XCallType on CallTypeEnum {
iconPath = AppAssets.nebulizationIcon;
}
if (qType == QTypeEnum.lab) {
iconPath = AppAssets.labIcon;
}
return SvgPicture.asset(
iconPath.isEmpty ? "assets/images/wait.svg" : iconPath,
height: height,
@ -260,7 +251,7 @@ extension XCallType on CallTypeEnum {
case CallTypeEnum.nebulization:
return 5;
case CallTypeEnum.none:
return 0;
return 1;
}
}
@ -277,7 +268,7 @@ extension XCallType on CallTypeEnum {
case CallTypeEnum.nebulization:
return AppColors.gradientBorderComboForVitalSigns;
case CallTypeEnum.none:
return AppColors.gradientBorderComboForDoctor;
return AppColors.gradientBorderComboForVitalSigns;
}
}
@ -293,8 +284,8 @@ extension XCallType on CallTypeEnum {
return ticket.callForVaccinationText;
case CallTypeEnum.nebulization:
return ticket.callForNebulizationText;
case CallTypeEnum.none:
return '';
default:
return ticket.callForVitalSignText;
}
}
}
@ -306,7 +297,7 @@ extension XCallTypeInt on int {
if (this == 3) return CallTypeEnum.procedure;
if (this == 4) return CallTypeEnum.vaccination;
if (this == 5) return CallTypeEnum.nebulization;
return CallTypeEnum.none;
return CallTypeEnum.vitalSign;
}
}

@ -12,6 +12,19 @@ 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 {
@ -92,4 +105,85 @@ 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 {
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,
);
}
}
}

@ -94,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");
if (response != null && response.isNotEmpty) {
TicketDetailsModel ticketDetailsModel = TicketDetailsModel.fromJson(response.first as Map<String, dynamic>);
addNewTicket(ticketDetailsModel);
@ -215,9 +215,7 @@ class QueuingViewModel extends ChangeNotifier {
callTypeEnum: ticketData.callTypeEnum,
);
} else {
screenConfigViewModel.acknowledgeTicket(ticketQueueID: ticketData.id ?? 0,
ipAddress: screenConfigViewModel.currentScreenIP,
);
screenConfigViewModel.acknowledgeTicket(ticketQueueID: ticketData.id ?? 0);
}
log("globalConfigurationsModel: ${globalConfigurationsModel.toString()}");

@ -1,5 +1,6 @@
import 'dart:developer';
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hmg_qline/config/dependency_injection.dart';
@ -23,7 +24,6 @@ import 'package:hmg_qline/utilities/native_method_handler.dart';
import 'package:hmg_qline/view_models/queuing_view_model.dart';
import 'package:hmg_qline/views/view_helpers/info_components.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
// import 'package:timezone/browser.dart' as tz;
class ScreenConfigViewModel extends ChangeNotifier {
@ -42,7 +42,6 @@ class ScreenConfigViewModel extends ChangeNotifier {
});
Future<void> initializeScreenConfigVM() async {
appStartTime = DateTime.now(); // Record when app actually started
await getGlobalConfigurationsByIP();
await getLastTimeUpdatedFromCache();
await getLastTimeLogsClearedFromCache();
@ -51,40 +50,20 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
Future<void> onAppResumed() async {
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
loggerService.logToFile(
message: "[didChangeAppLifecycleState] : [onAppResumed] - App uptime: ${uptimeHours}h",
source: "onAppResumed -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
// Re-enable wakelock when resuming
try {
await WakelockPlus.enable();
} catch (e) {
loggerService.logError("Failed to enable wakelock on resume: $e");
}
// Verify connections are still alive
syncHubConnectionState();
message: "[didChangeAppLifecycleState] : [onAppResumed]", source: "onAppResumed -> screen_config_view_model.dart", type: LogTypeEnum.data);
}
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!",
source: "onAppPaused -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
message: "[didChangeAppLifecycleState] : [onAppPaused]", source: "onAppPaused -> screen_config_view_model.dart", type: LogTypeEnum.data);
// nativeMethodChannelService.restartApp();
}
Future<void> onAppDetached() async {
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
loggerService.logToFile(
message: "[didChangeAppLifecycleState] : [onAppDetached] - App uptime: ${uptimeHours}h - CRITICAL: App being killed!",
source: "onAppDetached -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
message: "[didChangeAppLifecycleState] : [onAppDetached]", source: "onAppDetached -> screen_config_view_model.dart", type: LogTypeEnum.data);
// nativeMethodChannelService.restartApp();
}
Future<void> waitForIPAndInitializeConfigVM() async {
@ -181,21 +160,10 @@ class ScreenConfigViewModel extends ChangeNotifier {
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) {
// IPs that belong to Takhasusi main - using AppConstants
// IPs that belong to Takhasusi main
const takhasusiMainIPs = ['10.70.194.105', '10.70.194.87'];
try {
value.isFromTakhasusiMain = AppConstants.takhasusiMainBranchIps.contains(currentScreenIP);
// Debug logging for Takhasusi Main detection
log("=== TAKHASUSI MAIN CHECK ===");
log("Current Screen IP: $currentScreenIP");
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 getTurnsByOrientation()");
}
log("============================");
value.isFromTakhasusiMain = takhasusiMainIPs.contains(currentScreenIP);
} catch (_) {
value.isFromTakhasusiMain = false;
}
@ -265,7 +233,9 @@ 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(),
cityId:
((globalConfigurationsModel.cityKey == null || globalConfigurationsModel.cityKey == 0) ? testCityKey : globalConfigurationsModel.cityKey)
.toString(),
);
if (response == null) {
@ -350,23 +320,21 @@ class ScreenConfigViewModel extends ChangeNotifier {
int counter = 0;
DateTime lastChecked = DateTime.now();
DateTime appStartTime = DateTime.now(); // Track actual app start time
Timer? _midnightTimer;
Timer? _healthCheckTimer;
int healthCheckFailures = 0; // Track consecutive failures
Future<void> initializeTimer() async {
// Cancel any existing timer to avoid multiple timers running
_midnightTimer?.cancel();
_healthCheckTimer?.cancel();
if (!(globalConfigurationsModel.isWeatherReq) && !(globalConfigurationsModel.isPrayerTimeReq) && !(globalConfigurationsModel.isRssFeedReq)) {
// Even if widgets are not required, start a minimal health check
_startHealthCheckTimer();
return;
}
// Start the main periodic timer
// Only start timer if not already running
if (_midnightTimer != null) {
return;
}
_midnightTimer = Timer.periodic(
const Duration(minutes: 10),
(timer) async {
@ -374,7 +342,7 @@ class ScreenConfigViewModel extends ChangeNotifier {
DateTime now = DateTime.now();
log("counterValue: $counter");
if (globalConfigurationsModel.id == null) {
if (globalConfigurationsModel.id == null || state == ViewState.error) {
await getGlobalConfigurationsByIP();
}
@ -390,6 +358,16 @@ class ScreenConfigViewModel extends ChangeNotifier {
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 (globalConfigurationsModel.isRssFeedReq) {
// await getRssFeedDetailsFromServer();
// }
// if (globalConfigurationsModel.isWeatherReq) {
// await getWeatherDetailsFromServer();
// }
// if (globalConfigurationsModel.isPrayerTimeReq) {
// await getPrayerDetailsFromServer();
// }
lastChecked = now;
}
}
@ -397,97 +375,11 @@ class ScreenConfigViewModel extends ChangeNotifier {
syncHubConnectionState();
},
);
// Start health check timer
_startHealthCheckTimer();
}
void _startHealthCheckTimer() {
// Health check every 5 minutes to keep app active and check connections
_healthCheckTimer = Timer.periodic(
const Duration(minutes: 5),
(timer) async {
try {
final now = DateTime.now();
final actualUptime = now.difference(appStartTime);
final uptimeHours = actualUptime.inHours;
final uptimeMinutes = actualUptime.inMinutes % 60;
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)})",
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
// 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",
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
}
// Sync hub connection state
try {
syncHubConnectionState();
healthCheckFailures = 0; // Reset on success
} catch (e) {
healthCheckFailures++;
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",
type: LogTypeEnum.data,
);
QueuingViewModel queuingViewModel = getIt.get<QueuingViewModel>();
await queuingViewModel.stopHubConnection();
await Future.delayed(const Duration(seconds: 2));
await queuingViewModel.startHubConnection();
}
}
// Ensure wakelock is still enabled
try {
await WakelockPlus.enable();
} catch (e) {
loggerService.logError("Failed to enable wakelock: $e");
}
// Log memory/performance indicators
loggerService.logToFile(
message: "Health check - Hub connected: $isHubConnected, Internet: $isInternetConnected",
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
type: LogTypeEnum.connectivity,
);
// Trigger a small notifyListeners to keep UI thread active
notifyListeners();
} catch (e, stackTrace) {
healthCheckFailures++;
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",
source: "_startHealthCheckTimer -> screen_config_view_model.dart",
type: LogTypeEnum.data,
);
}
}
},
);
}
@override
void dispose() {
_midnightTimer?.cancel();
_healthCheckTimer?.cancel();
patientIdController.dispose();
super.dispose();
@ -507,7 +399,9 @@ 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());
}
}
@ -591,9 +485,9 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
}
Future<void> acknowledgeTicket({required int ticketQueueID, required String ipAddress}) async {
Future<void> acknowledgeTicket({required int ticketQueueID}) async {
GenericRespModel? response = await screenDetailsRepo.acknowledgeTicket(
ipAddress: ipAddress,
ipAddress: currentScreenIP,
ticketQueueID: ticketQueueID,
qTypeEnum: globalConfigurationsModel.qTypeEnum,
);
@ -605,7 +499,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
}
Future<void> acknowledgeTicketForAppointmentOnly({required int ticketQueueID, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
Future<void> acknowledgeTicketForAppointmentOnly(
{required int ticketQueueID, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
GenericRespModel? response = await screenDetailsRepo.acknowledgeTicketForAppointment(
ticketId: ticketQueueID,
ipAddress: ipAddress,

@ -1,6 +1,9 @@
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';
@ -309,11 +312,16 @@ class _AppFooterState extends State<AppFooter> {
Padding(
padding: EdgeInsets.only(top: SizeConfig.getHeightMultiplier()! * 0.1),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
AppText(
AppStrings.poweredBy,
fontSize: SizeConfig.getWidthMultiplier()! * 1.5,
fontWeight: FontWeight.w400,
color: AppColors.darkGreyTextColor,
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(
"v${screenConfigVM.currentScreenIP.replaceAll(".", "-")}(${AppConstants.currentBuildVersion})",

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hmg_qline/constants/app_constants.dart';
@ -130,7 +128,7 @@ Widget counterNoText({required int counterNo, required bool isRoomNoRequired, re
fontFamily: AppStrings.fontNamePoppins,
textAlign: TextAlign.center,
fontWeight: FontWeight.bold,
color: isRoomNoRequired ? AppColors.greyTextColor : Colors.transparent,
color: isRoomNoRequired ? Colors.black : Colors.transparent,
fontSize: SizeConfig.getWidthMultiplier() * 8,
);
}

@ -1,19 +1,14 @@
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: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';
import 'package:provider/provider.dart';
class AppHeader extends StatelessWidget implements PreferredSizeWidget {
const AppHeader({super.key});
@ -34,8 +29,10 @@ 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(

@ -293,7 +293,7 @@ class KioskMainScreen extends StatelessWidget {
selector: (context, screenConfigViewModel) => screenConfigViewModel.globalConfigurationsModel.orientationTypeEnum,
builder: (BuildContext context, ScreenOrientationEnum screenOrientationEnum, Widget? child) {
return RotatedBox(
quarterTurns: screenOrientationEnum.getTurnsByOrientationForNewVersions(),
quarterTurns: screenOrientationEnum.getTurnsByOrientation(),
child: AppScaffold(
resizeToAvoidBottomInset: false,
appBar: const AppHeader(),

@ -3,12 +3,10 @@ import 'package:flutter/material.dart';
import 'package:hmg_qline/models/global_config_model.dart';
import 'package:hmg_qline/models/ticket_model.dart';
import 'package:hmg_qline/utilities/enums.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/main_queue_screen/components/ticket_item_calling_card.dart';
import 'package:hmg_qline/views/main_queue_screen/components/ticket_item_normal_card.dart';
import 'package:hmg_qline/views/view_helpers/size_config.dart';
import 'package:provider/provider.dart';
class PriorityTickets extends StatelessWidget {
final List<TicketDetailsModel> tickets;
@ -95,10 +93,7 @@ class PriorityTickets extends StatelessWidget {
}
Widget _buildPrimaryTicket(BuildContext context, TicketDetailsModel ticket, {bool isFullWidth = false, bool isHalf = false}) {
final screenConfigViewModel = context.read<ScreenConfigViewModel>();
Widget primaryCallingCard = QueueItemCallingCard(
qTypeEnum: screenConfigViewModel.currentQTypeEnum,
isGradientRequired: true,
isBorderRequired: true,
isSingleTicket: isFullWidth,
@ -108,8 +103,7 @@ class PriorityTickets extends StatelessWidget {
roomNo: ticket.ticketModel?.roomNo ?? '',
roomText: _getRoomText(),
roomTextAr: _getRoomTextAr(),
isClinicAdded: false,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.none,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.vitalSign,
textDirection: globalConfigurationsModel.textDirection,
screenTypeEnum: globalConfigurationsModel.screenTypeEnum,
langTypeEnum: globalConfigurationsModel.screenLanguageEnum,
@ -136,7 +130,7 @@ class PriorityTickets extends StatelessWidget {
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.6, // 60% of screen width
minHeight: SizeConfig.getHeightMultiplier() * 2, // Minimum height
minHeight: SizeConfig.getHeightMultiplier() * 2.5, // Minimum height
),
child: Transform.scale(
scale: _getTicketScale() + 0.2,
@ -150,15 +144,12 @@ class PriorityTickets extends StatelessWidget {
}
Widget _buildSecondaryTicket(BuildContext context, TicketDetailsModel ticket, {EdgeInsets? margin, bool isHalf = false}) {
final screenConfigViewModel = context.read<ScreenConfigViewModel>();
Widget secondaryCallingCard = QueueItemNormalCard(
qTypeEnum: screenConfigViewModel.currentQTypeEnum,
ticketNo: ticket.ticketModel?.queueNo ?? '',
roomNo: ticket.ticketModel?.roomNo ?? '',
roomText: _getRoomText(),
roomTextAr: _getRoomTextAr(),
isClinicAdded: false,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.none,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.vitalSign,
textDirection: globalConfigurationsModel.textDirection,
screenTypeEnum: globalConfigurationsModel.screenTypeEnum,
langTypeEnum: globalConfigurationsModel.screenLanguageEnum,
@ -194,7 +185,7 @@ class PriorityTickets extends StatelessWidget {
// Helper methods to reduce repetition
double _getTicketScale() {
return globalConfigurationsModel.screenTypeEnum == ScreenTypeEnum.roomLevelScreen
? 1.5
? 2.0
: globalConfigurationsModel.isFromTakhasusiMain
? 0.8
: 1.2;

@ -48,62 +48,62 @@ class PriorityTicketsWithSideSection extends StatelessWidget {
),
),
),
// if (globalConfigurationsModel.qTypeEnum == QTypeEnum.appointment) ...[
Expanded(
flex: 8,
child: SizedBox(
height: SizeConfig.getHeightMultiplier() * 0.25,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ticketModel.callTypeEnum.getIconByCallType(SizeConfig.getHeightMultiplier() * 0.2, globalConfigurationsModel.qTypeEnum),
],
if (globalConfigurationsModel.qTypeEnum == QTypeEnum.appointment) ...[
Expanded(
flex: 8,
child: SizedBox(
height: SizeConfig.getHeightMultiplier() * 0.25,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ticketModel.callTypeEnum.getIconByCallType(SizeConfig.getHeightMultiplier() * 0.2),
],
),
),
),
Expanded(
flex: 9,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: AppText(
callMessageAr,
color: ticketModel.callTypeEnum.getColorByCallType(),
fontSize: SizeConfig.getWidthMultiplier() * 1.8,
fontWeight: FontWeight.bold,
fontFamily: AppStrings.fontNameGesTwo,
fontHeight: 1,
textOverflow: TextOverflow.clip,
maxLines: 1,
),
),
SizedBox(width: SizeConfig.getWidthMultiplier()),
if (callMessageEng.isNotEmpty) ...[
Expanded(
flex: 9,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
flex: 2,
flex: 3,
child: AppText(
"($callMessageEng)",
callMessageAr,
color: ticketModel.callTypeEnum.getColorByCallType(),
fontSize: SizeConfig.getWidthMultiplier() * 1.8,
fontFamily: AppStrings.fontNamePoppins,
fontWeight: FontWeight.bold,
fontFamily: AppStrings.fontNameGesTwo,
fontHeight: 1,
textOverflow: TextOverflow.clip,
maxLines: 1,
),
),
]
],
SizedBox(width: SizeConfig.getWidthMultiplier()),
if (callMessageEng.isNotEmpty) ...[
Expanded(
flex: 3,
child: AppText(
"($callMessageEng)",
color: ticketModel.callTypeEnum.getColorByCallType(),
fontSize: SizeConfig.getWidthMultiplier() * 1.8,
fontFamily: AppStrings.fontNamePoppins,
fontHeight: 1,
),
),
]
],
),
),
),
],
],
),
),
),
),
// ],
],
Expanded(
flex: 2,
child: Center(
@ -124,6 +124,8 @@ class PriorityTicketsWithSideSection extends StatelessWidget {
);
}
@override
Widget build(BuildContext context) {
List<TicketDetailsModel> priorityTickets = [];
@ -168,7 +170,7 @@ class PriorityTicketsWithSideSection extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 3,
flex: 4,
child: engArabicTextWithSeparatorWidget(
englishText: globalConfigurationsModel.queueNoTextEng ?? "",
arabicText: globalConfigurationsModel.queueNoTextArb ?? "",

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:blinking_text/blinking_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
@ -14,7 +16,6 @@ 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;
@ -24,12 +25,10 @@ class QueueItemCallingCard extends StatelessWidget {
final CallTypeEnum callTypeEnum;
final ScreenTypeEnum screenTypeEnum;
final LanguageEnum langTypeEnum;
final QTypeEnum qTypeEnum;
final bool isSingleTicket;
const QueueItemCallingCard({
super.key,
required this.isClinicAdded,
required this.ticketNo,
required this.roomNo,
required this.scale,
@ -40,28 +39,22 @@ class QueueItemCallingCard extends StatelessWidget {
required this.callTypeEnum,
required this.screenTypeEnum,
required this.langTypeEnum,
required this.qTypeEnum,
this.isGradientRequired = false,
this.isBorderRequired = false,
this.isSingleTicket = false,
this.blink = false,
});
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;
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;
}
@override
Widget build(BuildContext context) {
final text = callTypeEnum.getMessageByCallTypeForEnglish(globalConfigurationsModel, isListView: false) + (qTypeEnum == QTypeEnum.appointment ? ("| $roomText $roomNo") : "");
final text = "${callTypeEnum.getMessageByCallTypeForEnglish(globalConfigurationsModel, isListView: false)} | $roomText $roomNo";
return Stack(
children: [
customShadowSmoothContainerWithBackground(
@ -84,7 +77,7 @@ class QueueItemCallingCard extends StatelessWidget {
left: textDirection == TextDirection.rtl ? SizeConfig.getWidthMultiplier() * 3.5 : 0,
right: textDirection == TextDirection.ltr ? SizeConfig.getWidthMultiplier() * 3.5 : 0,
),
child: callTypeEnum.getIconByCallType(SizeConfig.getHeightMultiplier() * 0.5, qTypeEnum),
child: callTypeEnum.getIconByCallType(SizeConfig.getHeightMultiplier() * 0.5),
),
SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.15),
IntrinsicWidth(
@ -109,8 +102,8 @@ class QueueItemCallingCard extends StatelessWidget {
top: SizeConfig.getHeightMultiplier() * 0.25,
),
child: AppText(
getFormattedTicket(ticketNo, isClinicAdded),
fontSize: SizeConfig.getWidthMultiplier() * 7.4,
ticketNo,
fontSize: shouldReduceSize(ticketNo) ? SizeConfig.getWidthMultiplier() * 6 : SizeConfig.getWidthMultiplier() * 7.4,
letterSpacing: -1,
fontHeight: 0.5,
color: AppColors.greyTextColor,

@ -11,21 +11,18 @@ 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;
final GlobalConfigurationsModel globalConfigurationsModel;
final CallTypeEnum callTypeEnum;
final ScreenTypeEnum screenTypeEnum;
final QTypeEnum qTypeEnum;
final LanguageEnum langTypeEnum;
final double? height;
final double? width;
const QueueItemNormalCard({
super.key,
required this.isClinicAdded,
required this.ticketNo,
required this.roomNo,
required this.textDirection,
@ -35,27 +32,20 @@ class QueueItemNormalCard extends StatelessWidget {
required this.callTypeEnum,
required this.screenTypeEnum,
required this.langTypeEnum,
required this.qTypeEnum,
this.height,
this.width,
});
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;
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;
}
@override
Widget build(BuildContext context) {
final text = callTypeEnum.getMessageByCallTypeForEnglish(globalConfigurationsModel, isListView: false) + (qTypeEnum == QTypeEnum.appointment ? ("| $roomText $roomNo") : "");
final text = "${callTypeEnum.getMessageByCallTypeForEnglish(globalConfigurationsModel, isListView: false)} | $roomText $roomNo";
return Stack(
children: [
customShadowSmoothContainer(
@ -76,8 +66,8 @@ class QueueItemNormalCard extends StatelessWidget {
flex: 3,
child: Center(
child: AppText(
getFormattedTicket(ticketNo, isClinicAdded),
fontSize: SizeConfig.getWidthMultiplier() * 5,
ticketNo,
fontSize: shouldReduceSize(ticketNo) ? SizeConfig.getWidthMultiplier() * 2.5 : SizeConfig.getWidthMultiplier() * 5,
letterSpacing: -1,
fontHeight: 0.5,
color: AppColors.greyTextColor,
@ -90,12 +80,12 @@ class QueueItemNormalCard extends StatelessWidget {
flex: 3,
child: Row(
children: [
callTypeEnum.getIconByCallType(SizeConfig.getHeightMultiplier() * 0.35, qTypeEnum),
callTypeEnum.getIconByCallType(SizeConfig.getHeightMultiplier() * 0.35),
],
),
),
Flexible(
flex: 2,
flex: 4,
child: Center(
child: AppText(
textAlign: TextAlign.center,
@ -118,7 +108,7 @@ class QueueItemNormalCard extends StatelessWidget {
padding: EdgeInsets.all(SizeConfig.getHeightMultiplier() * 0.05),
color: callTypeEnum.getColorByCallType(),
child: engArabicTextWithSeparatorWidget(
fontSize: qTypeEnum != QTypeEnum.appointment ? SizeConfig.getWidthMultiplier()! * 1.9 : SizeConfig.getWidthMultiplier()! * 1.8,
fontSize: SizeConfig.getWidthMultiplier()! * 1.8,
englishText: roomNo.extractNumbersIfLong(),
arabicText: roomTextAr,
color: AppColors.whiteColor,

@ -70,7 +70,6 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
// roomText: '',
// );
log("screenConfigViewModel: ${screenConfigViewModel.currentScreenIP}");
if (screenConfigViewModel.currentQTypeEnum == QTypeEnum.general) {
text = AppStrings.awaitingQueueNumberEng;
}
@ -190,19 +189,12 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
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();
log("QuarterTurns applied: $quarterTurns");
log("MediaQuery size: ${MediaQuery.of(context).size}");
log("MediaQuery orientation: ${MediaQuery.of(context).orientation}");
log("========================");
// context.read<ScreenConfigViewModel>().createAutoTickets(numOfTicketsToCreate: 20);
// context.read<QueuingViewModel>().testSpeech();
return RotatedBox(
quarterTurns: quarterTurns,
quarterTurns: globalConfigurationsModel.isFromTakhasusiMain
? screenOrientationEnum.getTurnsByOrientationForOlderVersions()
: screenOrientationEnum.getTurnsByOrientation(),
child: AppScaffold(
backgroundColor: AppColors.backgroundColor,
appBar: const AppHeader(),

1
linux/.gitignore vendored

@ -0,0 +1 @@
flutter/ephemeral

@ -0,0 +1,145 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "hmg_qline")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.hmg_qline.hmg_qline")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Define the application target. To change its name, change BINARY_NAME above,
# not the value here, or `flutter run` will no longer work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

@ -0,0 +1,23 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

@ -0,0 +1,124 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "hmg_qline");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "hmg_qline");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

7
macos/.gitignore vendored

@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

@ -0,0 +1,26 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import audio_session
import connectivity_plus
import flutter_tts
import just_audio
import package_info_plus
import path_provider_foundation
import shared_preferences_foundation
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}

@ -0,0 +1,705 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* hmg_qline.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "hmg_qline.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* hmg_qline.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* hmg_qline.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.hmgqline.hmgQline.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hmg_qline.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hmg_qline";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.hmgqline.hmgQline.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hmg_qline.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hmg_qline";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.hmgqline.hmgQline.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hmg_qline.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hmg_qline";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "hmg_qline.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "hmg_qline.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "hmg_qline.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "hmg_qline.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

@ -0,0 +1,9 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}

@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,343 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="EPT-qC-fAb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>

@ -0,0 +1,14 @@
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = hmg_qline
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.hmgqline.hmgQline
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2024 com.example.hmg_qline. All rights reserved.

@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"

@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"

@ -0,0 +1,13 @@
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

@ -0,0 +1,15 @@
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>

@ -0,0 +1,12 @@
import Cocoa
import FlutterMacOS
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hmg_qline/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="hmg_qline">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>hmg_qline</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

@ -0,0 +1,35 @@
{
"name": "hmg_qline",
"short_name": "hmg_qline",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

17
windows/.gitignore vendored

@ -0,0 +1,17 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

@ -0,0 +1,108 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(hmg_qline LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "hmg_qline")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(VERSION 3.14...3.25)
# Define build configuration option.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
endif()
# Define settings for the Profile build mode.
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)

@ -0,0 +1,109 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.14)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)

@ -0,0 +1,17 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <flutter_tts/flutter_tts_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FlutterTtsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterTtsPlugin"));
}

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

@ -0,0 +1,25 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
flutter_tts
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

@ -0,0 +1,40 @@
cmake_minimum_required(VERSION 3.14)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME} WIN32
"flutter_window.cpp"
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the build version.
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)

@ -0,0 +1,121 @@
// Microsoft Visual C++ generated resource script.
//
#pragma code_page(65001)
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
#define VERSION_AS_NUMBER 1,0,0,0
#endif
#if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_AS_NUMBER
PRODUCTVERSION VERSION_AS_NUMBER
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example.hmg_qline" "\0"
VALUE "FileDescription", "hmg_qline" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "hmg_qline" "\0"
VALUE "LegalCopyright", "Copyright (C) 2024 com.example.hmg_qline. All rights reserved." "\0"
VALUE "OriginalFilename", "hmg_qline.exe" "\0"
VALUE "ProductName", "hmg_qline" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

@ -0,0 +1,71 @@
#include "flutter_window.h"
#include <optional>
#include "flutter/generated_plugin_registrant.h"
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
FlutterWindow::~FlutterWindow() {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow());
flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
});
// Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the
// window is shown. It is a no-op if the first frame hasn't completed yet.
flutter_controller_->ForceRedraw();
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_controller_) {
flutter_controller_ = nullptr;
}
Win32Window::OnDestroy();
}
LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
// Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) {
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) {
return *result;
}
}
switch (message) {
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
}
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}

@ -0,0 +1,33 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#include "win32_window.h"
// A window that does nothing but host a Flutter view.
class FlutterWindow : public Win32Window {
public:
// Creates a new FlutterWindow hosting a Flutter view running |project|.
explicit FlutterWindow(const flutter::DartProject& project);
virtual ~FlutterWindow();
protected:
// Win32Window:
bool OnCreate() override;
void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
LPARAM const lparam) noexcept override;
private:
// The project to run.
flutter::DartProject project_;
// The Flutter instance hosted by this window.
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_

@ -0,0 +1,43 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include "flutter_window.h"
#include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
// Initialize COM, so that it is available for use in the library and/or
// plugins.
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
flutter::DartProject project(L"data");
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"hmg_qline", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::CoUninitialize();
return EXIT_SUCCESS;
}

@ -0,0 +1,16 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Runner.rc
//
#define IDI_APP_ICON 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

@ -0,0 +1,65 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
void CreateAndAttachConsole() {
if (::AllocConsole()) {
FILE *unused;
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
_dup2(_fileno(stdout), 1);
}
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
_dup2(_fileno(stdout), 2);
}
std::ios::sync_with_stdio();
FlutterDesktopResyncOutputStreams();
}
}
std::vector<std::string> GetCommandLineArguments() {
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
int argc;
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
if (argv == nullptr) {
return std::vector<std::string>();
}
std::vector<std::string> command_line_arguments;
// Skip the first argument as it's the binary name.
for (int i = 1; i < argc; i++) {
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
}
::LocalFree(argv);
return command_line_arguments;
}
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) {
return std::string();
}
unsigned int target_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr)
-1; // remove the trailing null character
int input_length = (int)wcslen(utf16_string);
std::string utf8_string;
if (target_length == 0 || target_length > utf8_string.max_size()) {
return utf8_string;
}
utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
input_length, utf8_string.data(), target_length, nullptr, nullptr);
if (converted_length == 0) {
return std::string();
}
return utf8_string;
}

@ -0,0 +1,19 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// Creates a console for the process, and redirects stdout and stderr to
// it for both the runner and the Flutter library.
void CreateAndAttachConsole();
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
// encoded in UTF-8. Returns an empty std::string on failure.
std::string Utf8FromUtf16(const wchar_t* utf16_string);
// Gets the command line arguments passed in as a std::vector<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_

@ -0,0 +1,288 @@
#include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h>
#include "resource.h"
namespace {
/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
/// Registry key for app theme preference.
///
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
/// value indicates apps should use light mode.
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in
// scale factor
int Scale(int source, double scale_factor) {
return static_cast<int>(source * scale_factor);
}
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
// This API is only needed for PerMonitor V1 awareness mode.
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
HMODULE user32_module = LoadLibraryA("User32.dll");
if (!user32_module) {
return;
}
auto enable_non_client_dpi_scaling =
reinterpret_cast<EnableNonClientDpiScaling*>(
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd);
}
FreeLibrary(user32_module);
}
} // namespace
// Manages the Win32Window's window class registration.
class WindowClassRegistrar {
public:
~WindowClassRegistrar() = default;
// Returns the singleton registrar instance.
static WindowClassRegistrar* GetInstance() {
if (!instance_) {
instance_ = new WindowClassRegistrar();
}
return instance_;
}
// Returns the name of the window class, registering the class if it hasn't
// previously been registered.
const wchar_t* GetWindowClass();
// Unregisters the window class. Should only be called if there are no
// instances of the window.
void UnregisterWindowClass();
private:
WindowClassRegistrar() = default;
static WindowClassRegistrar* instance_;
bool class_registered_ = false;
};
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
const wchar_t* WindowClassRegistrar::GetWindowClass() {
if (!class_registered_) {
WNDCLASS window_class{};
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc;
RegisterClass(&window_class);
class_registered_ = true;
}
return kWindowClassName;
}
void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false;
}
Win32Window::Win32Window() {
++g_active_window_count;
}
Win32Window::~Win32Window() {
--g_active_window_count;
Destroy();
}
bool Win32Window::Create(const std::wstring& title,
const Point& origin,
const Size& size) {
Destroy();
const wchar_t* window_class =
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = {static_cast<LONG>(origin.x),
static_cast<LONG>(origin.y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);
if (!window) {
return false;
}
UpdateTheme(window);
return OnCreate();
}
bool Win32Window::Show() {
return ShowWindow(window_handle_, SW_SHOWNORMAL);
}
// static
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) {
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window;
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
}
LRESULT
Win32Window::MessageHandler(HWND hwnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
window_handle_ = nullptr;
Destroy();
if (quit_on_close_) {
PostQuitMessage(0);
}
return 0;
case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT*>(lparam);
LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_SIZE: {
RECT rect = GetClientArea();
if (child_content_ != nullptr) {
// Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
}
return 0;
}
case WM_ACTIVATE:
if (child_content_ != nullptr) {
SetFocus(child_content_);
}
return 0;
case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
}
return DefWindowProc(window_handle_, message, wparam, lparam);
}
void Win32Window::Destroy() {
OnDestroy();
if (window_handle_) {
DestroyWindow(window_handle_);
window_handle_ = nullptr;
}
if (g_active_window_count == 0) {
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
}
}
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<Win32Window*>(
GetWindowLongPtr(window, GWLP_USERDATA));
}
void Win32Window::SetChildContent(HWND content) {
child_content_ = content;
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);
SetFocus(child_content_);
}
RECT Win32Window::GetClientArea() {
RECT frame;
GetClientRect(window_handle_, &frame);
return frame;
}
HWND Win32Window::GetHandle() {
return window_handle_;
}
void Win32Window::SetQuitOnClose(bool quit_on_close) {
quit_on_close_ = quit_on_close;
}
bool Win32Window::OnCreate() {
// No-op; provided for subclasses.
return true;
}
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}
void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode);
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue,
RRF_RT_REG_DWORD, nullptr, &light_mode,
&light_mode_size);
if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
&enable_dark_mode, sizeof(enable_dark_mode));
}
}

@ -0,0 +1,102 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
// inherited from by classes that wish to specialize with custom
// rendering and input handling
class Win32Window {
public:
struct Point {
unsigned int x;
unsigned int y;
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
};
struct Size {
unsigned int width;
unsigned int height;
Size(unsigned int width, unsigned int height)
: width(width), height(height) {}
};
Win32Window();
virtual ~Win32Window();
// Creates a win32 window with |title| that is positioned and sized using
// |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size this function will scale the inputted width and height as
// as appropriate for the default monitor. The window is invisible until
// |Show| is called. Returns true if the window was created successfully.
bool Create(const std::wstring& title, const Point& origin, const Size& size);
// Show the current window. Returns true if the window was successfully shown.
bool Show();
// Release OS resources associated with window.
void Destroy();
// Inserts |content| into the window tree.
void SetChildContent(HWND content);
// Returns the backing Window handle to enable clients to set icon and other
// window properties. Returns nullptr if the window has been destroyed.
HWND GetHandle();
// If true, closing this window will quit the application.
void SetQuitOnClose(bool quit_on_close);
// Return a RECT representing the bounds of the current client area.
RECT GetClientArea();
protected:
// Processes and route salient window messages for mouse handling,
// size change and DPI. Delegates handling of these to member overloads that
// inheriting classes can handle.
virtual LRESULT MessageHandler(HWND window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Called when CreateAndShow is called, allowing subclass window-related
// setup. Subclasses should return false if setup fails.
virtual bool OnCreate();
// Called when Destroy is called.
virtual void OnDestroy();
private:
friend class WindowClassRegistrar;
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
// non-client DPI scaling so that the non-client area automatically
// responds to changes in DPI. All other messages are handled by
// MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
// Update the window frame's theme to match the system theme.
static void UpdateTheme(HWND const window);
bool quit_on_close_ = false;
// window handle for top level window.
HWND window_handle_ = nullptr;
// window handle for hosted content.
HWND child_content_ = nullptr;
};
#endif // RUNNER_WIN32_WINDOW_H_
Loading…
Cancel
Save