Frontend first draft

This commit is contained in:
Arseni
2025-11-13 15:06:15 +03:00
parent e47f343afb
commit ddb54ddfdc
504 changed files with 25498 additions and 1 deletions

View 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,
);
}
}

View 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();
}
}
}

View 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,
),
),
],
);
}
}