Live presenter annotations + keep styling out of saved .md #5

Merged
brenno merged 8 commits from feature/live-annotations-clean-md into main 2026-06-11 17:31:10 +00:00
5 changed files with 393 additions and 2 deletions
Showing only changes of commit c6190dc31b - Show all commits

View file

@ -3,8 +3,10 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'l10n/app_localizations.dart'; import 'l10n/app_localizations.dart';
import 'state/settings_provider.dart'; import 'state/settings_provider.dart';
import 'state/consent_provider.dart';
import 'theme/app_theme.dart'; import 'theme/app_theme.dart';
import 'widgets/app_shell.dart'; import 'widgets/app_shell.dart';
import 'widgets/dialogs/consent_dialog.dart';
class OciDeckApp extends ConsumerWidget { class OciDeckApp extends ConsumerWidget {
const OciDeckApp({super.key}); const OciDeckApp({super.key});
@ -47,7 +49,39 @@ class OciDeckApp extends ConsumerWidget {
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
], ],
home: const AppShell(), home: const _ConsentGate(),
); );
} }
} }
class _ConsentGate extends ConsumerWidget {
const _ConsentGate();
@override
Widget build(BuildContext context, WidgetRef ref) {
final consent = ref.watch(consentProvider);
if (consent.isLoading) {
return Scaffold(
body: Center(
child: const CircularProgressIndicator(),
),
);
}
if (!consent.hasAccepted) {
return MaterialApp(
title: 'OciDeck',
theme: ThemeData.light(),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: ConsentDialog(),
),
),
);
}
return const AppShell();
}
}

View file

@ -4191,5 +4191,22 @@ const _dutchSourceStringAdditions = {
'Alle afbeeldingen hebben tags.': 'Tur imágen tin tag.', 'Alle afbeeldingen hebben tags.': 'Tur imágen tin tag.',
'Zet het filter uit om alles weer te zien.': 'Zet het filter uit om alles weer te zien.':
'Paga e filter pa mira tur kos atrobe.', 'Paga e filter pa mira tur kos atrobe.',
'Welkom bij OciDeck': 'Welcome to OciDeck',
'Privacy en gebruik': 'Privacy and Usage',
'OciDeck is een lokale desktop-applicatie. Uw presentaties en gegevens worden uitsluitend op uw computer opgeslagen.':
'OciDeck is a local desktop application. Your presentations and data are stored exclusively on your computer.',
'De app verzamelt geen persoonlijke gegevens, geen statistieken en geen gebruiksgegevens. Uw privacy is onze prioriteit.':
'The app collects no personal data, no statistics, and no usage data. Your privacy is our priority.',
'Alle gegevens die u in OciDeck invoert, blijven op uw lokale systeem en worden niet naar externe servers gestuurd.':
'All data you enter in OciDeck remains on your local system and is not sent to external servers.',
'Licentie (MIT)': 'License (MIT)',
'Door op "Akkoord gaan" te klikken, accepteert u deze voorwaarden en gaat u akkoord met het gebruik van OciDeck.':
'By clicking "Agree", you accept these terms and agree to the use of OciDeck.',
'Volledige licentie online': 'Full license online',
'Akkoord gaan': 'Agree',
'Toestemming intrekken': 'Revoke Consent',
'Toestemming ingetrokken': 'Consent revoked',
'U moet eerst de privacy- en gebruiksvoorwaarden accepteren voordat u OciDeck kunt gebruiken.':
'You must accept the privacy and usage terms before you can use OciDeck.',
}, },
}; };

View file

@ -0,0 +1,67 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
const _consentKey = 'app_consent_accepted';
final consentProvider =
NotifierProvider<ConsentNotifier, ConsentState>(() {
return ConsentNotifier();
});
class ConsentState {
final bool hasAccepted;
final bool isLoading;
const ConsentState({
required this.hasAccepted,
this.isLoading = false,
});
ConsentState copyWith({
bool? hasAccepted,
bool? isLoading,
}) {
return ConsentState(
hasAccepted: hasAccepted ?? this.hasAccepted,
isLoading: isLoading ?? this.isLoading,
);
}
}
class ConsentNotifier extends Notifier<ConsentState> {
@override
ConsentState build() {
_initialize();
return const ConsentState(hasAccepted: false, isLoading: true);
}
Future<void> _initialize() async {
try {
final prefs = await SharedPreferences.getInstance();
final hasAccepted = prefs.getBool(_consentKey) ?? false;
state = state.copyWith(hasAccepted: hasAccepted, isLoading: false);
} catch (e) {
state = state.copyWith(isLoading: false);
}
}
Future<void> acceptConsent() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_consentKey, true);
state = state.copyWith(hasAccepted: true);
} catch (e) {
state = state.copyWith(hasAccepted: true);
}
}
Future<void> revokeConsent() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_consentKey, false);
state = state.copyWith(hasAccepted: false);
} catch (e) {
state = state.copyWith(hasAccepted: false);
}
}
}

View file

