Add welcome popup, changelog system, and account deletion button

This commit is contained in:
stopflock
2025-10-20 19:27:55 -05:00
parent 07fe869eec
commit d696e1dfb6
19 changed files with 861 additions and 10 deletions
+78 -1
View File
@@ -68,7 +68,18 @@ class AboutScreen extends StatelessWidget {
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
const SizedBox(height: 24),
// Release Notes button
Center(
child: OutlinedButton.icon(
onPressed: () {
Navigator.pushNamed(context, '/settings/release-notes');
},
icon: const Icon(Icons.article_outlined),
label: const Text('View Release Notes'),
),
),
const SizedBox(height: 24),
_buildHelpLinks(context),
],
),
@@ -93,10 +104,76 @@ class AboutScreen extends StatelessWidget {
_buildLinkText(context, 'Contact', 'https://deflock.me/contact'),
const SizedBox(height: 8),
_buildLinkText(context, 'Donate', 'https://deflock.me/donate'),
const SizedBox(height: 24),
// Divider for account management section
Divider(
color: Theme.of(context).dividerColor.withOpacity(0.3),
),
const SizedBox(height: 16),
// Account deletion link (less prominent)
_buildAccountDeletionLink(context),
],
);
}
Widget _buildAccountDeletionLink(BuildContext context) {
final locService = LocalizationService.instance;
return GestureDetector(
onTap: () => _showDeleteAccountDialog(context, locService),
child: Text(
locService.t('auth.deleteAccount'),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.error.withOpacity(0.7),
decoration: TextDecoration.underline,
),
textAlign: TextAlign.center,
),
);
}
void _showDeleteAccountDialog(BuildContext context, LocalizationService locService) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(locService.t('auth.deleteAccount')),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(locService.t('auth.deleteAccountExplanation')),
const SizedBox(height: 12),
Text(
locService.t('auth.deleteAccountWarning'),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.error,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(locService.t('actions.cancel')),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
_launchUrl('https://www.openstreetmap.org/account/deletion', context);
},
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
child: Text(locService.t('auth.goToOSM')),
),
],
),
);
}
Widget _buildLinkText(BuildContext context, String text, String url) {
return GestureDetector(
onTap: () => _launchUrl(url, context),
+58
View File
@@ -19,9 +19,12 @@ import '../widgets/measured_sheet.dart';
import '../widgets/navigation_sheet.dart';
import '../widgets/search_bar.dart';
import '../widgets/suspected_location_sheet.dart';
import '../widgets/welcome_dialog.dart';
import '../widgets/changelog_dialog.dart';
import '../models/osm_node.dart';
import '../models/suspected_location.dart';
import '../models/search_result.dart';
import '../services/changelog_service.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@@ -48,6 +51,9 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
// Track selected node for highlighting
int? _selectedNodeId;
// Track popup display to avoid showing multiple times
bool _hasCheckedForPopup = false;
@override
void initState() {
@@ -219,6 +225,52 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
});
}
// Check for and display welcome/changelog popup
Future<void> _checkForPopup() async {
if (!mounted) return;
try {
final popupType = await ChangelogService().getPopupType();
if (!mounted) return; // Check again after async operation
switch (popupType) {
case PopupType.welcome:
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const WelcomeDialog(),
);
break;
case PopupType.changelog:
final changelogContent = ChangelogService().getChangelogForCurrentVersion();
if (changelogContent != null) {
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => ChangelogDialog(changelogContent: changelogContent),
);
}
break;
case PopupType.none:
// No popup needed, but still update version tracking for future launches
await ChangelogService().updateLastSeenVersion();
break;
}
} catch (e) {
// Silently handle errors to avoid breaking the app launch
debugPrint('[HomeScreen] Error checking for popup: $e');
// Still update version tracking in case of error
try {
await ChangelogService().updateLastSeenVersion();
} catch (e2) {
debugPrint('[HomeScreen] Error updating version: $e2');
}
}
}
void _onStartRoute() {
final appState = context.read<AppState>();
@@ -526,6 +578,12 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
}
}
// Check for welcome/changelog popup after app is fully initialized
if (appState.isInitialized && !_hasCheckedForPopup) {
_hasCheckedForPopup = true;
WidgetsBinding.instance.addPostFrameCallback((_) => _checkForPopup());
}
// Pass the active sheet height directly to the map
final activeSheetHeight = _addSheetHeight > 0
? _addSheetHeight
+191
View File
@@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import '../services/changelog_service.dart';
import '../services/version_service.dart';
class ReleaseNotesScreen extends StatefulWidget {
const ReleaseNotesScreen({super.key});
@override
State<ReleaseNotesScreen> createState() => _ReleaseNotesScreenState();
}
class _ReleaseNotesScreenState extends State<ReleaseNotesScreen> {
Map<String, String>? _changelogs;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadChangelogs();
}
Future<void> _loadChangelogs() async {
try {
// Ensure changelog service is initialized
if (!ChangelogService().isInitialized) {
await ChangelogService().init();
}
final changelogs = ChangelogService().getAllChangelogs();
if (mounted) {
setState(() {
_changelogs = changelogs;
_isLoading = false;
});
}
} catch (e) {
debugPrint('[ReleaseNotesScreen] Error loading changelogs: $e');
if (mounted) {
setState(() {
_changelogs = {};
_isLoading = false;
});
}
}
}
List<String> _sortVersions(List<String> versions) {
// Simple version sorting - splits by '.' and compares numerically
versions.sort((a, b) {
final aParts = a.split('.').map(int.tryParse).where((v) => v != null).cast<int>().toList();
final bParts = b.split('.').map(int.tryParse).where((v) => v != null).cast<int>().toList();
// Compare version parts (reverse order for newest first)
for (int i = 0; i < aParts.length && i < bParts.length; i++) {
final comparison = bParts[i].compareTo(aParts[i]); // Reverse for desc order
if (comparison != 0) return comparison;
}
// If one version has more parts, the longer one is newer
return bParts.length.compareTo(aParts.length);
});
return versions;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Release Notes'),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _changelogs == null || _changelogs!.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(24.0),
child: Text(
'No release notes available.',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
),
)
: ListView(
padding: const EdgeInsets.all(16),
children: [
// Current version indicator
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
),
),
child: Row(
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'Current Version: ${VersionService().version}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
// Changelog entries
..._buildChangelogEntries(),
],
),
);
}
List<Widget> _buildChangelogEntries() {
if (_changelogs == null || _changelogs!.isEmpty) return [];
final sortedVersions = _sortVersions(_changelogs!.keys.toList());
final currentVersion = VersionService().version;
return sortedVersions.map((version) {
final content = _changelogs![version]!;
final isCurrentVersion = version == currentVersion;
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
border: Border.all(
color: isCurrentVersion
? Theme.of(context).colorScheme.primary.withOpacity(0.3)
: Theme.of(context).dividerColor.withOpacity(0.3),
),
borderRadius: BorderRadius.circular(8),
),
child: ExpansionTile(
title: Row(
children: [
Text(
'Version $version',
style: TextStyle(
fontWeight: FontWeight.bold,
color: isCurrentVersion
? Theme.of(context).colorScheme.primary
: null,
),
),
if (isCurrentVersion) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'CURRENT',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
],
),
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
child: Text(
content,
style: const TextStyle(height: 1.4),
),
),
],
),
);
}).toList();
}
}