Add Live Activity feature with consent dialog and settings integration
This commit is contained in:
@@ -68,6 +68,10 @@ class LiveCardProvider extends ChangeNotifier {
|
||||
|
||||
// Token figyelése: amikor az APNs tokent ad (első vagy rotation), regisztráljuk a szerverrel
|
||||
PlatformChannel.onTokenUpdated = (pushToken, deviceId, bundleId) {
|
||||
if (!_settings.liveActivityEnabled) {
|
||||
debugPrint("Push token érkezett, de Live Activity nincs engedélyezve - skip");
|
||||
return;
|
||||
}
|
||||
debugPrint("Push token érkezett: $pushToken");
|
||||
serverSync.registerAndSync(
|
||||
deviceId: deviceId,
|
||||
@@ -345,6 +349,16 @@ class LiveCardProvider extends ChangeNotifier {
|
||||
|
||||
//LIVE ACTIVITIES
|
||||
|
||||
// Guard: if not enabled, stop any running activity and skip
|
||||
if (!_settings.liveActivityEnabled) {
|
||||
if (hasActivityStarted) {
|
||||
debugPrint("Live Activity nincs engedélyezve, de fut – leállítás...");
|
||||
PlatformChannel.endLiveActivity();
|
||||
serverSync.unregister();
|
||||
hasActivityStarted = false;
|
||||
}
|
||||
} else {
|
||||
|
||||
//CREATE
|
||||
if (!hasActivityStarted &&
|
||||
!hasUserDismissed &&
|
||||
@@ -428,6 +442,9 @@ class LiveCardProvider extends ChangeNotifier {
|
||||
hasActivityStarted = false;
|
||||
hasUserDismissed = false;
|
||||
}
|
||||
|
||||
} // end of liveActivityEnabled else block
|
||||
|
||||
LAData = toMap();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:refilc/api/providers/database_provider.dart';
|
||||
@@ -57,6 +58,7 @@ const settingsDB = DatabaseStruct("settings", {
|
||||
"uwu_mode": int,
|
||||
"new_popups": int,
|
||||
"unseen_new_features": String,
|
||||
"live_activity_enabled": int,
|
||||
"cloud_sync_enabled": int,
|
||||
"cloud_sync_token": String,
|
||||
"local_updated_at": String,
|
||||
@@ -185,6 +187,21 @@ Future<Database> initDB(DatabaseProvider database) async {
|
||||
print("ERROR: migrateDB: $error");
|
||||
}
|
||||
|
||||
// One-time migration: add 'live_activity_consent' to unseen_new_features for existing users
|
||||
try {
|
||||
final rows = await db.query('settings', columns: ['unseen_new_features']);
|
||||
if (rows.isNotEmpty) {
|
||||
final raw = rows.first['unseen_new_features'] as String? ?? '[]';
|
||||
final list = (jsonDecode(raw) as List).cast<String>();
|
||||
if (!list.contains('live_activity_consent')) {
|
||||
list.add('live_activity_consent');
|
||||
await db.update('settings', {'unseen_new_features': jsonEncode(list)});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("ERROR: live_activity_consent migration: $e");
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ class SettingsProvider extends ChangeNotifier {
|
||||
bool _uwuMode;
|
||||
bool _newPopups;
|
||||
List<String> _unseenNewFeatures;
|
||||
bool _liveActivityEnabled;
|
||||
bool _cloudSyncEnabled;
|
||||
String _cloudSyncToken;
|
||||
DateTime _updatedAt;
|
||||
@@ -189,6 +190,7 @@ class SettingsProvider extends ChangeNotifier {
|
||||
required bool uwuMode,
|
||||
required bool newPopups,
|
||||
required List<String> unseenNewFeatures,
|
||||
required bool liveActivityEnabled,
|
||||
required bool cloudSyncEnabled,
|
||||
required String cloudSyncToken,
|
||||
required DateTime updatedAt,
|
||||
@@ -265,6 +267,7 @@ class SettingsProvider extends ChangeNotifier {
|
||||
_uwuMode = uwuMode,
|
||||
_newPopups = newPopups,
|
||||
_unseenNewFeatures = unseenNewFeatures,
|
||||
_liveActivityEnabled = liveActivityEnabled,
|
||||
_cloudSyncEnabled = cloudSyncEnabled,
|
||||
_cloudSyncToken = cloudSyncToken,
|
||||
_updatedAt = updatedAt,
|
||||
@@ -360,6 +363,7 @@ class SettingsProvider extends ChangeNotifier {
|
||||
uwuMode: map['uwu_mode'] == 1,
|
||||
newPopups: map['new_popups'] == 1,
|
||||
unseenNewFeatures: jsonDecode(map["unseen_new_features"]).cast<String>(),
|
||||
liveActivityEnabled: map['live_activity_enabled'] == 1,
|
||||
cloudSyncEnabled: map['cloud_sync_enabled'] == 1,
|
||||
cloudSyncToken: map['cloud_sync_token'],
|
||||
updatedAt: DateTime.tryParse(map['local_updated_at']) ?? DateTime.now(),
|
||||
@@ -443,6 +447,7 @@ class SettingsProvider extends ChangeNotifier {
|
||||
"uwu_mode": _uwuMode ? 1 : 0,
|
||||
"new_popups": _newPopups ? 1 : 0,
|
||||
"unseen_new_features": jsonEncode(_unseenNewFeatures),
|
||||
"live_activity_enabled": _liveActivityEnabled ? 1 : 0,
|
||||
"cloud_sync_enabled": _cloudSyncEnabled ? 1 : 0,
|
||||
"cloud_sync_token": _cloudSyncToken,
|
||||
"local_updated_at": _updatedAt.toIso8601String(),
|
||||
@@ -529,7 +534,8 @@ class SettingsProvider extends ChangeNotifier {
|
||||
newColors: true,
|
||||
uwuMode: false,
|
||||
newPopups: false,
|
||||
unseenNewFeatures: [],
|
||||
unseenNewFeatures: ['live_activity_consent'],
|
||||
liveActivityEnabled: false,
|
||||
cloudSyncEnabled: false,
|
||||
cloudSyncToken: '',
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -608,6 +614,7 @@ class SettingsProvider extends ChangeNotifier {
|
||||
bool get uwuMode => _uwuMode;
|
||||
bool get newPopups => _newPopups;
|
||||
List<String> get unseenNewFeatures => _unseenNewFeatures;
|
||||
bool get liveActivityEnabled => _liveActivityEnabled;
|
||||
bool get cloudSyncEnabled => _cloudSyncEnabled;
|
||||
String get cloudSyncToken => _cloudSyncToken;
|
||||
DateTime get updatedAt => _updatedAt;
|
||||
@@ -683,6 +690,7 @@ class SettingsProvider extends ChangeNotifier {
|
||||
bool? uwuMode,
|
||||
bool? newPopups,
|
||||
List<String>? unseenNewFeatures,
|
||||
bool? liveActivityEnabled,
|
||||
bool? cloudSyncEnabled,
|
||||
String? cloudSyncToken,
|
||||
bool? qTimetableLessonNum,
|
||||
@@ -886,6 +894,12 @@ class SettingsProvider extends ChangeNotifier {
|
||||
if (unseenNewFeatures != null && unseenNewFeatures != _unseenNewFeatures) {
|
||||
_unseenNewFeatures = unseenNewFeatures;
|
||||
}
|
||||
if (liveActivityEnabled != null && liveActivityEnabled != _liveActivityEnabled) {
|
||||
_liveActivityEnabled = liveActivityEnabled;
|
||||
if (!liveActivityEnabled && Platform.isIOS) {
|
||||
LiveCardProvider.hasActivitySettingsChanged = true;
|
||||
}
|
||||
}
|
||||
if (cloudSyncEnabled != null && cloudSyncEnabled != _cloudSyncEnabled) {
|
||||
_cloudSyncEnabled = cloudSyncEnabled;
|
||||
}
|
||||
@@ -1005,6 +1019,7 @@ class SettingsProvider extends ChangeNotifier {
|
||||
newPopups: map['new_popups'] == 1,
|
||||
unseenNewFeatures:
|
||||
jsonDecode(map["unseen_new_features"] ?? "[]").cast<String>(),
|
||||
liveActivityEnabled: map['live_activity_enabled'] == 1,
|
||||
cloudSyncEnabled: map['cloud_sync_enabled'] == 1,
|
||||
cloudSyncToken: map['cloud_sync_token'],
|
||||
qTimetableLessonNum: map['q_timetable_lesson_num'] == 1,
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:refilc_mobile_ui/screens/navigation/navigation_route_handler.dar
|
||||
import 'package:refilc_mobile_ui/screens/navigation/status_bar.dart';
|
||||
import 'package:refilc_mobile_ui/screens/news/news_view.dart';
|
||||
import 'package:refilc_mobile_ui/screens/settings/settings_screen.dart';
|
||||
import 'package:refilc_mobile_ui/screens/settings/live_activity_consent_dialog.dart';
|
||||
import 'package:refilc_plus/ui/mobile/goal_planner/goal_complete_modal.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -32,6 +33,7 @@ import 'package:wtf_sliding_sheet/wtf_sliding_sheet.dart';
|
||||
import 'package:background_fetch/background_fetch.dart';
|
||||
import 'package:refilc_plus/providers/goal_provider.dart';
|
||||
import 'package:refilc/api/providers/ad_provider.dart';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
class NavigationScreen extends StatefulWidget {
|
||||
const NavigationScreen({super.key});
|
||||
@@ -190,6 +192,14 @@ class NavigationScreenState extends State<NavigationScreen>
|
||||
// initial sync
|
||||
syncAll(context);
|
||||
setupQuickActions();
|
||||
|
||||
// Show live activity consent dialog on iOS
|
||||
if (Platform.isIOS &&
|
||||
settings.unseenNewFeatures.contains('live_activity_consent')) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
LiveActivityConsentDialog.show(context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart';
|
||||
|
||||
class LiveActivityConsentDialog extends StatelessWidget {
|
||||
const LiveActivityConsentDialog({super.key});
|
||||
|
||||
static Future<void> show(BuildContext context) => showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const LiveActivityConsentDialog(),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 100.0, horizontal: 32.0),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Text(
|
||||
"live_activity_consent_title".i18n,
|
||||
style: const TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Text(
|
||||
"live_activity_consent_body".i18n,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => _respond(context, false),
|
||||
child: Text("live_activity_decline".i18n),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () => _respond(context, true),
|
||||
child: Text("live_activity_accept".i18n),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _respond(BuildContext context, bool accepted) {
|
||||
final settings = Provider.of<SettingsProvider>(context, listen: false);
|
||||
final unseen =
|
||||
List<String>.from(settings.unseenNewFeatures);
|
||||
unseen.remove('live_activity_consent');
|
||||
settings.update(
|
||||
liveActivityEnabled: accepted,
|
||||
unseenNewFeatures: unseen,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,11 @@ extension SettingsLocalization on String {
|
||||
"rename_subjects": "Rename Subjects",
|
||||
"rename_teachers": "Rename Teachers",
|
||||
"show_breaks": "Show Breaks",
|
||||
"live_activity_enabled": "Live Activity",
|
||||
"live_activity_consent_title": "Live Activity – Data Privacy",
|
||||
"live_activity_consent_body": "The Live Activity feature sends data to our server for real-time push notification updates.\n\nData sent:\n• Device identifier\n• APNs push token\n• Your timetable\n\nPurpose:\n• Real-time Live Activity updates via push notifications\n\nData is stored on an encrypted server. You can disable this at any time in Settings > General.",
|
||||
"live_activity_accept": "Allow",
|
||||
"live_activity_decline": "Don't Allow",
|
||||
"fonts": "Fonts",
|
||||
"font_family": "Font Family",
|
||||
"calendar_sync": "Calendar Sync",
|
||||
@@ -228,6 +233,11 @@ extension SettingsLocalization on String {
|
||||
"rename_subjects": "Tantárgyak átnevezése",
|
||||
"rename_teachers": "Tanárok átnevezése",
|
||||
"show_breaks": "Szünetek megjelenítése",
|
||||
"live_activity_enabled": "Live Activity",
|
||||
"live_activity_consent_title": "Live Activity – Adatvédelem",
|
||||
"live_activity_consent_body": "A Live Activity funkció adatokat küld a szerverünkre a valós idejű push értesítés frissítésekhez.\n\nKüldött adatok:\n• Eszközazonosító\n• APNs push token\n• Órarendjeid\n\nCél:\n• Valós idejű Live Activity frissítések push értesítéssel\n\nAz adatokat titkosított szerveren tároljuk. Bármikor kikapcsolhatod a Beállítások > Általános menüben.",
|
||||
"live_activity_accept": "Engedélyezem",
|
||||
"live_activity_decline": "Nem engedélyezem",
|
||||
"fonts": "Betűk",
|
||||
"font_family": "Betűtípus",
|
||||
"calendar_sync": "Naptár szinkronizálás",
|
||||
@@ -358,6 +368,11 @@ extension SettingsLocalization on String {
|
||||
"rename_subjects": "Fächer umbenennen",
|
||||
"rename_teachers": "Lehrer umbenennen",
|
||||
"show_breaks": "Pausen anzeigen",
|
||||
"live_activity_enabled": "Live Activity",
|
||||
"live_activity_consent_title": "Live Activity – Datenschutz",
|
||||
"live_activity_consent_body": "Die Live Activity-Funktion sendet Daten an unseren Server für Echtzeit-Push-Benachrichtigungen.\n\nGesendete Daten:\n• Gerätekennung\n• APNs Push-Token\n• Ihr Stundenplan\n\nZweck:\n• Echtzeit Live Activity-Updates über Push-Benachrichtigungen\n\nDaten werden auf einem verschlüsselten Server gespeichert. Sie können dies jederzeit unter Einstellungen > Allgemein deaktivieren.",
|
||||
"live_activity_accept": "Erlauben",
|
||||
"live_activity_decline": "Nicht erlauben",
|
||||
"fonts": "Schriftarten",
|
||||
"font_family": "Schriftfamilie",
|
||||
"calendar_sync": "Kalender-Synchronisation",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:refilc/api/providers/liveactivity/platform_channel.dart';
|
||||
import 'package:refilc/api/providers/live_card_provider.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/theme/colors/colors.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
@@ -123,6 +126,61 @@ class GeneralSettingsScreenState extends State<GeneralSettingsScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (Platform.isIOS)
|
||||
SplittedPanel(
|
||||
padding: const EdgeInsets.only(top: 9.0),
|
||||
cardPadding: const EdgeInsets.all(4.0),
|
||||
isSeparated: true,
|
||||
children: [
|
||||
PanelButton(
|
||||
padding: const EdgeInsets.only(left: 14.0, right: 6.0),
|
||||
onPressed: () {
|
||||
final newVal = !settingsProvider.liveActivityEnabled;
|
||||
settingsProvider.update(liveActivityEnabled: newVal);
|
||||
if (!newVal) {
|
||||
PlatformChannel.endLiveActivity();
|
||||
LiveCardProvider.serverSync.unregister();
|
||||
LiveCardProvider.hasActivityStarted = false;
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
title: Text(
|
||||
"live_activity_enabled".i18n,
|
||||
style: TextStyle(
|
||||
color: AppColors.of(context).text.withValues(
|
||||
alpha: settingsProvider.liveActivityEnabled
|
||||
? .95
|
||||
: .25),
|
||||
),
|
||||
),
|
||||
leading: Icon(
|
||||
FeatherIcons.activity,
|
||||
size: 22.0,
|
||||
color: AppColors.of(context).text.withValues(
|
||||
alpha: settingsProvider.liveActivityEnabled
|
||||
? .95
|
||||
: .25),
|
||||
),
|
||||
trailing: Switch(
|
||||
onChanged: (v) {
|
||||
settingsProvider.update(liveActivityEnabled: v);
|
||||
if (!v) {
|
||||
PlatformChannel.endLiveActivity();
|
||||
LiveCardProvider.serverSync.unregister();
|
||||
LiveCardProvider.hasActivityStarted = false;
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
value: settingsProvider.liveActivityEnabled,
|
||||
activeColor: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12.0),
|
||||
bottom: Radius.circular(12.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SplittedPanel(
|
||||
padding: const EdgeInsets.only(top: 9.0),
|
||||
cardPadding: const EdgeInsets.all(4.0),
|
||||
|
||||
Reference in New Issue
Block a user