@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../l10n/app_localizations.dart';
import '../../state/consent_provider.dart';
class ConsentDialog extends ConsumerStatefulWidget {
const ConsentDialog({super.key});
@override
ConsumerState<ConsentDialog> createState() => _ConsentDialogState();
}
class _ConsentDialogState extends ConsumerState<ConsentDialog> {
bool _licenseExpanded = false;
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
return AlertDialog(
title: Text(l10n.d('Welkom bij OciDeck')),
content: SizedBox(
width: 600,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Privacy & Use Explanation
Text(
l10n.d('Privacy en gebruik'),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
border: Border.all(
color: theme.colorScheme.outlineVariant,
),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.d(
'OciDeck is een lokale desktop-applicatie. Uw presentaties en gegevens worden uitsluitend op uw computer opgeslagen.',
),
style: const TextStyle(fontSize: 12),
),
const SizedBox(height: 10),
Text(
l10n.d(
'De app verzamelt geen persoonlijke gegevens, geen statistieken en geen gebruiksgegevens. Uw privacy is onze prioriteit.',
),
style: const TextStyle(fontSize: 12),
),
const SizedBox(height: 10),
Text(
l10n.d(
'Alle gegevens die u in OciDeck invoert, blijven op uw lokale systeem en worden niet naar externe servers gestuurd.',
),
style: const TextStyle(fontSize: 12),
),
],
),
),
const SizedBox(height: 18),
// License Section
ExpansionTile(
title: Text(
l10n.d('Licentie (MIT)'),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
initiallyExpanded: _licenseExpanded,
onExpansionChanged: (expanded) {
setState(() => _licenseExpanded = expanded);
},
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
border: Border.all(
color: theme.colorScheme.outlineVariant,
),
borderRadius: BorderRadius.circular(8),
),
constraints: const BoxConstraints(maxHeight: 250),
child: SingleChildScrollView(
child: Text(
l10n.d(_getLicenseText()),
style: const TextStyle(
fontSize: 11,
fontFamily: 'monospace',
color: Color(0xFF475569),
),
),
),
),
],
),
const SizedBox(height: 18),
// Confirmation Section
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer
.withValues(alpha: 0.2),
border: Border.all(
color: theme.colorScheme.primary.withValues(alpha: 0.3),
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
l10n.d(
'Door op "Akkoord gaan" te klikken, accepteert u deze voorwaarden en gaat u akkoord met het gebruik van OciDeck.',
),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => _launchLicense(context),
child: Text(l10n.d('Volledige licentie online')),
),
const Spacer(),
ElevatedButton(
onPressed: () => _acceptConsent(ref),
child: Text(l10n.d('Akkoord gaan')),
),
],
);
}
String _getLicenseText() {
return '''MIT License
Copyright (c) 2024 De Winter Information Solutions
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''';
}
void _launchLicense(BuildContext context) async {
const url =
'https://github.com/yourusername/ocideck/blob/main/LICENSE.md';
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
}
}
void _acceptConsent(WidgetRef ref) {
ref.read(consentProvider.notifier).acceptConsent();
}
}

View file

@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/settings.dart'; import '../../models/settings.dart';
import '../../state/settings_provider.dart'; import '../../state/settings_provider.dart';
import '../../state/tabs_provider.dart'; import '../../state/tabs_provider.dart';
import '../../state/consent_provider.dart';
import '../../theme/app_theme.dart'; import '../../theme/app_theme.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
@ -191,7 +192,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
: profiles.first.name; : profiles.first.name;
return DefaultTabController( return DefaultTabController(
length: 5, length: 6,
child: AlertDialog( child: AlertDialog(
title: Text(l10n.t('settings')), title: Text(l10n.t('settings')),
content: SizedBox( content: SizedBox(
@ -223,6 +224,10 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
icon: const Icon(Icons.image_outlined), icon: const Icon(Icons.image_outlined),
text: l10n.t('settingsLogo'), text: l10n.t('settingsLogo'),
), ),
Tab(
icon: const Icon(Icons.privacy_tip_outlined),
text: l10n.d('Privacy'),
),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -234,6 +239,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
_tabBody(_styleTab(profiles, dropdownValue)), _tabBody(_styleTab(profiles, dropdownValue)),
_tabBody(_colorsTab()), _tabBody(_colorsTab()),
_tabBody(_logoTab()), _tabBody(_logoTab()),
_tabBody(_privacyTab()),
], ],
), ),
), ),
@ -1623,4 +1629,83 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
); );
return Color(value ?? 0xFFFFFFFF); return Color(value ?? 0xFFFFFFFF);
} }
Widget _privacyTab() {
final l10n = context.l10n;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle(l10n.d('Toestemming')),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF0F9FF),
border: Border.all(color: const Color(0xFFBFDBFE)),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.d('U hebt al toegestemd in het gebruik van OciDeck.'),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Text(
l10n.d(
'U kunt uw toestemming op elk moment intrekken. Na intrekking moet u deze voorwaarden opnieuw accepteren.',
),
style: const TextStyle(fontSize: 11, color: Color(0xFF475569)),
),
],
),
),
const SizedBox(height: 16),
Center(
child: ElevatedButton.icon(
onPressed: _revokeConsent,
icon: const Icon(Icons.undo, size: 16),
label: Text(l10n.d('Toestemming intrekken')),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
foregroundColor: Colors.white,
),
),
),
],
);
}
void _revokeConsent() {
final l10n = context.l10n;
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(l10n.d('Toestemming intrekken?')),
content: Text(
l10n.d(
'Als u uw toestemming intrekt, moet u deze voorwaarden opnieuw accepteren wanneer u OciDeck opnieuw start.',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(l10n.t('cancel')),
),
ElevatedButton(
onPressed: () {
ref.read(consentProvider.notifier).revokeConsent();
Navigator.pop(ctx);
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red[600]),
child: Text(
l10n.d('Intrekken'),
style: const TextStyle(color: Colors.white),
),
),
],
),
);
}
} }