A bit of notification turn on reminder
This commit is contained in:
parent
ec46c33c35
commit
e91e7b43d2
|
|
@ -1,3 +1,8 @@
|
|||
import com.android.build.gradle.LibraryExtension
|
||||
import org.gradle.api.tasks.Delete
|
||||
import com.android.build.gradle.BaseExtension
|
||||
import org.gradle.api.JavaVersion
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
|
|
@ -19,6 +24,50 @@ subprojects {
|
|||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
// Ensure older plugin modules that don't declare a `namespace` still build with AGP.
|
||||
subprojects.forEach { project ->
|
||||
project.plugins.withId("com.android.library") {
|
||||
try {
|
||||
val androidExt = project.extensions.findByName("android") as? LibraryExtension
|
||||
if (androidExt != null) {
|
||||
try {
|
||||
val current = androidExt.namespace
|
||||
if (current.isNullOrBlank()) {
|
||||
androidExt.namespace = "com.tasq.${project.name.replace('-', '_')}"
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
try {
|
||||
androidExt.namespace = "com.tasq.${project.name.replace('-', '_')}"
|
||||
} catch (_: Throwable) {}
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Ensure Kotlin JVM target matches Java compatibility to avoid mismatches in third-party modules.
|
||||
// Align Java compile options to Java 17 for Android modules so Kotlin JVM target mismatch is avoided.
|
||||
subprojects.forEach { project ->
|
||||
project.plugins.withId("com.android.library") {
|
||||
val androidExt = project.extensions.findByName("android") as? BaseExtension
|
||||
if (androidExt != null) {
|
||||
try {
|
||||
androidExt.compileOptions.sourceCompatibility = JavaVersion.VERSION_17
|
||||
androidExt.compileOptions.targetCompatibility = JavaVersion.VERSION_17
|
||||
} catch (_: Throwable) {}
|
||||
}
|
||||
}
|
||||
project.plugins.withId("com.android.application") {
|
||||
val androidExt = project.extensions.findByName("android") as? BaseExtension
|
||||
if (androidExt != null) {
|
||||
try {
|
||||
androidExt.compileOptions.sourceCompatibility = JavaVersion.VERSION_17
|
||||
androidExt.compileOptions.targetCompatibility = JavaVersion.VERSION_17
|
||||
} catch (_: Throwable) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -198,8 +198,9 @@ List<Map<String, dynamic>> _processTasksInIsolate(
|
|||
<String>{};
|
||||
|
||||
if (!isGlobal) {
|
||||
if (allowedTicketIds.isEmpty && allowedOfficeIds.isEmpty)
|
||||
if (allowedTicketIds.isEmpty && allowedOfficeIds.isEmpty) {
|
||||
return <Map<String, dynamic>>[];
|
||||
}
|
||||
list = list.where((t) {
|
||||
final tid = t['ticket_id'] as String?;
|
||||
final oid = t['office_id'] as String?;
|
||||
|
|
@ -262,7 +263,7 @@ List<Map<String, dynamic>> _processTasksInIsolate(
|
|||
return int.tryParse(m.group(0)!);
|
||||
}
|
||||
|
||||
int _parseCreatedAt(Map<String, dynamic> m) {
|
||||
int parseCreatedAt(Map<String, dynamic> m) {
|
||||
final v = m['created_at'];
|
||||
if (v == null) return 0;
|
||||
if (v is int) return v;
|
||||
|
|
@ -293,11 +294,11 @@ List<Map<String, dynamic>> _processTasksInIsolate(
|
|||
final bOrder = (b['queue_order'] as int?) ?? 0x7fffffff;
|
||||
final qcmp = aOrder.compareTo(bOrder);
|
||||
if (qcmp != 0) return qcmp;
|
||||
return _parseCreatedAt(a).compareTo(_parseCreatedAt(b));
|
||||
return parseCreatedAt(a).compareTo(parseCreatedAt(b));
|
||||
}
|
||||
|
||||
if (ra == 1) {
|
||||
return _parseCreatedAt(a).compareTo(_parseCreatedAt(b));
|
||||
return parseCreatedAt(a).compareTo(parseCreatedAt(b));
|
||||
}
|
||||
|
||||
if (ra == 2) {
|
||||
|
|
@ -306,14 +307,14 @@ List<Map<String, dynamic>> _processTasksInIsolate(
|
|||
if (an != null && bn != null) return bn.compareTo(an);
|
||||
if (an != null) return -1;
|
||||
if (bn != null) return 1;
|
||||
return _parseCreatedAt(b).compareTo(_parseCreatedAt(a));
|
||||
return parseCreatedAt(b).compareTo(parseCreatedAt(a));
|
||||
}
|
||||
|
||||
final aOrder = (a['queue_order'] as int?) ?? 0x7fffffff;
|
||||
final bOrder = (b['queue_order'] as int?) ?? 0x7fffffff;
|
||||
final cmp = aOrder.compareTo(bOrder);
|
||||
if (cmp != 0) return cmp;
|
||||
return _parseCreatedAt(a).compareTo(_parseCreatedAt(b));
|
||||
return parseCreatedAt(a).compareTo(parseCreatedAt(b));
|
||||
});
|
||||
|
||||
return list;
|
||||
|
|
|
|||
|
|
@ -185,7 +185,9 @@ List<Map<String, dynamic>> _processTicketsInIsolate(
|
|||
<String>{};
|
||||
|
||||
if (!isGlobal) {
|
||||
if (allowedOfficeIds.isEmpty) return <Map<String, dynamic>>[];
|
||||
if (allowedOfficeIds.isEmpty) {
|
||||
return <Map<String, dynamic>>[];
|
||||
}
|
||||
list = list
|
||||
.where((t) => allowedOfficeIds.contains(t['office_id']))
|
||||
.toList();
|
||||
|
|
@ -212,7 +214,7 @@ List<Map<String, dynamic>> _processTicketsInIsolate(
|
|||
|
||||
// Sort newest first. `created_at` may be ISO strings or timestamps;
|
||||
// handle strings and numeric values.
|
||||
int _parseCreatedAt(Map<String, dynamic> m) {
|
||||
int parseCreatedAt(Map<String, dynamic> m) {
|
||||
final v = m['created_at'];
|
||||
if (v == null) return 0;
|
||||
if (v is int) return v;
|
||||
|
|
@ -227,7 +229,7 @@ List<Map<String, dynamic>> _processTicketsInIsolate(
|
|||
return 0;
|
||||
}
|
||||
|
||||
list.sort((a, b) => _parseCreatedAt(b).compareTo(_parseCreatedAt(a)));
|
||||
list.sort((a, b) => parseCreatedAt(b).compareTo(parseCreatedAt(a)));
|
||||
|
||||
final start = (payload['offset'] as int?) ?? 0;
|
||||
final limit = (payload['limit'] as int?) ?? 50;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
|||
refreshListenable: notifier,
|
||||
redirect: (context, state) {
|
||||
final authState = ref.read(authStateChangesProvider);
|
||||
var session;
|
||||
dynamic session;
|
||||
if (authState is AsyncData) {
|
||||
final state = authState.value;
|
||||
session = state?.session;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../models/profile.dart';
|
||||
|
|
@ -299,9 +302,49 @@ final dashboardMetricsProvider = Provider<AsyncValue<DashboardMetrics>>((ref) {
|
|||
);
|
||||
});
|
||||
|
||||
class DashboardScreen extends StatelessWidget {
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||
}
|
||||
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final seen = prefs.getBool('has_seen_notif_showcase') ?? false;
|
||||
if (!seen) {
|
||||
if (!mounted) return;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Never miss an update'),
|
||||
content: const Text(
|
||||
'Ensure notification sounds and vibration are enabled for important alerts.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
openAppSettings();
|
||||
},
|
||||
child: const Text('Open settings'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Got it'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
await prefs.setBool('has_seen_notif_showcase', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final realtime = ProviderScope.containerOf(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../../services/notification_service.dart';
|
||||
|
||||
import '../../providers/notifications_provider.dart';
|
||||
import '../../providers/profile_provider.dart';
|
||||
|
|
@ -10,11 +13,34 @@ import '../../widgets/mono_text.dart';
|
|||
import '../../widgets/responsive_body.dart';
|
||||
import '../../theme/app_surfaces.dart';
|
||||
|
||||
class NotificationsScreen extends ConsumerWidget {
|
||||
class NotificationsScreen extends ConsumerStatefulWidget {
|
||||
const NotificationsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<NotificationsScreen> createState() =>
|
||||
_NotificationsScreenState();
|
||||
}
|
||||
|
||||
class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
|
||||
bool _showBanner = false;
|
||||
bool _dismissed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkChannel();
|
||||
}
|
||||
|
||||
Future<void> _checkChannel() async {
|
||||
final muted = await NotificationService.isHighPriorityChannelMuted();
|
||||
if (!mounted) return;
|
||||
if (muted) {
|
||||
setState(() => _showBanner = true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final notificationsAsync = ref.watch(notificationsProvider);
|
||||
final profilesAsync = ref.watch(profilesProvider);
|
||||
final ticketsAsync = ref.watch(ticketsProvider);
|
||||
|
|
@ -49,6 +75,29 @@ class NotificationsScreen extends ConsumerWidget {
|
|||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
if (_showBanner && !_dismissed)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MaterialBanner(
|
||||
content: const Text(
|
||||
'Push notifications are currently silenced. Tap here to fix.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
openAppSettings();
|
||||
},
|
||||
child: const Text('Open settings'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() => _dismissed = true);
|
||||
},
|
||||
child: const Text('Dismiss'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
|
|
|
|||
|
|
@ -94,4 +94,33 @@ class NotificationService {
|
|||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns true when the high-priority channel appears to be muted or
|
||||
/// has sound disabled on the device.
|
||||
static Future<bool> isHighPriorityChannelMuted() async {
|
||||
final androidImpl = _plugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>();
|
||||
if (androidImpl == null) return false;
|
||||
try {
|
||||
final impl = androidImpl as dynamic;
|
||||
final channel = await impl.getNotificationChannel(_highChannelId);
|
||||
if (channel == null) return false;
|
||||
final importance = channel.importance as dynamic;
|
||||
final playSound = channel.playSound as bool?;
|
||||
if (importance == null) return false;
|
||||
// importance may be an enum-like value; compare using index if present.
|
||||
try {
|
||||
final impIndex = (importance.index is int)
|
||||
? importance.index as int
|
||||
: int.parse(importance.toString());
|
||||
if (impIndex < Importance.high.index) return true;
|
||||
} catch (_) {}
|
||||
if (playSound == false) return true;
|
||||
return false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
// showcaseview removed due to null-safety incompatibility; onboarding shown via dialog
|
||||
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../providers/notifications_provider.dart';
|
||||
import '../providers/profile_provider.dart';
|
||||
import 'app_breakpoints.dart';
|
||||
|
||||
final GlobalKey notificationBellKey = GlobalKey();
|
||||
|
||||
class AppScaffold extends ConsumerWidget {
|
||||
const AppScaffold({super.key, required this.child});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user