Add Live Activity feature with consent dialog and settings integration

This commit is contained in:
2026-03-07 12:13:23 +01:00
parent 5d95e1e1d7
commit aea12d20fc
7 changed files with 214 additions and 1 deletions

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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",

View File

@@ -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),