Frontend first draft
This commit is contained in:
107
frontend/pweb/lib/pages/settings/profile/account/avatar.dart
Normal file
107
frontend/pweb/lib/pages/settings/profile/account/avatar.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
//import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AvatarTile extends StatefulWidget {
|
||||
final String? avatarUrl;
|
||||
final String title;
|
||||
final String description;
|
||||
final String errorText;
|
||||
|
||||
const AvatarTile({
|
||||
super.key,
|
||||
required this.avatarUrl,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.errorText,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AvatarTile> createState() => _AvatarTileState();
|
||||
}
|
||||
|
||||
class _AvatarTileState extends State<AvatarTile> {
|
||||
static const double _avatarSize = 96.0;
|
||||
static const double _iconSize = 32.0;
|
||||
static const double _titleSpacing = 4.0;
|
||||
static const String _placeholderAsset = 'assets/images/avatar_placeholder.png';
|
||||
|
||||
bool _isHovering = false;
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
final picker = ImagePicker();
|
||||
final file = await picker.pickImage(source: ImageSource.gallery);
|
||||
if (file != null) {
|
||||
debugPrint('Selected new avatar: ${file.path}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final safeUrl =
|
||||
widget.avatarUrl?.trim().isNotEmpty == true ? widget.avatarUrl : null;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovering = true),
|
||||
onExit: (_) => setState(() => _isHovering = false),
|
||||
child: GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ClipOval(
|
||||
child: safeUrl != null
|
||||
? Image.network(
|
||||
safeUrl,
|
||||
width: _avatarSize,
|
||||
height: _avatarSize,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => _buildPlaceholder(),
|
||||
)
|
||||
: _buildPlaceholder(),
|
||||
),
|
||||
if (_isHovering)
|
||||
ClipOval(
|
||||
child: Container(
|
||||
width: _avatarSize,
|
||||
height: _avatarSize,
|
||||
color: theme.colorScheme.primary.withAlpha(90),
|
||||
child: Icon(
|
||||
Icons.camera_alt,
|
||||
color: theme.colorScheme.onSecondary,
|
||||
size: _iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: _titleSpacing),
|
||||
Text(
|
||||
loc.avatarHint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlaceholder() {
|
||||
return Image.asset(
|
||||
_placeholderAsset,
|
||||
width: _avatarSize,
|
||||
height: _avatarSize,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
}
|
||||
86
frontend/pweb/lib/pages/settings/profile/account/locale.dart
Normal file
86
frontend/pweb/lib/pages/settings/profile/account/locale.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/locale.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
import 'package:pweb/services/amplitude.dart';
|
||||
|
||||
|
||||
class LocalePicker extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const LocalePicker({
|
||||
super.key,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
static const double _pickerWidth = 300;
|
||||
static const double _iconSize = 20;
|
||||
static const double _gapMedium = 6;
|
||||
static const double _gapLarge = 8;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
return Consumer<LocaleProvider>(
|
||||
builder: (context, localeProvider, _) {
|
||||
final currentLocale = localeProvider.locale;
|
||||
final options = AppLocalizations.supportedLocales;
|
||||
|
||||
return SizedBox(
|
||||
width: _pickerWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.language_outlined, color: theme.colorScheme.primary, size: _iconSize),
|
||||
const SizedBox(width: _gapMedium),
|
||||
Text(title, style: theme.textTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: _gapLarge),
|
||||
DropdownButtonFormField<Locale>(
|
||||
initialValue: currentLocale,
|
||||
items: options
|
||||
.map(
|
||||
(locale) => DropdownMenuItem(
|
||||
value: locale,
|
||||
child: Text(_localizedLocaleName(locale, loc)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (locale) {
|
||||
if (locale != null) {
|
||||
localeProvider.setLocale(locale);
|
||||
AmplitudeService.localeChanged(locale);
|
||||
}
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _localizedLocaleName(Locale locale, AppLocalizations loc) {
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
return 'English';
|
||||
case 'ru':
|
||||
return 'Русский';
|
||||
case 'de':
|
||||
return 'Deutsch';
|
||||
default:
|
||||
return locale.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
127
frontend/pweb/lib/pages/settings/profile/account/name.dart
Normal file
127
frontend/pweb/lib/pages/settings/profile/account/name.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class AccountName extends StatefulWidget {
|
||||
final String name;
|
||||
final String title;
|
||||
final String hintText;
|
||||
final String errorText;
|
||||
|
||||
const AccountName({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.title,
|
||||
required this.hintText,
|
||||
required this.errorText,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AccountName> createState() => _AccountNameState();
|
||||
}
|
||||
|
||||
class _AccountNameState extends State<AccountName> {
|
||||
static const double _inputWidth = 200;
|
||||
static const double _spacing = 8;
|
||||
static const double _errorSpacing = 4;
|
||||
static const double _borderWidth = 2;
|
||||
|
||||
late final TextEditingController _controller;
|
||||
bool _isEditing = false;
|
||||
late String _originalName;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.name);
|
||||
_originalName = widget.name;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startEditing() => setState(() => _isEditing = true);
|
||||
|
||||
void _cancelEditing() {
|
||||
setState(() {
|
||||
_controller.text = _originalName;
|
||||
_isEditing = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _saveEditing() {
|
||||
setState(() {
|
||||
_originalName = _controller.text;
|
||||
_isEditing = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (_isEditing)
|
||||
SizedBox(
|
||||
width: _inputWidth,
|
||||
child: TextFormField(
|
||||
controller: _controller,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
isDense: true,
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: _borderWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
_originalName,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _spacing),
|
||||
if (_isEditing) ...[
|
||||
IconButton(
|
||||
icon: Icon(Icons.check, color: theme.colorScheme.primary),
|
||||
onPressed: _saveEditing,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: theme.colorScheme.error),
|
||||
onPressed: _cancelEditing,
|
||||
),
|
||||
] else
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit, color: theme.colorScheme.primary),
|
||||
onPressed: _startEditing,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: _errorSpacing),
|
||||
if (widget.errorText.isEmpty)
|
||||
Text(
|
||||
widget.errorText,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user