Live presenter annotations + keep styling out of saved .md #5
5 changed files with 393 additions and 2 deletions
36
lib/app.dart
36
lib/app.dart
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
67
lib/state/consent_provider.dart
Normal file
67
lib/state/consent_provider.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
188
lib/widgets/dialogs/consent_dialog.dart
Normal file
188
lib/widgets/dialogs/consent_dialog.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue