mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-13 10:42:12 +00:00
improved styling
This commit is contained in:
parent
24efc76c02
commit
e2cf5ec74a
20 changed files with 1434 additions and 932 deletions
|
|
@ -208,7 +208,8 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
_isTwonlyLocked = false;
|
||||
}),
|
||||
);
|
||||
} else if (!userService.currentUser.skipSetupPages && userService.currentUser.currentSetupPage != null) {
|
||||
} else if (!userService.currentUser.skipSetupPages &&
|
||||
userService.currentUser.currentSetupPage != null) {
|
||||
// This will only be shown in case the user have not skipped
|
||||
child = SetupView(
|
||||
onUpdate: () => setState(() {
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @registerSlogan.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stay in touch with friends privately and securely.'**
|
||||
/// **'Stay in touch privately.'**
|
||||
String get registerSlogan;
|
||||
|
||||
/// No description provided for @onboardingWelcomeTitle.
|
||||
|
|
@ -173,7 +173,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @registerUsernameSlogan.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your public username'**
|
||||
/// **'Create your account'**
|
||||
String get registerUsernameSlogan;
|
||||
|
||||
/// No description provided for @registerUsernameDecoration.
|
||||
|
|
@ -1205,8 +1205,8 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @userFoundBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Do you want to create a follow request?'**
|
||||
String get userFoundBody;
|
||||
/// **'Do you want to connect with {username}?'**
|
||||
String userFoundBody(String username);
|
||||
|
||||
/// No description provided for @errorInternalError.
|
||||
///
|
||||
|
|
@ -3689,6 +3689,48 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{Import 1 item} other{Import {count} items}}'**
|
||||
String importGalleryImportCount(num count);
|
||||
|
||||
/// No description provided for @emptyChatListTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Find your first friend'**
|
||||
String get emptyChatListTitle;
|
||||
|
||||
/// No description provided for @emptyChatListDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Let friends scan your QR code, or share them your profile.'**
|
||||
String get emptyChatListDesc;
|
||||
|
||||
/// No description provided for @emptyChatListShareBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Share your profile'**
|
||||
String get emptyChatListShareBtn;
|
||||
|
||||
/// No description provided for @emptyChatListScanBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'QR Code'**
|
||||
String get emptyChatListScanBtn;
|
||||
|
||||
/// No description provided for @emptyChatListAddUsernameBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'By Username'**
|
||||
String get emptyChatListAddUsernameBtn;
|
||||
|
||||
/// No description provided for @avatarCustomizeRandomize.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Randomize'**
|
||||
String get avatarCustomizeRandomize;
|
||||
|
||||
/// No description provided for @avatarCustomizeReset.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reset'**
|
||||
String get avatarCustomizeReset;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||
|
||||
@override
|
||||
String get registerSlogan =>
|
||||
'Privat und sicher mit Freunden in Kontakt bleiben.';
|
||||
String get registerSlogan => 'Privat in Kontakt bleiben.';
|
||||
|
||||
@override
|
||||
String get onboardingWelcomeTitle => 'Willkommen bei twonly!';
|
||||
|
|
@ -52,7 +51,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get onboardingGetStartedTitle => 'Auf geht\'s';
|
||||
|
||||
@override
|
||||
String get registerUsernameSlogan => 'Dein öffentlicher Benutzername';
|
||||
String get registerUsernameSlogan => 'Konto erstellen';
|
||||
|
||||
@override
|
||||
String get registerUsernameDecoration => 'Benutzername';
|
||||
|
|
@ -611,7 +610,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get userFoundBody => 'Möchtest du eine Folgeanfrage stellen?';
|
||||
String userFoundBody(String username) {
|
||||
return 'Möchtest du dich mit $username vernetzen?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get errorInternalError =>
|
||||
|
|
@ -2127,4 +2128,26 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get emptyChatListTitle => 'Finde deinen ersten Freund';
|
||||
|
||||
@override
|
||||
String get emptyChatListDesc =>
|
||||
'Lass Freunde deinen QR-Code scannen oder teile dein Profil mit ihnen.';
|
||||
|
||||
@override
|
||||
String get emptyChatListShareBtn => 'Profil teilen';
|
||||
|
||||
@override
|
||||
String get emptyChatListScanBtn => 'QR-Code';
|
||||
|
||||
@override
|
||||
String get emptyChatListAddUsernameBtn => 'Per Benutzername';
|
||||
|
||||
@override
|
||||
String get avatarCustomizeRandomize => 'Zufällig';
|
||||
|
||||
@override
|
||||
String get avatarCustomizeReset => 'Zurücksetzen';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get registerSlogan =>
|
||||
'Stay in touch with friends privately and securely.';
|
||||
String get registerSlogan => 'Stay in touch privately.';
|
||||
|
||||
@override
|
||||
String get onboardingWelcomeTitle => 'Welcome to twonly!';
|
||||
|
|
@ -51,7 +50,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get onboardingGetStartedTitle => 'Let\'s go!';
|
||||
|
||||
@override
|
||||
String get registerUsernameSlogan => 'Your public username';
|
||||
String get registerUsernameSlogan => 'Create your account';
|
||||
|
||||
@override
|
||||
String get registerUsernameDecoration => 'Username';
|
||||
|
|
@ -606,7 +605,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String get userFoundBody => 'Do you want to create a follow request?';
|
||||
String userFoundBody(String username) {
|
||||
return 'Do you want to connect with $username?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get errorInternalError =>
|
||||
|
|
@ -2109,4 +2110,26 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get emptyChatListTitle => 'Find your first friend';
|
||||
|
||||
@override
|
||||
String get emptyChatListDesc =>
|
||||
'Let friends scan your QR code, or share them your profile.';
|
||||
|
||||
@override
|
||||
String get emptyChatListShareBtn => 'Share your profile';
|
||||
|
||||
@override
|
||||
String get emptyChatListScanBtn => 'QR Code';
|
||||
|
||||
@override
|
||||
String get emptyChatListAddUsernameBtn => 'By Username';
|
||||
|
||||
@override
|
||||
String get avatarCustomizeRandomize => 'Randomize';
|
||||
|
||||
@override
|
||||
String get avatarCustomizeReset => 'Reset';
|
||||
}
|
||||
|
|
|
|||
208
lib/src/visual/elements/my_button.element.dart
Normal file
208
lib/src/visual/elements/my_button.element.dart
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
|
||||
enum MyButtonVariant {
|
||||
primary,
|
||||
secondary,
|
||||
text,
|
||||
primaryDense,
|
||||
secondaryDense,
|
||||
}
|
||||
|
||||
class MyButton extends StatefulWidget {
|
||||
const MyButton({
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
this.onLongPress,
|
||||
this.variant = MyButtonVariant.primary,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onLongPress;
|
||||
final MyButtonVariant variant;
|
||||
|
||||
@override
|
||||
State<MyButton> createState() => _MyButtonState();
|
||||
}
|
||||
|
||||
class _MyButtonState extends State<MyButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller =
|
||||
AnimationController(
|
||||
vsync: this,
|
||||
lowerBound: double.negativeInfinity,
|
||||
upperBound: double.infinity,
|
||||
value: 0,
|
||||
)..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_controller.animateTo(
|
||||
1,
|
||||
duration: const Duration(milliseconds: 60),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_bounce();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_bounce();
|
||||
}
|
||||
}
|
||||
|
||||
void _bounce() {
|
||||
const spring = SpringDescription(
|
||||
mass: 1,
|
||||
stiffness: 400,
|
||||
damping: 15,
|
||||
);
|
||||
final simulation = SpringSimulation(
|
||||
spring,
|
||||
_controller.value,
|
||||
0,
|
||||
_controller.velocity,
|
||||
);
|
||||
_controller.animateWith(simulation);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 0 (unpressed) -> scale 1.0
|
||||
// 1 (pressed) -> scale 0.98 (subtle bounce)
|
||||
final scale = 1.0 - (_controller.value * 0.02);
|
||||
final isEnabled = widget.onPressed != null || widget.onLongPress != null;
|
||||
final isDark = isDarkMode(context);
|
||||
|
||||
late final ButtonStyle buttonStyle;
|
||||
switch (widget.variant) {
|
||||
case MyButtonVariant.primary:
|
||||
buttonStyle = FilledButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.black87,
|
||||
minimumSize: const Size.fromHeight(60),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
elevation: 0,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
case MyButtonVariant.secondary:
|
||||
buttonStyle = FilledButton.styleFrom(
|
||||
backgroundColor: isDark ? Colors.grey[800] : Colors.grey[200],
|
||||
foregroundColor: isDark ? Colors.white : Colors.black87,
|
||||
minimumSize: const Size.fromHeight(60),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
elevation: 0,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
case MyButtonVariant.text:
|
||||
buttonStyle = TextButton.styleFrom(
|
||||
minimumSize: const Size(0, 50),
|
||||
foregroundColor: isDark
|
||||
? Colors.white.withValues(alpha: 0.7)
|
||||
: Colors.black.withValues(alpha: 0.7),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
);
|
||||
case MyButtonVariant.primaryDense:
|
||||
buttonStyle = FilledButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.black87,
|
||||
minimumSize: const Size(0, 40),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
case MyButtonVariant.secondaryDense:
|
||||
buttonStyle = FilledButton.styleFrom(
|
||||
backgroundColor: isDark ? Colors.grey[800] : Colors.grey[200],
|
||||
foregroundColor: isDark ? Colors.white : Colors.black87,
|
||||
minimumSize: const Size(0, 40),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final childButton = widget.variant == MyButtonVariant.text
|
||||
? TextButton(
|
||||
style: buttonStyle,
|
||||
onPressed: isEnabled ? () {} : null,
|
||||
child: widget.child,
|
||||
)
|
||||
: FilledButton(
|
||||
style: buttonStyle,
|
||||
onPressed: isEnabled ? () {} : null,
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapDown: isEnabled ? _onTapDown : null,
|
||||
onTapUp: isEnabled ? _onTapUp : null,
|
||||
onTapCancel: isEnabled ? _onTapCancel : null,
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: AbsorbPointer(
|
||||
child: childButton,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
237
lib/src/visual/elements/my_input.element.dart
Normal file
237
lib/src/visual/elements/my_input.element.dart
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class MyInput extends StatefulWidget {
|
||||
const MyInput({
|
||||
required this.controller,
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
this.inputFormatters,
|
||||
this.hintText,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.keyboardType,
|
||||
this.autofocus = false,
|
||||
this.errorText,
|
||||
this.obscureText = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final String? hintText;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
final TextInputType? keyboardType;
|
||||
final bool autofocus;
|
||||
final String? errorText;
|
||||
final bool obscureText;
|
||||
|
||||
@override
|
||||
State<MyInput> createState() => _MyInputState();
|
||||
}
|
||||
|
||||
class _MyInputState extends State<MyInput> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller =
|
||||
AnimationController(
|
||||
vsync: this,
|
||||
lowerBound: double.negativeInfinity,
|
||||
upperBound: double.infinity,
|
||||
value: 0,
|
||||
)..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
_controller.animateTo(
|
||||
1,
|
||||
duration: const Duration(milliseconds: 60),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
_bounce();
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
_bounce();
|
||||
}
|
||||
|
||||
void _bounce() {
|
||||
const spring = SpringDescription(
|
||||
mass: 1,
|
||||
stiffness: 400,
|
||||
damping: 15,
|
||||
);
|
||||
final simulation = SpringSimulation(
|
||||
spring,
|
||||
_controller.value,
|
||||
0,
|
||||
_controller.velocity,
|
||||
);
|
||||
_controller.animateWith(simulation);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 0 (unpressed) -> scale 1.0
|
||||
// 1 (pressed) -> scale 0.98 (subtle bounce)
|
||||
final scale = 1.0 - (_controller.value * 0.02);
|
||||
final isDark = isDarkMode(context);
|
||||
|
||||
final inputFillColor = isDark
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: Colors.black.withValues(alpha: 0.05);
|
||||
|
||||
final inputBorderColor = isDark
|
||||
? Colors.white.withValues(alpha: 0.15)
|
||||
: Colors.black.withValues(alpha: 0.15);
|
||||
|
||||
final inputHintColor = isDark
|
||||
? Colors.white.withValues(alpha: 0.5)
|
||||
: Colors.black.withValues(alpha: 0.5);
|
||||
|
||||
final prefixIconColor = isDark
|
||||
? Colors.white.withValues(alpha: 0.6)
|
||||
: Colors.black.withValues(alpha: 0.6);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
onChanged: widget.onChanged,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onTapOutside: (event) {
|
||||
final pointer = event.pointer;
|
||||
final startPosition = event.position;
|
||||
var moved = false;
|
||||
|
||||
void handlePointerEvent(PointerEvent routeEvent) {
|
||||
if (routeEvent is PointerMoveEvent) {
|
||||
if ((routeEvent.position - startPosition).distance > 10) {
|
||||
moved = true;
|
||||
}
|
||||
} else if (routeEvent is PointerUpEvent) {
|
||||
GestureBinding.instance.pointerRouter.removeRoute(
|
||||
pointer,
|
||||
handlePointerEvent,
|
||||
);
|
||||
if (!moved) {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
}
|
||||
} else if (routeEvent is PointerCancelEvent) {
|
||||
GestureBinding.instance.pointerRouter.removeRoute(
|
||||
pointer,
|
||||
handlePointerEvent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GestureBinding.instance.pointerRouter.addRoute(
|
||||
pointer,
|
||||
handlePointerEvent,
|
||||
);
|
||||
},
|
||||
inputFormatters: widget.inputFormatters,
|
||||
keyboardType: widget.keyboardType,
|
||||
autofocus: widget.autofocus,
|
||||
obscureText: widget.obscureText,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
hintStyle: TextStyle(
|
||||
color: inputHintColor,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: inputFillColor,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 18,
|
||||
horizontal: 24,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: inputBorderColor,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: inputBorderColor,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.redAccent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
errorStyle: const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
errorText: widget.errorText,
|
||||
prefixIcon: widget.prefixIcon != null
|
||||
? IconTheme(
|
||||
data: IconThemeData(
|
||||
color: prefixIconColor,
|
||||
),
|
||||
child: widget.prefixIcon!,
|
||||
)
|
||||
: null,
|
||||
suffixIcon: widget.suffixIcon != null
|
||||
? IconTheme(
|
||||
data: IconThemeData(
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
child: widget.suffixIcon!,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart' show FaIcon, FontAwesomeIcons;
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart'
|
||||
show FaIcon, FontAwesomeIcons;
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart'
|
||||
show ProfileQrCodeComp;
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class EmptyChatListComp extends StatelessWidget {
|
||||
const EmptyChatListComp({super.key});
|
||||
|
|
@ -17,7 +19,8 @@ class EmptyChatListComp extends StatelessWidget {
|
|||
try {
|
||||
final pubKey = await getUserPublicKey();
|
||||
final params = ShareParams(
|
||||
text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
||||
text:
|
||||
'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
||||
);
|
||||
await SharePlus.instance.share(params);
|
||||
} catch (e) {
|
||||
|
|
@ -37,16 +40,16 @@ class EmptyChatListComp extends StatelessWidget {
|
|||
height: 24,
|
||||
width: double.infinity,
|
||||
),
|
||||
const Text(
|
||||
'Find your first friend',
|
||||
Text(
|
||||
context.lang.emptyChatListTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Let friends scan your QR code, or share them your profile.',
|
||||
context.lang.emptyChatListDesc,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: context.color.onSurface.withValues(alpha: 0.6),
|
||||
|
|
@ -56,61 +59,67 @@ class EmptyChatListComp extends StatelessWidget {
|
|||
const SizedBox(height: 36),
|
||||
const Center(child: ProfileQrCodeComp()),
|
||||
const SizedBox(height: 36),
|
||||
// 3. Action Buttons
|
||||
// Button 1: Share Profile (Full Width)
|
||||
FilledButton.icon(
|
||||
style: primaryColorButtonStyle,
|
||||
MyButton(
|
||||
onPressed: () => _shareProfile(context),
|
||||
icon: const FaIcon(FontAwesomeIcons.shareNodes, size: 20),
|
||||
label: const Text(
|
||||
'Share your profile',
|
||||
style: TextStyle(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.shareNodes, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.lang.emptyChatListShareBtn,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Button Row: Scan QR Code & Enter Username
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
style: secondaryGreyButtonStyle(context),
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: () => context.push(Routes.cameraQRScanner),
|
||||
icon: const Icon(Icons.qr_code_scanner_rounded, size: 20),
|
||||
label: const FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'Scan QR Code',
|
||||
style: TextStyle(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.qr_code_scanner_rounded, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.lang.emptyChatListScanBtn,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
style: secondaryGreyButtonStyle(context),
|
||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||
icon: const Icon(Icons.person_add_rounded, size: 20),
|
||||
label: const FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'Add by Username',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.person_add_rounded, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.lang.emptyChatListAddUsernameBtn,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 50),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ class _AddContactViaQrLinkViewState extends State<AddContactViaQrLinkView> {
|
|||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
context.lang.userFoundBody,
|
||||
context.lang.userFoundBody(widget.profile.username),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: context.color.onSurfaceVariant,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import 'package:twonly/src/services/signal/identity.signal.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||
import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart';
|
||||
import 'package:twonly/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart';
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
final TextEditingController _usernameController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
bool hasRequestedUsers = false;
|
||||
String? _searchError;
|
||||
|
||||
List<Contact> _openRequestsContacts = [];
|
||||
late StreamSubscription<List<Contact>> _contactsStream;
|
||||
|
|
@ -63,14 +65,18 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
},
|
||||
);
|
||||
|
||||
_newAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchNewAnnouncedUsersWithRelations().listen((update) {
|
||||
_newAnnouncedUsersStream = twonlyDB.userDiscoveryDao
|
||||
.watchNewAnnouncedUsersWithRelations()
|
||||
.listen((update) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_newAnnouncedUsers = update;
|
||||
});
|
||||
}
|
||||
});
|
||||
_allAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchAllAnnouncedUsersWithRelations().listen((update) {
|
||||
_allAnnouncedUsersStream = twonlyDB.userDiscoveryDao
|
||||
.watchAllAnnouncedUsersWithRelations()
|
||||
.listen((update) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_allAnnouncedUsers = update;
|
||||
|
|
@ -90,7 +96,8 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
Future<void> _shareProfile() async {
|
||||
final pubKey = await getUserPublicKey();
|
||||
final params = ShareParams(
|
||||
text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
||||
text:
|
||||
'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
||||
);
|
||||
await SharePlus.instance.share(params);
|
||||
}
|
||||
|
|
@ -164,18 +171,20 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
});
|
||||
|
||||
if (userdata == null) {
|
||||
await showAlertDialog(
|
||||
context,
|
||||
context.lang.searchUsernameNotFound,
|
||||
context.lang.searchUsernameNotFoundBody(username),
|
||||
);
|
||||
setState(() {
|
||||
_searchError = context.lang.searchUsernameNotFound;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_searchError = null;
|
||||
});
|
||||
|
||||
final addUser = await showAlertDialog(
|
||||
context,
|
||||
context.lang.userFound(username),
|
||||
context.lang.userFoundBody,
|
||||
context.lang.userFoundBody(username),
|
||||
);
|
||||
|
||||
if (!addUser || !mounted) return;
|
||||
|
|
@ -190,7 +199,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
),
|
||||
);
|
||||
|
||||
if (widget.publicKey != null && mounted && widget.publicKey!.equals(userdata.publicIdentityKey)) {
|
||||
if (widget.publicKey != null &&
|
||||
mounted &&
|
||||
widget.publicKey!.equals(userdata.publicIdentityKey)) {
|
||||
final markAsVerified = await showAlertDialog(
|
||||
context,
|
||||
context.lang.linkFromUsername(username),
|
||||
|
|
@ -218,137 +229,135 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
child: SearchBar(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
||||
child: MyInput(
|
||||
controller: _usernameController,
|
||||
hintText: context.lang.searchUsernameInput,
|
||||
elevation: const WidgetStatePropertyAll(0),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
context.color.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||
),
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
leading: const Icon(Icons.search, size: 20, color: Colors.grey),
|
||||
trailing: [
|
||||
if (_usernameController.text.isNotEmpty) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear, size: 20),
|
||||
onPressed: () {
|
||||
_usernameController.clear();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (_isLoading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.magnifyingGlassPlus,
|
||||
size: 20,
|
||||
color: context.color.primary,
|
||||
),
|
||||
onPressed: () => _requestNewUserByUsername(
|
||||
_usernameController.text,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
IconButton(
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.camera,
|
||||
size: 20,
|
||||
color: context.color.primary,
|
||||
),
|
||||
onPressed: () => context.push(Routes.cameraQRScanner),
|
||||
tooltip: context.lang.scanOtherProfile,
|
||||
),
|
||||
],
|
||||
],
|
||||
onSubmitted: _requestNewUserByUsername,
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
errorText: _searchError,
|
||||
onChanged: (value) {
|
||||
_usernameController.text = value.toLowerCase();
|
||||
_usernameController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _usernameController.text.length),
|
||||
);
|
||||
setState(() {
|
||||
_searchError = null;
|
||||
});
|
||||
},
|
||||
onSubmitted: _requestNewUserByUsername,
|
||||
suffixIcon: _usernameController.text.isNotEmpty
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton.filled(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor:
|
||||
context.color.surfaceContainerHighest,
|
||||
foregroundColor: context.color.onSurface,
|
||||
minimumSize: const Size(32, 32),
|
||||
padding: EdgeInsets.zero,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
iconSize: 16,
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_usernameController.clear();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 0),
|
||||
if (_isLoading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
context.color.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
IconButton.filled(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: context.color.primary,
|
||||
foregroundColor: context.color.onPrimary,
|
||||
minimumSize: const Size(32, 32),
|
||||
padding: EdgeInsets.zero,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
iconSize: 16,
|
||||
icon: const Icon(Icons.person_add_rounded),
|
||||
onPressed: () => _requestNewUserByUsername(
|
||||
_usernameController.text,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: IconButton.filled(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: context.color.primary,
|
||||
foregroundColor: context.color.onPrimary,
|
||||
minimumSize: const Size(36, 36),
|
||||
padding: EdgeInsets.zero,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
iconSize: 18,
|
||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||
onPressed: () => context.push(Routes.cameraQRScanner),
|
||||
tooltip: context.lang.scanOtherProfile,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Column(
|
||||
child: Row(
|
||||
children: [
|
||||
MyButton(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
onPressed: _shareProfile,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.black87,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 10,
|
||||
),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: _shareProfile,
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.shareNodes,
|
||||
size: 14,
|
||||
),
|
||||
label: Text(
|
||||
const FaIcon(FontAwesomeIcons.shareNodes, size: 14),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.lang.shareYourProfile,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: context.color.secondaryContainer,
|
||||
foregroundColor: context.color.onSecondaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 10,
|
||||
),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: _showMyQrCode,
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.qrcode,
|
||||
size: 14,
|
||||
),
|
||||
label: Text(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.qrcode, size: 14),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.lang.openYourOwnQRcode,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
|
||||
class OnboardingWrapper extends StatelessWidget {
|
||||
const OnboardingWrapper({
|
||||
required this.children,
|
||||
super.key,
|
||||
});
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = isDarkMode(context);
|
||||
final backgroundColor = isDark ? const Color(0xFF0F172A) : primaryColor;
|
||||
final topBlobColor = isDark
|
||||
? primaryColor.withValues(alpha: 0.15)
|
||||
: Colors.white.withValues(alpha: 0.1);
|
||||
final bottomBlobColor = isDark
|
||||
? primaryColor.withValues(alpha: 0.08)
|
||||
: Colors.black.withValues(alpha: 0.05);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Scaffold(
|
||||
backgroundColor: backgroundColor,
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -100,
|
||||
right: -100,
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: topBlobColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -50,
|
||||
left: -50,
|
||||
child: Container(
|
||||
width: 200,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: bottomBlobColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:introduction_screen/introduction_screen.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class OnboardingView extends StatelessWidget {
|
||||
const OnboardingView({required this.callbackOnSuccess, super.key});
|
||||
|
|
@ -53,19 +54,6 @@ class OnboardingView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
// PageViewModel(
|
||||
// title: context.lang.onboardingSendTwonliesTitle,
|
||||
// body: context.lang.onboardingSendTwonliesBody,
|
||||
// image: Center(
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.only(top: 100),
|
||||
// child: Lottie.asset(
|
||||
// 'assets/animations/twonlies.lottie',
|
||||
// repeat: false,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
PageViewModel(
|
||||
title: context.lang.onboardingNotProductTitle,
|
||||
bodyWidget: Column(
|
||||
|
|
@ -81,7 +69,7 @@ class OnboardingView extends StatelessWidget {
|
|||
right: 50,
|
||||
top: 20,
|
||||
),
|
||||
child: FilledButton(
|
||||
child: MyButton(
|
||||
onPressed: callbackOnSuccess,
|
||||
child: Text(context.lang.registerSubmitButton),
|
||||
),
|
||||
|
|
@ -97,17 +85,6 @@ class OnboardingView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
// PageViewModel(
|
||||
// title: context.lang.onboardingGetStartedTitle,
|
||||
// image: Center(
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.only(top: 100),
|
||||
// child: Lottie.asset(
|
||||
// 'assets/animations/rocket.lottie',
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
done: const Text(''),
|
||||
next: Text(context.lang.next),
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||
import 'package:restart_app/restart_app.dart';
|
||||
import 'package:twonly/src/services/backup.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/components/link_logo_animation.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/components/onboarding_wrapper.dart';
|
||||
|
||||
class BackupRecoveryView extends StatefulWidget {
|
||||
const BackupRecoveryView({super.key});
|
||||
|
|
@ -64,13 +63,94 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
});
|
||||
}
|
||||
|
||||
void _showBackupExplanation(BuildContext context) {
|
||||
final isDark = isDarkMode(context);
|
||||
final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
|
||||
final textColor = isDark ? Colors.white : Colors.black87;
|
||||
final subtitleColor = isDark ? Colors.white70 : Colors.black54;
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: backgroundColor,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(28),
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 12, 24, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.white24 : Colors.black12,
|
||||
borderRadius: BorderRadius.circular(2.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'twonly Backup',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
context.lang.backupTwonlySafeLongDesc,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1.5,
|
||||
color: subtitleColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
MyButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Got it'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = isDarkMode(context);
|
||||
final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white;
|
||||
final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100];
|
||||
final titleColor = isDark ? Colors.white : Colors.black87;
|
||||
final iconColor = isDark ? Colors.white70 : Colors.black54;
|
||||
|
||||
return OnboardingWrapper(
|
||||
return GestureDetector(
|
||||
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
|
|
@ -79,29 +159,25 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
icon: const Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
),
|
||||
color: Colors.white,
|
||||
color: iconColor,
|
||||
iconSize: 20,
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await showAlertDialog(
|
||||
context,
|
||||
'twonly Backup',
|
||||
context.lang.backupTwonlySafeLongDesc,
|
||||
);
|
||||
},
|
||||
onPressed: () => _showBackupExplanation(context),
|
||||
icon: const FaIcon(FontAwesomeIcons.circleInfo),
|
||||
color: Colors.white,
|
||||
color: iconColor,
|
||||
iconSize: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Center(
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: LinkLogoAnimation(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: LinkLogoAnimation(
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -110,83 +186,28 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
child: Text(
|
||||
context.lang.twonlySafeRecoverTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
color: titleColor,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark
|
||||
? Colors.black.withValues(alpha: 0.3)
|
||||
: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
MyInput(
|
||||
controller: usernameCtrl,
|
||||
onChanged: (value) => setState(() {}),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: context.lang.registerUsernameDecoration,
|
||||
hintStyle: TextStyle(
|
||||
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
||||
),
|
||||
filled: true,
|
||||
fillColor: inputColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.alternate_email,
|
||||
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
||||
),
|
||||
),
|
||||
prefixIcon: const Icon(Icons.alternate_email),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
MyInput(
|
||||
controller: passwordCtrl,
|
||||
onChanged: (value) => setState(() {}),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
obscureText: obscureText,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.lang.password,
|
||||
hintStyle: TextStyle(
|
||||
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
||||
),
|
||||
filled: true,
|
||||
fillColor: inputColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.lock_outline_rounded,
|
||||
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
||||
),
|
||||
prefixIcon: const Icon(Icons.lock_outline_rounded),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
|
|
@ -198,46 +219,36 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
? FontAwesomeIcons.eye
|
||||
: FontAwesomeIcons.eyeSlash,
|
||||
size: 16,
|
||||
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FilledButton(
|
||||
MyButton(
|
||||
onPressed: (!isLoading) ? _recoverTwonlySafe : null,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size.fromHeight(60),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
Colors.white,
|
||||
),
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
context.lang.twonlySafeRecoverBtn,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
: Text(context.lang.twonlySafeRecoverBtn),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/utils/pow.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/components/link_logo_animation.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/components/onboarding_wrapper.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||
|
||||
class RegisterView extends StatefulWidget {
|
||||
|
|
@ -43,8 +43,9 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
bool _registrationDisabled = false;
|
||||
bool _isTryingToRegister = false;
|
||||
bool _isValidUserName = false;
|
||||
bool _showUserNameError = false;
|
||||
|
||||
bool _showProofOfWorkError = false;
|
||||
String? _usernameErrorText;
|
||||
|
||||
late Future<int>? proofOfWork;
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
Future<void> createNewUser() async {
|
||||
if (!_isValidUserName) {
|
||||
setState(() {
|
||||
_showUserNameError = true;
|
||||
_usernameErrorText = context.lang.registerUsernameLimits;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -67,10 +68,11 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
|
||||
setState(() {
|
||||
_isTryingToRegister = true;
|
||||
_showUserNameError = false;
|
||||
_usernameErrorText = null;
|
||||
_showProofOfWorkError = false;
|
||||
});
|
||||
|
||||
try {
|
||||
late int proof;
|
||||
|
||||
if (proofOfWork != null) {
|
||||
|
|
@ -78,12 +80,14 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
} else {
|
||||
final (pow, registrationDisabled) = await apiService.getProofOfWork();
|
||||
if (pow == null) {
|
||||
setState(() {
|
||||
_registrationDisabled = registrationDisabled;
|
||||
_isTryingToRegister = false;
|
||||
});
|
||||
if (mounted) {
|
||||
showNetworkIssue(context);
|
||||
}
|
||||
return;
|
||||
// Starting with the proof of work.
|
||||
}
|
||||
proof = await calculatePoW(pow.prefix, pow.difficulty.toInt());
|
||||
}
|
||||
|
|
@ -101,7 +105,10 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
} else {
|
||||
proofOfWork = null;
|
||||
if (res.error == ErrorCode.RegistrationDisabled) {
|
||||
setState(() {
|
||||
_registrationDisabled = true;
|
||||
_isTryingToRegister = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (res.error == ErrorCode.UserIdAlreadyTaken) {
|
||||
|
|
@ -109,6 +116,17 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
await deleteLocalUserData();
|
||||
return createNewUser();
|
||||
}
|
||||
if (res.error == ErrorCode.UsernameAlreadyTaken ||
|
||||
res.error == ErrorCode.UsernameNotValid) {
|
||||
setState(() {
|
||||
_usernameErrorText = errorCodeToText(
|
||||
context,
|
||||
res.error as ErrorCode,
|
||||
);
|
||||
_isTryingToRegister = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (res.error == ErrorCode.InvalidProofOfWork) {
|
||||
await deleteLocalUserData();
|
||||
setState(() {
|
||||
|
|
@ -147,23 +165,50 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
|
||||
await apiService.authenticate();
|
||||
widget.callbackOnSuccess();
|
||||
} catch (e, stack) {
|
||||
Log.error('Error creating new user', e, stack);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isTryingToRegister = false;
|
||||
});
|
||||
await showAlertDialog(
|
||||
context,
|
||||
'Error',
|
||||
e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = isDarkMode(context);
|
||||
final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white;
|
||||
final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100];
|
||||
final sloganColor = isDark ? Colors.white.withValues(alpha: 0.9) : Colors.grey[800];
|
||||
final secondaryButtonColor = isDark ? Colors.grey[400] : Colors.grey[600];
|
||||
|
||||
return OnboardingWrapper(
|
||||
return GestureDetector(
|
||||
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: const LinkLogoAnimation(),
|
||||
child: LinkLogoAnimation(
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -174,61 +219,55 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
color:
|
||||
Theme.of(context).textTheme.bodyMedium?.color
|
||||
?.withValues(alpha: 0.7) ??
|
||||
(isDark
|
||||
? Colors.white.withValues(alpha: 0.7)
|
||||
: Colors.black.withValues(alpha: 0.7)),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
if (_registrationDisabled) ...[
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
context.lang.registrationClosed,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.red,
|
||||
color: Colors.redAccent,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
const SizedBox(height: 40),
|
||||
] else ...[
|
||||
Text(
|
||||
context.lang.registerUsernameSlogan,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: sloganColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 22,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextField(
|
||||
const SizedBox(height: 24),
|
||||
MyInput(
|
||||
controller: usernameController,
|
||||
errorText: _usernameErrorText,
|
||||
onChanged: (value) {
|
||||
usernameController.text = value.toLowerCase();
|
||||
usernameController.selection = TextSelection.fromPosition(
|
||||
usernameController.selection =
|
||||
TextSelection.fromPosition(
|
||||
TextPosition(
|
||||
offset: usernameController.text.length,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_isValidUserName = usernameController.text.length >= 3;
|
||||
_isValidUserName =
|
||||
usernameController.text.length >= 3;
|
||||
_usernameErrorText = null;
|
||||
});
|
||||
},
|
||||
inputFormatters: [
|
||||
|
|
@ -237,108 +276,63 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
RegExp('[a-z0-9A-Z._]'),
|
||||
),
|
||||
],
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: context.lang.registerUsernameDecoration,
|
||||
hintStyle: TextStyle(
|
||||
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
||||
prefixIcon: const Icon(Icons.alternate_email),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: inputColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.alternate_email,
|
||||
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_showUserNameError && usernameController.text.length < 3) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.lang.registerUsernameLimits,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (_showProofOfWorkError) ...[
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
context.lang.registerProofOfWorkFailed,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 13,
|
||||
color: Colors.redAccent,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: _isTryingToRegister ? null : createNewUser,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size.fromHeight(60),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
MyButton(
|
||||
onPressed: _isTryingToRegister
|
||||
? null
|
||||
: createNewUser,
|
||||
child: _isTryingToRegister
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
valueColor: AlwaysStoppedAnimation(Colors.white),
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
Colors.white,
|
||||
),
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
context.lang.registerSubmitButton,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
TextButton(
|
||||
MyButton(
|
||||
onPressed: () => context.push(
|
||||
Routes.settingsBackupRecovery,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
foregroundColor: secondaryButtonColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
),
|
||||
variant: MyButtonVariant.secondary,
|
||||
child: Text(
|
||||
context.lang.twonlySafeRecoverBtn,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:twonly/locator.dart';
|
|||
import 'package:twonly/src/services/profile.service.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup/backup.setup.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup/profile.setup.dart';
|
||||
|
|
@ -152,7 +153,9 @@ class _SetupViewState extends State<SetupView> {
|
|||
right: index == currentPage.totalPages - 1 ? 0 : 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isFinished ? context.color.primary : context.color.surfaceContainer,
|
||||
color: isFinished
|
||||
? context.color.primary
|
||||
: context.color.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
|
|
@ -175,35 +178,30 @@ class _SetupViewState extends State<SetupView> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (currentPage.index > 0)
|
||||
TextButton(
|
||||
MyButton(
|
||||
onPressed: () async {
|
||||
await UserService.update((u) {
|
||||
u.currentSetupPage = currentPage.previous()?.name;
|
||||
});
|
||||
},
|
||||
variant: MyButtonVariant.text,
|
||||
child: Text(
|
||||
context.lang.back,
|
||||
style: TextStyle(
|
||||
color: context.color.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (currentPage.index > 0 && !currentPage.isLast) const SizedBox(width: 24),
|
||||
if (currentPage.index > 0 && !currentPage.isLast)
|
||||
const SizedBox(width: 24),
|
||||
if (!currentPage.isLast)
|
||||
TextButton(
|
||||
MyButton(
|
||||
onPressed: () async {
|
||||
await UserService.update(
|
||||
(u) => u.skipSetupPages = true,
|
||||
);
|
||||
widget.onUpdate?.call();
|
||||
},
|
||||
variant: MyButtonVariant.text,
|
||||
child: Text(
|
||||
context.lang.onboardingFinishLater,
|
||||
style: TextStyle(
|
||||
color: context.color.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||
|
||||
class FinishSetupComp extends StatefulWidget {
|
||||
|
|
@ -123,29 +124,19 @@ class _FinishSetupCompState extends State<FinishSetupComp> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
FilledButton.icon(
|
||||
MyButton(
|
||||
onPressed: onTap,
|
||||
icon: const Icon(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.arrow_forward_rounded,
|
||||
size: 18,
|
||||
),
|
||||
label: Text(
|
||||
context.lang.finishSetupCardAction,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: context.color.primary,
|
||||
foregroundColor: context.color.onPrimary,
|
||||
minimumSize: const Size(0, 40),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
const SizedBox(width: 8),
|
||||
Text(context.lang.finishSetupCardAction),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||
|
||||
class NextButtonComp extends StatelessWidget {
|
||||
|
|
@ -24,7 +25,7 @@ class NextButtonComp extends StatelessWidget {
|
|||
final currentPage = SetupPagesExtension.fromStr(
|
||||
userService.currentUser.currentSetupPage,
|
||||
);
|
||||
return ElevatedButton(
|
||||
return MyButton(
|
||||
onPressed: (canSubmit && !isLoading)
|
||||
? () async {
|
||||
if (onPressed != null) {
|
||||
|
|
@ -36,15 +37,6 @@ class NextButtonComp extends StatelessWidget {
|
|||
});
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 56),
|
||||
backgroundColor: context.color.primary,
|
||||
foregroundColor: context.color.onPrimary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
|
|
@ -56,7 +48,6 @@ class NextButtonComp extends StatelessWidget {
|
|||
)
|
||||
: Text(
|
||||
currentPage.isLast ? context.lang.finishSetup : context.lang.next,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import 'package:avatar_maker/avatar_maker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'
|
||||
show AvatarIcon;
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';
|
||||
import 'package:vector_graphics/vector_graphics.dart';
|
||||
|
||||
class ProfileSetupPage extends StatefulWidget {
|
||||
const ProfileSetupPage({super.key});
|
||||
|
|
@ -17,10 +18,7 @@ class ProfileSetupPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||
final AvatarMakerController _avatarMakerController =
|
||||
PersistentAvatarMakerController(customizedPropertyCategories: []);
|
||||
late final TextEditingController _displayNameController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -35,6 +33,9 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: userService.onUserUpdated,
|
||||
builder: (context, asyncSnapshot) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
@ -65,52 +66,34 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
|||
width: 4,
|
||||
),
|
||||
),
|
||||
child: userService.currentUser.avatarSvg == null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
child: Container(
|
||||
width: 160,
|
||||
height: 160,
|
||||
color: context.color.surfaceContainer,
|
||||
child: const SvgPicture(
|
||||
AssetBytesLoader(
|
||||
'assets/images/default_avatar.svg.vec',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: AvatarMakerAvatar(
|
||||
backgroundColor: context.color.surfaceContainer,
|
||||
radius: 80,
|
||||
controller: _avatarMakerController,
|
||||
child: const AvatarIcon(
|
||||
fontSize: 70,
|
||||
myAvatar: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextButton.icon(
|
||||
MyButton(
|
||||
onPressed: () async {
|
||||
await context.push(Routes.settingsProfileModifyAvatar);
|
||||
await _avatarMakerController.performRestore();
|
||||
},
|
||||
icon: const Icon(Icons.palette_outlined),
|
||||
label: Text(context.lang.settingsProfileCustomizeAvatar),
|
||||
variant: MyButtonVariant.text,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.palette_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.lang.settingsProfileCustomizeAvatar),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
TextField(
|
||||
MyInput(
|
||||
controller: _displayNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.lang.settingsProfileEditDisplayName,
|
||||
hintText: context.lang.settingsProfileEditDisplayNameNew,
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
filled: true,
|
||||
fillColor: context.color.surfaceContainerLow,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
NextButtonComp(
|
||||
|
|
@ -125,5 +108,7 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
|||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||
|
||||
Future<bool> isSecurePassword(String password) async {
|
||||
final badPasswordsStr = await rootBundle.loadString(
|
||||
|
|
@ -46,19 +46,11 @@ class _BackupPasswordTextFieldState extends State<BackupPasswordTextField> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
return MyInput(
|
||||
controller: widget.controller,
|
||||
onChanged: widget.onChanged,
|
||||
obscureText: _obscureText,
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.labelText,
|
||||
filled: true,
|
||||
fillColor: context.color.surfaceContainerLow,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
hintText: widget.labelText,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
|
|
@ -70,7 +62,6 @@ class _BackupPasswordTextFieldState extends State<BackupPasswordTextField> {
|
|||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class ModifyAvatarView extends StatefulWidget {
|
||||
const ModifyAvatarView({super.key});
|
||||
|
|
@ -15,12 +16,19 @@ class ModifyAvatarView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||
final AvatarMakerController _avatarMakerController =
|
||||
PersistentAvatarMakerController(customizedPropertyCategories: []);
|
||||
late final _CustomAvatarMakerController _avatarMakerController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final svg = userService.currentUser.avatarSvg;
|
||||
if (svg != null && svg.isNotEmpty) {
|
||||
_avatarMakerController = _CustomAvatarMakerController(
|
||||
svg: svg,
|
||||
);
|
||||
} else {
|
||||
_avatarMakerController = _CustomAvatarMakerController.defaultAvatar();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateUserAvatar(String json, String svg) async {
|
||||
|
|
@ -33,49 +41,38 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
}
|
||||
|
||||
AvatarMakerThemeData getAvatarMakerTheme(BuildContext context) {
|
||||
if (isDarkMode(context)) {
|
||||
final colors = context.color;
|
||||
final isDark = isDarkMode(context);
|
||||
return AvatarMakerThemeData(
|
||||
boxDecoration: const BoxDecoration(
|
||||
boxShadow: [BoxShadow()],
|
||||
boxDecoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
unselectedTileDecoration: BoxDecoration(
|
||||
color: const Color.fromARGB(255, 50, 50, 50), // Dark mode color
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
selectedTileDecoration: BoxDecoration(
|
||||
color: const Color.fromARGB(255, 100, 100, 100), // Dark mode color
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: colors.primary.withValues(alpha: 0.15),
|
||||
border: Border.all(color: colors.primary, width: 2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
selectedIconColor: colors.primary,
|
||||
unselectedIconColor: colors.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
primaryBgColor: colors.surface,
|
||||
secondaryBgColor: colors.surfaceContainerLow,
|
||||
labelTextStyle: TextStyle(
|
||||
color: colors.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
selectedIconColor: Colors.white,
|
||||
unselectedIconColor: Colors.grey,
|
||||
primaryBgColor: Colors.black, // Dark mode background
|
||||
secondaryBgColor: Colors.grey[850], // Dark mode secondary background
|
||||
labelTextStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
), // Light text for dark mode
|
||||
);
|
||||
} else {
|
||||
return AvatarMakerThemeData(
|
||||
boxDecoration: const BoxDecoration(
|
||||
boxShadow: [BoxShadow()],
|
||||
),
|
||||
unselectedTileDecoration: BoxDecoration(
|
||||
color: const Color.fromARGB(255, 240, 240, 240), // Light mode color
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
selectedTileDecoration: BoxDecoration(
|
||||
color: const Color.fromARGB(255, 200, 200, 200), // Light mode color
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
selectedIconColor: Colors.black,
|
||||
unselectedIconColor: Colors.grey,
|
||||
primaryBgColor: Colors.white, // Light mode background
|
||||
secondaryBgColor: Colors.grey[200], // Light mode secondary background
|
||||
labelTextStyle: const TextStyle(
|
||||
color: Colors.black,
|
||||
), // Dark text for light mode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool?> _showBackDialog() {
|
||||
|
|
@ -148,39 +145,64 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
controller: _avatarMakerController,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 10,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.floppyDisk),
|
||||
MyButton(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
onPressed: storeAvatarAndExit,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.floppyDisk, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Text(context.lang.avatarSaveChangesStore),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.shuffle),
|
||||
),
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed:
|
||||
_avatarMakerController.randomizedSelectedOptions,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.shuffle, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Text(context.lang.avatarCustomizeRandomize),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.rotateLeft),
|
||||
onLongPress: () async {
|
||||
await PersistentAvatarMakerController.clearAvatarMaker();
|
||||
await _avatarMakerController.restoreState();
|
||||
},
|
||||
),
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: _avatarMakerController.restoreState,
|
||||
onLongPress: () {
|
||||
_avatarMakerController.clearCustomizations();
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.rotateLeft, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Text(context.lang.avatarCustomizeReset),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 30,
|
||||
),
|
||||
child: AvatarMakerCustomizer(
|
||||
scaffoldWidth: min(
|
||||
600,
|
||||
MediaQuery.of(context).size.width * 0.85,
|
||||
MediaQuery.of(context).size.width * 0.95,
|
||||
),
|
||||
theme: getAvatarMakerTheme(context),
|
||||
controller: _avatarMakerController,
|
||||
|
|
@ -194,3 +216,72 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomAvatarMakerController extends NonPersistentAvatarMakerController {
|
||||
_CustomAvatarMakerController({
|
||||
required super.svg,
|
||||
}) : _initialSvg = svg,
|
||||
super.fromSvg() {
|
||||
_initialOptions = Map.from(selectedOptions);
|
||||
}
|
||||
|
||||
_CustomAvatarMakerController.defaultAvatar() : _initialSvg = '', super() {
|
||||
_initialOptions = Map.from(defaultSelectedOptions);
|
||||
}
|
||||
|
||||
final String _initialSvg;
|
||||
late final Map<PropertyCategoryIds, PropertyItem> _initialOptions;
|
||||
List<CustomizedPropertyCategory>? _customPropertyCategories;
|
||||
|
||||
void clearCustomizations() {
|
||||
selectedOptions = Map.from(defaultSelectedOptions);
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
@override
|
||||
List<CustomizedPropertyCategory> get propertyCategories {
|
||||
var list = _customPropertyCategories;
|
||||
if (list == null) {
|
||||
list = super.propertyCategories.map((category) {
|
||||
return CustomizedPropertyCategory(
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
iconFile: category.iconFile,
|
||||
properties: category.properties,
|
||||
defaultValue: category.defaultValue,
|
||||
);
|
||||
}).toList();
|
||||
_customPropertyCategories = list;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@override
|
||||
List<CustomizedPropertyCategory> get displayedPropertyCategories {
|
||||
final order = [
|
||||
PropertyCategoryIds.SkinColor,
|
||||
PropertyCategoryIds.EyeType,
|
||||
PropertyCategoryIds.EyebrowType,
|
||||
PropertyCategoryIds.Nose,
|
||||
PropertyCategoryIds.MouthType,
|
||||
PropertyCategoryIds.HairStyle,
|
||||
PropertyCategoryIds.HairColor,
|
||||
PropertyCategoryIds.FacialHairType,
|
||||
PropertyCategoryIds.FacialHairColor,
|
||||
PropertyCategoryIds.OutfitType,
|
||||
PropertyCategoryIds.OutfitColor,
|
||||
PropertyCategoryIds.Accessory,
|
||||
];
|
||||
return (propertyCategories.where((c) => order.contains(c.id)).toList()
|
||||
..sort((a, b) => order.indexOf(a.id).compareTo(order.indexOf(b.id))));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RestoredData> performRestore() async {
|
||||
final restoredSvg = _initialSvg.isNotEmpty ? _initialSvg : drawAvatarSVG();
|
||||
return RestoredData(
|
||||
svg: restoredSvg,
|
||||
options: Map.from(_initialOptions),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:avatar_maker/avatar_maker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
|
@ -10,8 +9,10 @@ import 'package:twonly/src/constants/routes.keys.dart';
|
|||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||
|
||||
class ProfileView extends StatefulWidget {
|
||||
|
|
@ -22,9 +23,6 @@ class ProfileView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ProfileViewState extends State<ProfileView> {
|
||||
final AvatarMakerController _avatarMakerController =
|
||||
PersistentAvatarMakerController(customizedPropertyCategories: []);
|
||||
|
||||
int twonlyScore = 0;
|
||||
late StreamSubscription<int> twonlyScoreSub;
|
||||
|
||||
|
|
@ -104,22 +102,24 @@ class _ProfileViewState extends State<ProfileView> {
|
|||
physics: const BouncingScrollPhysics(),
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 25),
|
||||
AvatarMakerAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
radius: 80,
|
||||
controller: _avatarMakerController,
|
||||
const AvatarIcon(
|
||||
fontSize: 70,
|
||||
myAvatar: true,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.edit),
|
||||
label: Text(context.lang.settingsProfileCustomizeAvatar),
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: () async {
|
||||
await context.push(Routes.settingsProfileModifyAvatar);
|
||||
await _avatarMakerController.performRestore();
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.edit, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.lang.settingsProfileCustomizeAvatar),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue