Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,677 changes: 52 additions & 1,625 deletions app/lib/pages/profile_page.dart

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions app/lib/widgets/profile/about_settings_section.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:papyrus/widgets/settings/settings_row.dart';
import 'package:papyrus/widgets/settings/settings_section.dart';

/// About settings section for ProfilePage.
class AboutSettingsSection extends StatelessWidget {
final bool isDesktop;

const AboutSettingsSection({super.key, this.isDesktop = false});

static void showLicenses(BuildContext context) {
showLicensePage(context: context, applicationName: 'Papyrus', applicationVersion: '1.0.0');
}

@override
Widget build(BuildContext context) {
if (isDesktop) {
return _buildDesktop(context);
}
return _buildMobile(context);
}

Widget _buildMobile(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SettingsSectionHeader(title: 'About'),
const SettingsRow(label: 'Version', value: '1.0.0', showChevron: false),
SettingsRow(label: 'Licenses', onTap: () => showLicenses(context)),
SettingsRow(label: 'Support', onTap: () {}),
SettingsRow(label: "What's new", onTap: () {}),
SettingsRow(label: 'GitHub repository', onTap: () {}),
],
);
}

Widget _buildDesktop(BuildContext context) {
return SettingsCard(
children: [
const SettingsRow(label: 'Version', value: '1.0.0', showChevron: false),
SettingsRow(label: "What's new", onTap: () {}),
SettingsRow(label: 'Licenses', onTap: () => showLicenses(context)),
SettingsRow(label: 'Support', onTap: () {}),
SettingsRow(label: 'GitHub repository', onTap: () {}),
],
);
}
}
80 changes: 80 additions & 0 deletions app/lib/widgets/profile/accessibility_settings_section.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:papyrus/providers/preferences_provider.dart';
import 'package:papyrus/themes/design_tokens.dart';
import 'package:papyrus/widgets/settings/settings_row.dart';
import 'package:papyrus/widgets/settings/settings_section.dart';
import 'package:provider/provider.dart';

/// Accessibility settings section for ProfilePage.
class AccessibilitySettingsSection extends StatelessWidget {
final bool isDesktop;

const AccessibilitySettingsSection({super.key, this.isDesktop = false});

@override
Widget build(BuildContext context) {
if (isDesktop) {
return _buildDesktop(context);
}
return _buildMobile(context);
}

Widget _buildMobile(BuildContext context) {
final prefs = context.watch<PreferencesProvider>();

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SettingsSectionHeader(title: 'Accessibility'),
SettingsToggleRow(
label: 'Reduce animations',
value: prefs.reduceAnimations,
onChanged: (value) => prefs.reduceAnimations = value,
),
SettingsToggleRow(
label: 'Dyslexia-friendly font',
value: prefs.dyslexiaFont,
onChanged: (value) => prefs.dyslexiaFont = value,
),
],
);
}

Widget _buildDesktop(BuildContext context) {
final prefs = context.watch<PreferencesProvider>();

return SettingsCard(
children: [
SettingsToggleRow(
label: 'Reduce animations',
value: prefs.reduceAnimations,
onChanged: (value) => prefs.reduceAnimations = value,
),
Padding(
padding: const EdgeInsets.only(left: Spacing.sm, right: Spacing.sm, bottom: Spacing.md),
child: Text(
'Minimizes motion effects throughout the app. '
'Separate from e-ink mode.',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
SettingsToggleRow(
label: 'Dyslexia-friendly font',
value: prefs.dyslexiaFont,
onChanged: (value) => prefs.dyslexiaFont = value,
),
Padding(
padding: const EdgeInsets.only(left: Spacing.sm, right: Spacing.sm, bottom: Spacing.md),
child: Text(
'Use OpenDyslexic font across the app interface.',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
],
);
}
}
145 changes: 145 additions & 0 deletions app/lib/widgets/profile/account_settings_section.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:papyrus/providers/auth_provider.dart';
import 'package:papyrus/themes/design_tokens.dart';
import 'package:papyrus/widgets/settings/settings_row.dart';
import 'package:papyrus/widgets/settings/settings_section.dart';
import 'package:provider/provider.dart';

/// Account settings section for ProfilePage.
class AccountSettingsSection extends StatelessWidget {
final bool isDesktop;

const AccountSettingsSection({super.key, this.isDesktop = false});

@override
Widget build(BuildContext context) {
if (isDesktop) {
return _buildDesktop(context);
}
return _buildMobile(context);
}

Widget _buildMobile(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SettingsSectionHeader(title: 'Account'),
SettingsRow(label: 'Change password', onTap: () {}),
SettingsRow(label: 'Linked accounts', onTap: () {}),
SettingsRow(label: 'Two-factor authentication', onTap: () {}),
SettingsRow(label: 'Active sessions', onTap: () {}),
SettingsRow(label: 'Delete account', onTap: () {}),
],
);
}

Widget _buildDesktop(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final auth = context.watch<AuthProvider>();
final user = auth.user;
final displayName = user?.displayName ?? 'Anonymous User';
final userEmail = user?.email;
final email = (userEmail != null && userEmail.trim().isNotEmpty) ? userEmail : 'No email provided';

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsCard(
children: [
Row(
children: [
_buildAvatar(context, displayName: displayName, avatarUrl: user?.avatarUrl, size: 96),
const SizedBox(width: Spacing.lg),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(displayName, style: textTheme.headlineSmall),
const SizedBox(height: Spacing.xs),
Text(email, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
const SizedBox(height: Spacing.md),
Align(
alignment: Alignment.centerLeft,
child: OutlinedButton(
onPressed: () => context.go('/profile/edit'),
child: const Text('Edit profile'),
),
),
],
),
),
],
),
],
),
const SizedBox(height: Spacing.lg),
SettingsCard(
title: 'Security',
children: [
SettingsRow(label: 'Change password', onTap: () {}),
SettingsRow(label: 'Two-factor authentication', onTap: () {}),
SettingsRow(label: 'Active sessions', onTap: () {}),
],
),
const SizedBox(height: Spacing.lg),
SettingsCard(
title: 'Connected accounts',
children: [SettingsRow(label: 'Google', value: 'Not connected', onTap: () {})],
),
const SizedBox(height: Spacing.lg),
SettingsCard(
title: 'Danger zone',
children: [SettingsRow(label: 'Delete account', onTap: () {})],
),
],
);
}

Widget _buildAvatar(
BuildContext context, {
required String displayName,
required String? avatarUrl,
required double size,
}) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final initials = _getInitials(displayName);

return Container(
width: size,
height: size,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(size / 2), color: colorScheme.primaryContainer),
clipBehavior: Clip.antiAlias,
child: avatarUrl != null && avatarUrl.isNotEmpty
? Image.network(
avatarUrl,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => Center(
child: Text(
initials,
style: textTheme.headlineMedium?.copyWith(
color: colorScheme.onPrimaryContainer,
fontSize: size * 0.35,
),
),
),
)
: Center(
child: Text(
initials,
style: textTheme.headlineMedium?.copyWith(color: colorScheme.onPrimaryContainer, fontSize: size * 0.35),
),
),
);
}

String _getInitials(String name) {
final parts = name.split(' ');
if (parts.length >= 2) {
return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
}
return name.isNotEmpty ? name[0].toUpperCase() : '?';
}
}
106 changes: 106 additions & 0 deletions app/lib/widgets/profile/appearance_settings_section.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:papyrus/providers/preferences_provider.dart';
import 'package:papyrus/themes/design_tokens.dart';
import 'package:papyrus/widgets/profile/profile_picker_sheet.dart';
import 'package:papyrus/widgets/settings/settings_row.dart';
import 'package:papyrus/widgets/settings/settings_section.dart';
import 'package:provider/provider.dart';

/// Appearance and theme settings section for ProfilePage.
class AppearanceSettingsSection extends StatelessWidget {
final bool isDesktop;

const AppearanceSettingsSection({super.key, this.isDesktop = false});

static String getThemeLabel(String theme) {
switch (theme) {
case 'light':
return 'Light';
case 'dark':
return 'Dark';
case 'eink':
return 'E-ink';
case 'system':
default:
return 'System';
}
}

@override
Widget build(BuildContext context) {
if (isDesktop) {
return _buildDesktop(context);
}
return _buildMobile(context);
}

Widget _buildMobile(BuildContext context) {
final prefs = context.watch<PreferencesProvider>();

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SettingsSectionHeader(title: 'Appearance'),
SettingsRow(label: 'Theme', value: getThemeLabel(prefs.themeModePref), onTap: () => _showThemePicker(context)),
],
);
}

Widget _buildDesktop(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsCard(title: 'Theme', children: [_buildThemeRadioGroup(context)]),
],
);
}

Widget _buildThemeRadioGroup(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;

return Material(
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(AppRadius.md),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
_buildRadioTile(context, 'Light', 'light'),
_buildRadioTile(context, 'Dark', 'dark'),
_buildRadioTile(context, 'E-ink', 'eink'),
_buildRadioTile(context, 'System default', 'system'),
],
),
);
}

Widget _buildRadioTile(BuildContext context, String label, String value) {
final prefs = context.watch<PreferencesProvider>();
final isSelected = prefs.themeModePref == value;

return ListTile(
title: Text(label),
leading: Radio<String>(
value: value,
// ignore: deprecated_member_use
groupValue: prefs.themeModePref,
// ignore: deprecated_member_use
onChanged: (newValue) {
if (newValue != null) prefs.themeModePref = newValue;
},
),
onTap: () => prefs.themeModePref = value,
selected: isSelected,
);
}

void _showThemePicker(BuildContext context) {
final prefs = context.read<PreferencesProvider>();

showProfilePickerSheet(
context,
items: const [('Light', 'light'), ('Dark', 'dark'), ('E-ink', 'eink'), ('System', 'system')],
selected: prefs.themeModePref,
onSelected: (value) => prefs.themeModePref = value,
);
}
}
Loading
Loading