💄 More transparency
This commit is contained in:
parent
2e9c4d166e
commit
d7e6fe2d8f
@ -4,7 +4,6 @@ import 'package:get/get.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:solian/widgets/root_container.dart';
|
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
@ -16,132 +15,130 @@ class AboutScreen extends StatelessWidget {
|
|||||||
const denseButtonStyle =
|
const denseButtonStyle =
|
||||||
ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
||||||
|
|
||||||
return RootContainer(
|
return SizedBox(
|
||||||
child: SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
child: Column(
|
||||||
child: Column(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
children: [
|
ClipRRect(
|
||||||
ClipRRect(
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
child: Image.asset('assets/logo.png', width: 120, height: 120),
|
||||||
child: Image.asset('assets/logo.png', width: 120, height: 120),
|
),
|
||||||
),
|
const Gap(8),
|
||||||
const Gap(8),
|
Text(
|
||||||
Text(
|
'Solian',
|
||||||
'Solian',
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
),
|
||||||
),
|
const Text(
|
||||||
const Text(
|
'The Solar Network',
|
||||||
'The Solar Network',
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
),
|
||||||
),
|
const Gap(8),
|
||||||
const Gap(8),
|
FutureBuilder(
|
||||||
FutureBuilder(
|
future: PackageInfo.fromPlatform(),
|
||||||
future: PackageInfo.fromPlatform(),
|
builder: (context, snapshot) {
|
||||||
builder: (context, snapshot) {
|
if (!snapshot.hasData) {
|
||||||
if (!snapshot.hasData) {
|
return const SizedBox.shrink();
|
||||||
return const SizedBox.shrink();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
CenteredContainer(
|
CenteredContainer(
|
||||||
maxWidth: 280,
|
maxWidth: 280,
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: denseButtonStyle,
|
style: denseButtonStyle,
|
||||||
child: Text('appDetails'.tr),
|
child: Text('appDetails'.tr),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final info = await PackageInfo.fromPlatform();
|
final info = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationVersion:
|
applicationVersion:
|
||||||
'${info.version} (${info.buildNumber})',
|
'${info.version} (${info.buildNumber})',
|
||||||
applicationLegalese:
|
applicationLegalese:
|
||||||
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
||||||
applicationIcon: ClipRRect(
|
applicationIcon: ClipRRect(
|
||||||
borderRadius:
|
borderRadius:
|
||||||
const BorderRadius.all(Radius.circular(16)),
|
const BorderRadius.all(Radius.circular(16)),
|
||||||
child: Image.asset('assets/logo.png',
|
child: Image.asset('assets/logo.png',
|
||||||
width: 60, height: 60),
|
width: 60, height: 60),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: denseButtonStyle,
|
style: denseButtonStyle,
|
||||||
child: Text('projectWebsite'.tr),
|
child: Text('projectWebsite'.tr),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
launchUrlString(
|
launchUrlString(
|
||||||
'https://solsynth.dev/products/solar-network');
|
'https://solsynth.dev/products/solar-network');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: denseButtonStyle,
|
style: denseButtonStyle,
|
||||||
child: Text('termRelated'.tr),
|
child: Text('termRelated'.tr),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
launchUrlString('https://solsynth.dev/terms');
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: denseButtonStyle,
|
style: denseButtonStyle,
|
||||||
child: Text('serviceStatus'.tr),
|
child: Text('serviceStatus'.tr),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
launchUrlString('https://status.solsynth.dev');
|
launchUrlString('https://status.solsynth.dev');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Gap(16),
|
),
|
||||||
const Text(
|
const Gap(16),
|
||||||
'Open-sourced under AGPLv3',
|
const Text(
|
||||||
style: TextStyle(
|
'Open-sourced under AGPLv3',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: SharedPreferences.getInstance(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
const textStyle = TextStyle(
|
||||||
fontWeight: FontWeight.w300,
|
fontWeight: FontWeight.w300,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
);
|
||||||
),
|
if (!snapshot.hasData ||
|
||||||
FutureBuilder(
|
!snapshot.data!.containsKey('first_boot_time')) {
|
||||||
future: SharedPreferences.getInstance(),
|
return Text(
|
||||||
builder: (context, snapshot) {
|
'firstBootTime'.trParams({'time': 'unknown'.tr}),
|
||||||
const textStyle = TextStyle(
|
style: textStyle,
|
||||||
fontWeight: FontWeight.w300,
|
|
||||||
fontSize: 12,
|
|
||||||
);
|
);
|
||||||
if (!snapshot.hasData ||
|
} else {
|
||||||
!snapshot.data!.containsKey('first_boot_time')) {
|
return Text(
|
||||||
return Text(
|
'firstBootTime'.trParams({
|
||||||
'firstBootTime'.trParams({'time': 'unknown'.tr}),
|
'time': DateFormat('yyyy-MM-dd').format(
|
||||||
style: textStyle,
|
DateTime.tryParse(
|
||||||
);
|
snapshot.data!.getString('first_boot_time')!,
|
||||||
} else {
|
)?.toLocal() ??
|
||||||
return Text(
|
DateTime.now(),
|
||||||
'firstBootTime'.trParams({
|
),
|
||||||
'time': DateFormat('yyyy-MM-dd').format(
|
}),
|
||||||
DateTime.tryParse(
|
style: textStyle,
|
||||||
snapshot.data!.getString('first_boot_time')!,
|
);
|
||||||
)?.toLocal() ??
|
}
|
||||||
DateTime.now(),
|
},
|
||||||
),
|
),
|
||||||
}),
|
],
|
||||||
style: textStyle,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import 'package:solian/providers/account_status.dart';
|
|||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/widgets/account/account_heading.dart';
|
import 'package:solian/widgets/account/account_heading.dart';
|
||||||
import 'package:solian/widgets/root_container.dart';
|
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:badges/badges.dart' as badges;
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
|
||||||
@ -50,112 +49,110 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
|
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
return RootContainer(
|
return SafeArea(
|
||||||
child: SafeArea(
|
child: Obx(() {
|
||||||
child: Obx(() {
|
if (auth.isAuthorized.isFalse) {
|
||||||
if (auth.isAuthorized.isFalse) {
|
return Center(
|
||||||
return Center(
|
child: Column(
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
_ActionCard(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.login,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
title: 'signin'.tr,
|
|
||||||
caption: 'signinCaption'.tr,
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.pushNamed('signin').then((val) async {
|
|
||||||
if (val == true) {
|
|
||||||
await auth.refreshUserProfile();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_ActionCard(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.add,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
title: 'signup'.tr,
|
|
||||||
caption: 'signupCaption'.tr,
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.pushNamed('signup').then((_) {
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
TextButton(
|
|
||||||
style: const ButtonStyle(
|
|
||||||
visualDensity: VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
AppRouter.instance.pushNamed('settings');
|
|
||||||
},
|
|
||||||
child: Text('settings'.tr),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CenteredContainer(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
children: [
|
||||||
if (auth.userProfile.value != null)
|
_ActionCard(
|
||||||
const AccountHeading().paddingOnly(bottom: 8, top: 16),
|
icon: Icon(
|
||||||
...(actionItems.map(
|
Icons.login,
|
||||||
(x) => ListTile(
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
|
||||||
leading: x.$1,
|
|
||||||
title: Text(x.$2),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance
|
|
||||||
.pushNamed(x.$3)
|
|
||||||
.then((_) => setState(() {}));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)),
|
title: 'signin'.tr,
|
||||||
const Divider(thickness: 0.3, height: 1)
|
caption: 'signinCaption'.tr,
|
||||||
.paddingSymmetric(vertical: 4),
|
|
||||||
ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
|
||||||
leading: const Icon(Icons.settings),
|
|
||||||
title: Text('settings'.tr),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AppRouter.instance.pushNamed('settings');
|
AppRouter.instance.pushNamed('signin').then((val) async {
|
||||||
|
if (val == true) {
|
||||||
|
await auth.refreshUserProfile();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (auth.isAuthorized.value)
|
_ActionCard(
|
||||||
ListTile(
|
icon: Icon(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
Icons.add,
|
||||||
leading: const Icon(Icons.edit_notifications),
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
title: Text('notificationPreferences'.tr),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.pushNamed('notificationPreferences');
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const Divider(thickness: 0.3, height: 1)
|
title: 'signup'.tr,
|
||||||
.paddingSymmetric(vertical: 4),
|
caption: 'signupCaption'.tr,
|
||||||
ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
|
||||||
leading: const Icon(Icons.logout),
|
|
||||||
title: Text('signout'.tr),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
auth.signout();
|
AppRouter.instance.pushNamed('signup').then((_) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextButton(
|
||||||
|
style: const ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
AppRouter.instance.pushNamed('settings');
|
||||||
|
},
|
||||||
|
child: Text('settings'.tr),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}
|
||||||
),
|
|
||||||
|
return CenteredContainer(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
if (auth.userProfile.value != null)
|
||||||
|
const AccountHeading().paddingOnly(bottom: 8, top: 16),
|
||||||
|
...(actionItems.map(
|
||||||
|
(x) => ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
|
leading: x.$1,
|
||||||
|
title: Text(x.$2),
|
||||||
|
onTap: () {
|
||||||
|
AppRouter.instance
|
||||||
|
.pushNamed(x.$3)
|
||||||
|
.then((_) => setState(() {}));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
const Divider(thickness: 0.3, height: 1)
|
||||||
|
.paddingSymmetric(vertical: 4),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
|
leading: const Icon(Icons.settings),
|
||||||
|
title: Text('settings'.tr),
|
||||||
|
onTap: () {
|
||||||
|
AppRouter.instance.pushNamed('settings');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (auth.isAuthorized.value)
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
|
leading: const Icon(Icons.edit_notifications),
|
||||||
|
title: Text('notificationPreferences'.tr),
|
||||||
|
onTap: () {
|
||||||
|
AppRouter.instance.pushNamed('notificationPreferences');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(thickness: 0.3, height: 1)
|
||||||
|
.paddingSymmetric(vertical: 4),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
|
leading: const Icon(Icons.logout),
|
||||||
|
title: Text('signout'.tr),
|
||||||
|
onTap: () {
|
||||||
|
auth.signout();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'package:solian/exceptions/request.dart';
|
import 'package:solian/exceptions/request.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/widgets/root_container.dart';
|
|
||||||
|
|
||||||
class NotificationPreferencesScreen extends StatefulWidget {
|
class NotificationPreferencesScreen extends StatefulWidget {
|
||||||
const NotificationPreferencesScreen({super.key});
|
const NotificationPreferencesScreen({super.key});
|
||||||
@ -75,44 +74,42 @@ class _NotificationPreferencesScreenState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RootContainer(
|
return Column(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
ListTile(
|
||||||
ListTile(
|
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
leading: const Icon(Icons.save),
|
||||||
leading: const Icon(Icons.save),
|
title: Text('save'.tr),
|
||||||
title: Text('save'.tr),
|
enabled: !_isBusy,
|
||||||
enabled: !_isBusy,
|
onTap: () {
|
||||||
onTap: () {
|
_savePreferences();
|
||||||
_savePreferences();
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _topicMap.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final element = _topicMap.entries.elementAt(index);
|
||||||
|
return CheckboxListTile(
|
||||||
|
title: Text(element.value),
|
||||||
|
subtitle: Text(
|
||||||
|
element.key,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||||
|
),
|
||||||
|
value: _config[element.key] ?? true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_config[element.key] = value ?? false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: ListView.builder(
|
],
|
||||||
itemCount: _topicMap.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = _topicMap.entries.elementAt(index);
|
|
||||||
return CheckboxListTile(
|
|
||||||
title: Text(element.value),
|
|
||||||
subtitle: Text(
|
|
||||||
element.key,
|
|
||||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
|
||||||
),
|
|
||||||
value: _config[element.key] ?? true,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_config[element.key] = value ?? false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,163 +187,161 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const double padding = 32;
|
const double padding = 32;
|
||||||
|
|
||||||
return RootContainer(
|
return ListView(
|
||||||
child: ListView(
|
children: [
|
||||||
children: [
|
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
const Gap(24),
|
||||||
const Gap(24),
|
Stack(
|
||||||
Stack(
|
children: [
|
||||||
children: [
|
AccountAvatar(content: _avatar, radius: 40),
|
||||||
AccountAvatar(content: _avatar, radius: 40),
|
Positioned(
|
||||||
Positioned(
|
bottom: 0,
|
||||||
bottom: 0,
|
left: 40,
|
||||||
left: 40,
|
child: FloatingActionButton.small(
|
||||||
child: FloatingActionButton.small(
|
heroTag: const Key('avatar-editor'),
|
||||||
heroTag: const Key('avatar-editor'),
|
onPressed: () => _editImage('avatar'),
|
||||||
onPressed: () => _editImage('avatar'),
|
child: const Icon(
|
||||||
child: const Icon(
|
Icons.camera,
|
||||||
Icons.camera,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
).paddingSymmetric(horizontal: padding),
|
|
||||||
const Gap(16),
|
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
child: Container(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: _banner != null
|
|
||||||
? Image.network(
|
|
||||||
ServiceFinder.buildUrl(
|
|
||||||
'files', '/attachments/$_banner'),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
loadingBuilder: (BuildContext context, Widget child,
|
|
||||||
ImageChunkEvent? loadingProgress) {
|
|
||||||
if (loadingProgress == null) return child;
|
|
||||||
return Center(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
value: loadingProgress.expectedTotalBytes !=
|
|
||||||
null
|
|
||||||
? loadingProgress.cumulativeBytesLoaded /
|
|
||||||
loadingProgress.expectedTotalBytes!
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 16,
|
|
||||||
right: 16,
|
|
||||||
child: FloatingActionButton(
|
|
||||||
heroTag: const Key('banner-editor'),
|
|
||||||
onPressed: () => _editImage('banner'),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.camera_alt,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingSymmetric(horizontal: padding),
|
|
||||||
const Gap(24),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TextField(
|
|
||||||
readOnly: true,
|
|
||||||
controller: _usernameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
labelText: 'username'.tr,
|
|
||||||
prefixText: '@',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TextField(
|
|
||||||
controller: _nicknameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
labelText: 'nickname'.tr,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingSymmetric(horizontal: padding),
|
|
||||||
const Gap(16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TextField(
|
|
||||||
controller: _firstNameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
labelText: 'firstName'.tr,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TextField(
|
|
||||||
controller: _lastNameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
labelText: 'lastName'.tr,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingSymmetric(horizontal: padding),
|
|
||||||
const Gap(16),
|
|
||||||
TextField(
|
|
||||||
controller: _descriptionController,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
maxLines: null,
|
|
||||||
minLines: 3,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
labelText: 'description'.tr,
|
|
||||||
),
|
),
|
||||||
).paddingSymmetric(horizontal: padding),
|
],
|
||||||
const Gap(16),
|
).paddingSymmetric(horizontal: padding),
|
||||||
TextField(
|
const Gap(16),
|
||||||
controller: _birthdayController,
|
Stack(
|
||||||
readOnly: true,
|
children: [
|
||||||
decoration: InputDecoration(
|
ClipRRect(
|
||||||
border: const OutlineInputBorder(),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
labelText: 'birthday'.tr,
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
child: _banner != null
|
||||||
|
? Image.network(
|
||||||
|
ServiceFinder.buildUrl(
|
||||||
|
'files', '/attachments/$_banner'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
loadingBuilder: (BuildContext context, Widget child,
|
||||||
|
ImageChunkEvent? loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: loadingProgress.expectedTotalBytes !=
|
||||||
|
null
|
||||||
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
|
loadingProgress.expectedTotalBytes!
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () => _selectBirthday(),
|
Positioned(
|
||||||
).paddingSymmetric(horizontal: padding),
|
bottom: 16,
|
||||||
const Gap(16),
|
right: 16,
|
||||||
Row(
|
child: FloatingActionButton(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
heroTag: const Key('banner-editor'),
|
||||||
children: [
|
onPressed: () => _editImage('banner'),
|
||||||
TextButton(
|
child: const Icon(
|
||||||
onPressed: _isBusy ? null : () => _syncWidget(),
|
Icons.camera_alt,
|
||||||
child: Text('reset'.tr),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
),
|
||||||
onPressed: _isBusy ? null : () => _editUserInfo(),
|
],
|
||||||
child: Text('apply'.tr),
|
).paddingSymmetric(horizontal: padding),
|
||||||
|
const Gap(24),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TextField(
|
||||||
|
readOnly: true,
|
||||||
|
controller: _usernameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'username'.tr,
|
||||||
|
prefixText: '@',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
).paddingSymmetric(horizontal: padding),
|
const Gap(16),
|
||||||
],
|
Flexible(
|
||||||
),
|
flex: 1,
|
||||||
|
child: TextField(
|
||||||
|
controller: _nicknameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'nickname'.tr,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: padding),
|
||||||
|
const Gap(16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TextField(
|
||||||
|
controller: _firstNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'firstName'.tr,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TextField(
|
||||||
|
controller: _lastNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'lastName'.tr,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: padding),
|
||||||
|
const Gap(16),
|
||||||
|
TextField(
|
||||||
|
controller: _descriptionController,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
maxLines: null,
|
||||||
|
minLines: 3,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'description'.tr,
|
||||||
|
),
|
||||||
|
).paddingSymmetric(horizontal: padding),
|
||||||
|
const Gap(16),
|
||||||
|
TextField(
|
||||||
|
controller: _birthdayController,
|
||||||
|
readOnly: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'birthday'.tr,
|
||||||
|
),
|
||||||
|
onTap: () => _selectBirthday(),
|
||||||
|
).paddingSymmetric(horizontal: padding),
|
||||||
|
const Gap(16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _syncWidget(),
|
||||||
|
child: Text('reset'.tr),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isBusy ? null : () => _editUserInfo(),
|
||||||
|
child: Text('apply'.tr),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: padding),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,298 +217,288 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RootContainer(
|
return CenteredContainer(
|
||||||
child: CenteredContainer(
|
maxWidth: 360,
|
||||||
maxWidth: 360,
|
child: Theme(
|
||||||
child: Theme(
|
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
child: PageTransitionSwitcher(
|
||||||
child: PageTransitionSwitcher(
|
transitionBuilder: (
|
||||||
transitionBuilder: (
|
Widget child,
|
||||||
Widget child,
|
Animation<double> primaryAnimation,
|
||||||
Animation<double> primaryAnimation,
|
Animation<double> secondaryAnimation,
|
||||||
Animation<double> secondaryAnimation,
|
) {
|
||||||
) {
|
return SharedAxisTransition(
|
||||||
return SharedAxisTransition(
|
animation: primaryAnimation,
|
||||||
animation: primaryAnimation,
|
secondaryAnimation: secondaryAnimation,
|
||||||
secondaryAnimation: secondaryAnimation,
|
transitionType: SharedAxisTransitionType.horizontal,
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
child: child,
|
||||||
child: child,
|
);
|
||||||
);
|
},
|
||||||
},
|
child: switch (_period % 3) {
|
||||||
child: switch (_period % 3) {
|
1 => ListView(
|
||||||
1 => ListView(
|
shrinkWrap: true,
|
||||||
shrinkWrap: true,
|
key: const ValueKey<int>(1),
|
||||||
key: const ValueKey<int>(1),
|
children: [
|
||||||
children: [
|
Align(
|
||||||
Align(
|
alignment: Alignment.centerLeft,
|
||||||
alignment: Alignment.centerLeft,
|
child: ClipRRect(
|
||||||
child: ClipRRect(
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
borderRadius:
|
child:
|
||||||
const BorderRadius.all(Radius.circular(8)),
|
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||||
child: Image.asset('assets/logo.png',
|
).paddingOnly(bottom: 8, left: 4),
|
||||||
width: 64, height: 64),
|
),
|
||||||
).paddingOnly(bottom: 8, left: 4),
|
Text(
|
||||||
|
'signinPickFactor'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
Text(
|
).paddingOnly(left: 4, bottom: 16),
|
||||||
'signinPickFactor'.tr,
|
Card(
|
||||||
style: const TextStyle(
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
fontSize: 28,
|
child: Column(
|
||||||
fontWeight: FontWeight.w900,
|
children: _factors
|
||||||
),
|
?.map(
|
||||||
).paddingOnly(left: 4, bottom: 16),
|
(x) => CheckboxListTile(
|
||||||
Card(
|
shape: const RoundedRectangleBorder(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
borderRadius: BorderRadius.all(
|
||||||
child: Column(
|
Radius.circular(8),
|
||||||
children: _factors
|
|
||||||
?.map(
|
|
||||||
(x) => CheckboxListTile(
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(
|
|
||||||
Radius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
secondary: Icon(
|
|
||||||
_factorLabelMap[x.type]?.$2 ??
|
|
||||||
Icons.question_mark,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
_factorLabelMap[x.type]?.$1 ??
|
|
||||||
'unknown'.tr,
|
|
||||||
),
|
|
||||||
enabled: !_currentTicket!.factorTrail
|
|
||||||
.contains(x.id),
|
|
||||||
value: _factorPicked == x.id,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == true) {
|
|
||||||
setState(() => _factorPicked = x.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
secondary: Icon(
|
||||||
.toList() ??
|
_factorLabelMap[x.type]?.$2 ??
|
||||||
List.empty(),
|
Icons.question_mark,
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'signinMultiFactor'.trParams(
|
|
||||||
{'n': _currentTicket!.stepRemain.toString()},
|
|
||||||
),
|
|
||||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
|
||||||
).paddingOnly(left: 16, right: 16),
|
|
||||||
const Gap(12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: (_isBusy || _period > 1)
|
|
||||||
? null
|
|
||||||
: () => _previousStep(),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: Colors.grey),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.chevron_left),
|
|
||||||
Text('prev'.tr),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed:
|
|
||||||
_isBusy ? null : () => _performGetFactorCode(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('next'.tr),
|
|
||||||
const Icon(Icons.chevron_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
2 => ListView(
|
|
||||||
key: const ValueKey<int>(2),
|
|
||||||
shrinkWrap: true,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: Image.asset('assets/logo.png',
|
|
||||||
width: 64, height: 64),
|
|
||||||
).paddingOnly(bottom: 8, left: 4),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'signinEnterPassword'.tr,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
),
|
|
||||||
).paddingOnly(left: 4, bottom: 16),
|
|
||||||
TextField(
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
controller: _passwordController,
|
|
||||||
obscureText: true,
|
|
||||||
autofillHints: [
|
|
||||||
(_factorLabelMap[_factorPickedType]?.$3 ?? true)
|
|
||||||
? AutofillHints.password
|
|
||||||
: AutofillHints.oneTimeCode
|
|
||||||
],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
labelText:
|
|
||||||
(_factorLabelMap[_factorPickedType]?.$3 ?? true)
|
|
||||||
? 'passwordOneTime'.tr
|
|
||||||
: 'password'.tr,
|
|
||||||
helperText:
|
|
||||||
(_factorLabelMap[_factorPickedType]?.$3 ?? true)
|
|
||||||
? 'passwordOneTimeInputHint'.tr
|
|
||||||
: 'passwordInputHint'.tr,
|
|
||||||
),
|
|
||||||
onTapOutside: (_) =>
|
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
onSubmitted:
|
|
||||||
_isBusy ? null : (_) => _performCheckTicket(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: _isBusy ? null : () => _previousStep(),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: Colors.grey),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.chevron_left),
|
|
||||||
Text('prev'.tr),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed:
|
|
||||||
_isBusy ? null : () => _performCheckTicket(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('next'.tr),
|
|
||||||
const Icon(Icons.chevron_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
_ => ListView(
|
|
||||||
key: const ValueKey<int>(0),
|
|
||||||
shrinkWrap: true,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: Image.asset('assets/logo.png',
|
|
||||||
width: 64, height: 64),
|
|
||||||
).paddingOnly(bottom: 8, left: 4),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'signinGreeting'.tr,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
),
|
|
||||||
).paddingOnly(left: 4, bottom: 16),
|
|
||||||
TextField(
|
|
||||||
autocorrect: false,
|
|
||||||
enableSuggestions: false,
|
|
||||||
controller: _usernameController,
|
|
||||||
autofillHints: const [AutofillHints.username],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
labelText: 'username'.tr,
|
|
||||||
helperText: 'usernameInputHint'.tr,
|
|
||||||
),
|
|
||||||
onTapOutside: (_) =>
|
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
onSubmitted: _isBusy ? null : (_) => _performNewTicket(),
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed:
|
|
||||||
_isBusy ? null : () => _requestResetPassword(),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: Colors.grey),
|
|
||||||
child: Text('forgotPassword'.tr),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: _isBusy ? null : () => _performNewTicket(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('next'.tr),
|
|
||||||
const Icon(Icons.chevron_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 290),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'termAcceptNextWithAgree'.tr,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodySmall!
|
|
||||||
.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurface
|
|
||||||
.withOpacity(0.75),
|
|
||||||
),
|
),
|
||||||
),
|
title: Text(
|
||||||
Material(
|
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr,
|
||||||
color: Colors.transparent,
|
),
|
||||||
child: InkWell(
|
enabled: !_currentTicket!.factorTrail
|
||||||
child: Row(
|
.contains(x.id),
|
||||||
mainAxisSize: MainAxisSize.min,
|
value: _factorPicked == x.id,
|
||||||
children: [
|
onChanged: (value) {
|
||||||
Text('termAcceptLink'.tr),
|
if (value == true) {
|
||||||
const Gap(4),
|
setState(() => _factorPicked = x.id);
|
||||||
const Icon(Icons.launch, size: 14),
|
}
|
||||||
],
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
)
|
||||||
launchUrlString('https://solsynth.dev/terms');
|
.toList() ??
|
||||||
},
|
List.empty(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
'signinMultiFactor'.trParams(
|
||||||
|
{'n': _currentTicket!.stepRemain.toString()},
|
||||||
|
),
|
||||||
|
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||||
|
).paddingOnly(left: 16, right: 16),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: (_isBusy || _period > 1)
|
||||||
|
? null
|
||||||
|
: () => _previousStep(),
|
||||||
|
style:
|
||||||
|
TextButton.styleFrom(foregroundColor: Colors.grey),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.chevron_left),
|
||||||
|
Text('prev'.tr),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).paddingSymmetric(horizontal: 16),
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
_isBusy ? null : () => _performGetFactorCode(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next'.tr),
|
||||||
|
const Icon(Icons.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
2 => ListView(
|
||||||
|
key: const ValueKey<int>(2),
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child:
|
||||||
|
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||||
|
).paddingOnly(bottom: 8, left: 4),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'signinEnterPassword'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
],
|
).paddingOnly(left: 4, bottom: 16),
|
||||||
),
|
TextField(
|
||||||
},
|
autocorrect: false,
|
||||||
),
|
enableSuggestions: false,
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
autofillHints: [
|
||||||
|
(_factorLabelMap[_factorPickedType]?.$3 ?? true)
|
||||||
|
? AutofillHints.password
|
||||||
|
: AutofillHints.oneTimeCode
|
||||||
|
],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText:
|
||||||
|
(_factorLabelMap[_factorPickedType]?.$3 ?? true)
|
||||||
|
? 'passwordOneTime'.tr
|
||||||
|
: 'password'.tr,
|
||||||
|
helperText:
|
||||||
|
(_factorLabelMap[_factorPickedType]?.$3 ?? true)
|
||||||
|
? 'passwordOneTimeInputHint'.tr
|
||||||
|
: 'passwordInputHint'.tr,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onSubmitted: _isBusy ? null : (_) => _performCheckTicket(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _previousStep(),
|
||||||
|
style:
|
||||||
|
TextButton.styleFrom(foregroundColor: Colors.grey),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.chevron_left),
|
||||||
|
Text('prev'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _performCheckTicket(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next'.tr),
|
||||||
|
const Icon(Icons.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_ => ListView(
|
||||||
|
key: const ValueKey<int>(0),
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child:
|
||||||
|
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||||
|
).paddingOnly(bottom: 8, left: 4),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'signinGreeting'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
).paddingOnly(left: 4, bottom: 16),
|
||||||
|
TextField(
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _usernameController,
|
||||||
|
autofillHints: const [AutofillHints.username],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'username'.tr,
|
||||||
|
helperText: 'usernameInputHint'.tr,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onSubmitted: _isBusy ? null : (_) => _performNewTicket(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
_isBusy ? null : () => _requestResetPassword(),
|
||||||
|
style:
|
||||||
|
TextButton.styleFrom(foregroundColor: Colors.grey),
|
||||||
|
child: Text('forgotPassword'.tr),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _performNewTicket(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next'.tr),
|
||||||
|
const Icon(Icons.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 290),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'termAcceptNextWithAgree'.tr,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('termAcceptLink'.tr),
|
||||||
|
const Gap(4),
|
||||||
|
const Icon(Icons.launch, size: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingSymmetric(horizontal: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
).paddingAll(24),
|
).paddingAll(24),
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,6 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
import 'package:solian/widgets/root_container.dart';
|
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
@ -66,147 +65,141 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RootContainer(
|
return CenteredContainer(
|
||||||
child: CenteredContainer(
|
maxWidth: 360,
|
||||||
maxWidth: 360,
|
child: ListView(
|
||||||
child: ListView(
|
shrinkWrap: true,
|
||||||
shrinkWrap: true,
|
children: [
|
||||||
children: [
|
Align(
|
||||||
Align(
|
alignment: Alignment.centerLeft,
|
||||||
alignment: Alignment.centerLeft,
|
child: ClipRRect(
|
||||||
child: ClipRRect(
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
child: Image.asset('assets/logo.png', width: 64, height: 64),
|
||||||
child: Image.asset('assets/logo.png', width: 64, height: 64),
|
).paddingOnly(bottom: 8, left: 4),
|
||||||
).paddingOnly(bottom: 8, left: 4),
|
),
|
||||||
|
Text(
|
||||||
|
'signupGreeting'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
Text(
|
).paddingOnly(left: 4, bottom: 16),
|
||||||
'signupGreeting'.tr,
|
TextField(
|
||||||
style: const TextStyle(
|
autocorrect: false,
|
||||||
fontSize: 28,
|
enableSuggestions: false,
|
||||||
fontWeight: FontWeight.w900,
|
controller: _usernameController,
|
||||||
),
|
autofillHints: const [AutofillHints.username],
|
||||||
).paddingOnly(left: 4, bottom: 16),
|
decoration: InputDecoration(
|
||||||
TextField(
|
isDense: true,
|
||||||
autocorrect: false,
|
border: const OutlineInputBorder(),
|
||||||
enableSuggestions: false,
|
labelText: 'username'.tr,
|
||||||
controller: _usernameController,
|
|
||||||
autofillHints: const [AutofillHints.username],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
labelText: 'username'.tr,
|
|
||||||
),
|
|
||||||
onTapOutside: (_) =>
|
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(12),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
TextField(
|
),
|
||||||
autocorrect: false,
|
const Gap(12),
|
||||||
enableSuggestions: false,
|
TextField(
|
||||||
controller: _nicknameController,
|
autocorrect: false,
|
||||||
autofillHints: const [AutofillHints.nickname],
|
enableSuggestions: false,
|
||||||
decoration: InputDecoration(
|
controller: _nicknameController,
|
||||||
isDense: true,
|
autofillHints: const [AutofillHints.nickname],
|
||||||
border: const OutlineInputBorder(),
|
decoration: InputDecoration(
|
||||||
labelText: 'nickname'.tr,
|
isDense: true,
|
||||||
),
|
border: const OutlineInputBorder(),
|
||||||
onTapOutside: (_) =>
|
labelText: 'nickname'.tr,
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(12),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
TextField(
|
),
|
||||||
autocorrect: false,
|
const Gap(12),
|
||||||
enableSuggestions: false,
|
TextField(
|
||||||
controller: _emailController,
|
autocorrect: false,
|
||||||
autofillHints: const [AutofillHints.email],
|
enableSuggestions: false,
|
||||||
decoration: InputDecoration(
|
controller: _emailController,
|
||||||
isDense: true,
|
autofillHints: const [AutofillHints.email],
|
||||||
border: const OutlineInputBorder(),
|
decoration: InputDecoration(
|
||||||
labelText: 'email'.tr,
|
isDense: true,
|
||||||
),
|
border: const OutlineInputBorder(),
|
||||||
onTapOutside: (_) =>
|
labelText: 'email'.tr,
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(12),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
TextField(
|
),
|
||||||
obscureText: true,
|
const Gap(12),
|
||||||
autocorrect: false,
|
TextField(
|
||||||
enableSuggestions: false,
|
obscureText: true,
|
||||||
autofillHints: const [AutofillHints.password],
|
autocorrect: false,
|
||||||
controller: _passwordController,
|
enableSuggestions: false,
|
||||||
decoration: InputDecoration(
|
autofillHints: const [AutofillHints.password],
|
||||||
isDense: true,
|
controller: _passwordController,
|
||||||
border: const OutlineInputBorder(),
|
decoration: InputDecoration(
|
||||||
labelText: 'password'.tr,
|
isDense: true,
|
||||||
),
|
border: const OutlineInputBorder(),
|
||||||
onTapOutside: (_) =>
|
labelText: 'password'.tr,
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
onSubmitted: (_) => _performAction(context),
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
CheckboxListTile(
|
onSubmitted: (_) => _performAction(context),
|
||||||
value: _isTermAccepted,
|
),
|
||||||
title: Text(
|
const Gap(8),
|
||||||
'termAccept'.tr,
|
CheckboxListTile(
|
||||||
style: const TextStyle(height: 1.2),
|
value: _isTermAccepted,
|
||||||
).paddingOnly(bottom: 4),
|
title: Text(
|
||||||
shape: const RoundedRectangleBorder(
|
'termAccept'.tr,
|
||||||
borderRadius: BorderRadius.all(
|
style: const TextStyle(height: 1.2),
|
||||||
Radius.circular(8),
|
).paddingOnly(bottom: 4),
|
||||||
),
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
),
|
),
|
||||||
subtitle: RichText(
|
),
|
||||||
text: TextSpan(
|
subtitle: RichText(
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
text: TextSpan(
|
||||||
color: Theme.of(context)
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
.colorScheme
|
color: Theme.of(context)
|
||||||
.onSurface
|
.colorScheme
|
||||||
.withOpacity(0.75),
|
.onSurface
|
||||||
),
|
.withOpacity(0.75),
|
||||||
children: [
|
),
|
||||||
TextSpan(text: 'termAcceptDesc'.tr),
|
children: [
|
||||||
WidgetSpan(
|
TextSpan(text: 'termAcceptDesc'.tr),
|
||||||
child: Material(
|
WidgetSpan(
|
||||||
color: Colors.transparent,
|
child: Material(
|
||||||
child: InkWell(
|
color: Colors.transparent,
|
||||||
child: Row(
|
child: InkWell(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Row(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Text('termAcceptLink'.tr),
|
children: [
|
||||||
const Gap(4),
|
Text('termAcceptLink'.tr),
|
||||||
const Icon(Icons.launch, size: 14),
|
const Gap(4),
|
||||||
],
|
const Icon(Icons.launch, size: 14),
|
||||||
),
|
],
|
||||||
onTap: () {
|
|
||||||
launchUrlString('https://solsynth.dev/terms');
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
|
||||||
setState(() => _isTermAccepted = value ?? false);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const Gap(16),
|
onChanged: (value) {
|
||||||
Align(
|
setState(() => _isTermAccepted = value ?? false);
|
||||||
alignment: Alignment.centerRight,
|
},
|
||||||
child: TextButton(
|
),
|
||||||
onPressed:
|
const Gap(16),
|
||||||
!_isTermAccepted ? null : () => _performAction(context),
|
Align(
|
||||||
child: Row(
|
alignment: Alignment.centerRight,
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: TextButton(
|
||||||
children: [
|
onPressed:
|
||||||
Text('next'.tr),
|
!_isTermAccepted ? null : () => _performAction(context),
|
||||||
const Icon(Icons.chevron_right),
|
child: Row(
|
||||||
],
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: [
|
||||||
|
Text('next'.tr),
|
||||||
|
const Icon(Icons.chevron_right),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
).paddingAll(24),
|
).paddingAll(24),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ import 'package:solian/providers/database/database.dart';
|
|||||||
import 'package:solian/providers/theme_switcher.dart';
|
import 'package:solian/providers/theme_switcher.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/widgets/reports/abuse_report.dart';
|
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||||
import 'package:solian/widgets/root_container.dart';
|
|
||||||
|
|
||||||
class SettingScreen extends StatefulWidget {
|
class SettingScreen extends StatefulWidget {
|
||||||
const SettingScreen({super.key});
|
const SettingScreen({super.key});
|
||||||
@ -83,259 +82,258 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RootContainer(
|
return ListView(
|
||||||
child: ListView(
|
children: [
|
||||||
children: [
|
_buildCaptionHeader('theme'.tr),
|
||||||
_buildCaptionHeader('theme'.tr),
|
ListTile(
|
||||||
ListTile(
|
leading: const Icon(Icons.palette),
|
||||||
leading: const Icon(Icons.palette),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
title: Text('globalTheme'.tr),
|
||||||
title: Text('globalTheme'.tr),
|
trailing: DropdownButtonHideUnderline(
|
||||||
trailing: DropdownButtonHideUnderline(
|
child: DropdownButton2<SolianThemeData>(
|
||||||
child: DropdownButton2<SolianThemeData>(
|
isExpanded: true,
|
||||||
isExpanded: true,
|
hint: Text(
|
||||||
hint: Text(
|
'theme'.tr,
|
||||||
'theme'.tr,
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 14,
|
||||||
fontSize: 14,
|
color: Theme.of(context).hintColor,
|
||||||
color: Theme.of(context).hintColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
items: _presentTheme
|
),
|
||||||
.map((SolianThemeData item) =>
|
items: _presentTheme
|
||||||
DropdownMenuItem<SolianThemeData>(
|
.map((SolianThemeData item) =>
|
||||||
value: item,
|
DropdownMenuItem<SolianThemeData>(
|
||||||
child: Row(
|
value: item,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: Row(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Icon(Icons.circle, color: item.seedColor),
|
children: [
|
||||||
const Gap(8),
|
Icon(Icons.circle, color: item.seedColor),
|
||||||
Text(
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
item.id.tr,
|
item.id.tr,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
))
|
),
|
||||||
.toList(),
|
))
|
||||||
value: (_prefs?.containsKey('global_theme') ?? false)
|
.toList(),
|
||||||
? SolianThemeData.fromJson(
|
value: (_prefs?.containsKey('global_theme') ?? false)
|
||||||
jsonDecode(_prefs!.getString('global_theme')!),
|
? SolianThemeData.fromJson(
|
||||||
)
|
jsonDecode(_prefs!.getString('global_theme')!),
|
||||||
: null,
|
)
|
||||||
onChanged: (SolianThemeData? value) {
|
: null,
|
||||||
context.read<ThemeSwitcher>().setThemeData(value);
|
onChanged: (SolianThemeData? value) {
|
||||||
setState(() {});
|
context.read<ThemeSwitcher>().setThemeData(value);
|
||||||
},
|
setState(() {});
|
||||||
buttonStyleData: const ButtonStyleData(
|
},
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
buttonStyleData: const ButtonStyleData(
|
||||||
height: 40,
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
width: 140,
|
height: 40,
|
||||||
),
|
width: 140,
|
||||||
menuItemStyleData: const MenuItemStyleData(
|
),
|
||||||
height: 40,
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
),
|
height: 40,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
CheckboxListTile(
|
),
|
||||||
secondary: const Icon(Icons.military_tech),
|
CheckboxListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
secondary: const Icon(Icons.military_tech),
|
||||||
title: Text('agedTheme'.tr),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
subtitle: Text('agedThemeDesc'.tr),
|
title: Text('agedTheme'.tr),
|
||||||
value: _prefs?.getBool('aged_theme') ?? false,
|
subtitle: Text('agedThemeDesc'.tr),
|
||||||
onChanged: (value) {
|
value: _prefs?.getBool('aged_theme') ?? false,
|
||||||
if (value != null) {
|
onChanged: (value) {
|
||||||
context.read<ThemeSwitcher>().setAgedTheme(value);
|
if (value != null) {
|
||||||
|
context.read<ThemeSwitcher>().setAgedTheme(value);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!PlatformInfo.isWeb)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.wallpaper),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 22, right: 31),
|
||||||
|
title: Text('appBackgroundImage'.tr),
|
||||||
|
subtitle: Text('appBackgroundImageDesc'.tr),
|
||||||
|
trailing: File('$_docBasepath/app_background_image').existsSync()
|
||||||
|
? const Icon(Icons.check_box)
|
||||||
|
: const Icon(Icons.check_box_outline_blank),
|
||||||
|
onTap: () async {
|
||||||
|
if (File('$_docBasepath/app_background_image').existsSync()) {
|
||||||
|
File('$_docBasepath/app_background_image').deleteSync();
|
||||||
|
} else {
|
||||||
|
final image = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
);
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
await File(image.path)
|
||||||
|
.copy('$_docBasepath/app_background_image');
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (!PlatformInfo.isWeb)
|
_buildCaptionHeader('notification'.tr),
|
||||||
ListTile(
|
Tooltip(
|
||||||
leading: const Icon(Icons.wallpaper),
|
message: 'settingsNotificationBgServiceDesc'.tr,
|
||||||
contentPadding: const EdgeInsets.only(left: 22, right: 31),
|
child: CheckboxListTile(
|
||||||
title: Text('appBackgroundImage'.tr),
|
|
||||||
subtitle: Text('appBackgroundImageDesc'.tr),
|
|
||||||
trailing: File('$_docBasepath/app_background_image').existsSync()
|
|
||||||
? const Icon(Icons.check_box)
|
|
||||||
: const Icon(Icons.check_box_outline_blank),
|
|
||||||
onTap: () async {
|
|
||||||
if (File('$_docBasepath/app_background_image').existsSync()) {
|
|
||||||
File('$_docBasepath/app_background_image').deleteSync();
|
|
||||||
} else {
|
|
||||||
final image = await ImagePicker().pickImage(
|
|
||||||
source: ImageSource.gallery,
|
|
||||||
);
|
|
||||||
if (image == null) return;
|
|
||||||
|
|
||||||
await File(image.path)
|
|
||||||
.copy('$_docBasepath/app_background_image');
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_buildCaptionHeader('notification'.tr),
|
|
||||||
Tooltip(
|
|
||||||
message: 'settingsNotificationBgServiceDesc'.tr,
|
|
||||||
child: CheckboxListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
|
||||||
secondary: const Icon(Icons.system_security_update_warning),
|
|
||||||
enabled: PlatformInfo.isAndroid,
|
|
||||||
title: Text('settingsNotificationBgService'.tr),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text('holdToSeeDetail'.tr),
|
|
||||||
Text(
|
|
||||||
'needRestartToApply'.tr,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
value:
|
|
||||||
_prefs?.getBool('service_background_notification') ?? false,
|
|
||||||
onChanged: (value) {
|
|
||||||
_prefs
|
|
||||||
?.setBool('service_background_notification', value ?? false)
|
|
||||||
.then((_) {
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildCaptionHeader('update'.tr),
|
|
||||||
CheckboxListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
secondary: const Icon(Icons.sync_alt),
|
secondary: const Icon(Icons.system_security_update_warning),
|
||||||
title: Text('updateCheckStrictly'.tr),
|
enabled: PlatformInfo.isAndroid,
|
||||||
subtitle: Text('updateCheckStrictlyDesc'.tr),
|
title: Text('settingsNotificationBgService'.tr),
|
||||||
value: _prefs?.getBool('check_update_strictly') ?? false,
|
subtitle: Column(
|
||||||
onChanged: (value) {
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
_prefs
|
|
||||||
?.setBool('check_update_strictly', value ?? false)
|
|
||||||
.then((_) {
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
final AuthProvider auth = Get.find<AuthProvider>();
|
|
||||||
if (!auth.isAuthorized.value) return const SizedBox.shrink();
|
|
||||||
return Column(
|
|
||||||
children: [
|
children: [
|
||||||
_buildCaptionHeader('account'.tr),
|
Text('holdToSeeDetail'.tr),
|
||||||
ListTile(
|
Text(
|
||||||
leading: const Icon(Icons.flag),
|
'needRestartToApply'.tr,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
)
|
||||||
title: Text('reportAbuse'.tr),
|
|
||||||
subtitle: Text('reportAbuseDesc'.tr),
|
|
||||||
onTap: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => const AbuseReportDialog(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.person_remove),
|
|
||||||
trailing: const Icon(Icons.chevron_right),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
|
||||||
title: Text('accountDeletion'.tr),
|
|
||||||
subtitle: Text('accountDeletionDesc'.tr),
|
|
||||||
onTap: () {
|
|
||||||
context
|
|
||||||
.showSlideToConfirmDialog(
|
|
||||||
'accountDeletionConfirm'.tr,
|
|
||||||
'accountDeletionConfirmDesc'.trParams({
|
|
||||||
'account': '@${auth.userProfile.value!['name']}',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.then((value) async {
|
|
||||||
if (value != true) return;
|
|
||||||
final client = await auth.configureClient('id');
|
|
||||||
final resp = await client.post('/users/me/deletion', {});
|
|
||||||
if (resp.statusCode != 200) {
|
|
||||||
context.showErrorDialog(RequestException(resp));
|
|
||||||
} else {
|
|
||||||
context.showSnackbar('accountDeletionRequested'.tr);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
}),
|
value: _prefs?.getBool('service_background_notification') ?? false,
|
||||||
_buildCaptionHeader('performance'.tr),
|
|
||||||
CheckboxListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
|
||||||
secondary: const Icon(Icons.message),
|
|
||||||
title: Text('animatedMessageList'.tr),
|
|
||||||
subtitle: Text('animatedMessageListDesc'.tr),
|
|
||||||
value: _prefs?.getBool('non_animated_message_list') ?? false,
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_prefs
|
_prefs
|
||||||
?.setBool('non_animated_message_list', value ?? false)
|
?.setBool('service_background_notification', value ?? false)
|
||||||
.then((_) {
|
.then((_) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildCaptionHeader('more'.tr),
|
),
|
||||||
ListTile(
|
_buildCaptionHeader('update'.tr),
|
||||||
leading: const Icon(Icons.delete_sweep),
|
CheckboxListTile(
|
||||||
trailing: const Icon(Icons.chevron_right),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
subtitle: FutureBuilder(
|
secondary: const Icon(Icons.sync_alt),
|
||||||
future: AppDatabase.getDatabaseSize(),
|
title: Text('updateCheckStrictly'.tr),
|
||||||
builder: (context, snapshot) {
|
subtitle: Text('updateCheckStrictlyDesc'.tr),
|
||||||
if (!snapshot.hasData) {
|
value: _prefs?.getBool('check_update_strictly') ?? false,
|
||||||
return Text('localDatabaseSize'.trParams(
|
onChanged: (value) {
|
||||||
{'size': 'unknown'.tr},
|
_prefs?.setBool('check_update_strictly', value ?? false).then((_) {
|
||||||
));
|
setState(() {});
|
||||||
}
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Obx(() {
|
||||||
|
final AuthProvider auth = Get.find<AuthProvider>();
|
||||||
|
if (!auth.isAuthorized.value) return const SizedBox.shrink();
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildCaptionHeader('account'.tr),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.flag),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
|
title: Text('reportAbuse'.tr),
|
||||||
|
subtitle: Text('reportAbuseDesc'.tr),
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const AbuseReportDialog(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.person_remove),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
|
title: Text('accountDeletion'.tr),
|
||||||
|
subtitle: Text('accountDeletionDesc'.tr),
|
||||||
|
onTap: () {
|
||||||
|
context
|
||||||
|
.showSlideToConfirmDialog(
|
||||||
|
'accountDeletionConfirm'.tr,
|
||||||
|
'accountDeletionConfirmDesc'.trParams({
|
||||||
|
'account': '@${auth.userProfile.value!['name']}',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then((value) async {
|
||||||
|
if (value != true) return;
|
||||||
|
final client = await auth.configureClient('id');
|
||||||
|
final resp = await client.post('/users/me/deletion', {});
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
context.showErrorDialog(RequestException(resp));
|
||||||
|
} else {
|
||||||
|
context.showSnackbar('accountDeletionRequested'.tr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
_buildCaptionHeader('performance'.tr),
|
||||||
|
CheckboxListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
|
secondary: const Icon(Icons.message),
|
||||||
|
title: Text('animatedMessageList'.tr),
|
||||||
|
subtitle: Text('animatedMessageListDesc'.tr),
|
||||||
|
value: _prefs?.getBool('non_animated_message_list') ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
_prefs
|
||||||
|
?.setBool('non_animated_message_list', value ?? false)
|
||||||
|
.then((_) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildCaptionHeader('more'.tr),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.delete_sweep),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
subtitle: FutureBuilder(
|
||||||
|
future: AppDatabase.getDatabaseSize(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
return Text('localDatabaseSize'.trParams(
|
return Text('localDatabaseSize'.trParams(
|
||||||
{'size': snapshot.data!.formatBytes()},
|
{'size': 'unknown'.tr},
|
||||||
));
|
));
|
||||||
},
|
}
|
||||||
),
|
return Text('localDatabaseSize'.trParams(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
{'size': snapshot.data!.formatBytes()},
|
||||||
title: Text('localDatabaseWipe'.tr),
|
));
|
||||||
onTap: () {
|
|
||||||
AppDatabase.removeDatabase().then((_) {
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (PlatformInfo.canRateTheApp)
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
ListTile(
|
title: Text('localDatabaseWipe'.tr),
|
||||||
leading: const Icon(Icons.star),
|
onTap: () {
|
||||||
trailing: const Icon(Icons.chevron_right),
|
AppDatabase.removeDatabase().then((_) {
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
setState(() {});
|
||||||
title: Text('rateTheApp'.tr),
|
});
|
||||||
subtitle: Text('rateTheAppDesc'.tr),
|
},
|
||||||
onTap: () {
|
),
|
||||||
final inAppReview = InAppReview.instance;
|
if (PlatformInfo.canRateTheApp)
|
||||||
|
|
||||||
inAppReview.openStoreListing(
|
|
||||||
appStoreId: '6499032345',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.info_outline),
|
leading: const Icon(Icons.star),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
title: Text('about'.tr),
|
title: Text('rateTheApp'.tr),
|
||||||
|
subtitle: Text('rateTheAppDesc'.tr),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AppRouter.instance.pushNamed('about');
|
final inAppReview = InAppReview.instance;
|
||||||
|
|
||||||
|
inAppReview.openStoreListing(
|
||||||
|
appStoreId: '6499032345',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
ListTile(
|
||||||
),
|
leading: const Icon(Icons.info_outline),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
|
title: Text('about'.tr),
|
||||||
|
onTap: () {
|
||||||
|
AppRouter.instance.pushNamed('about');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,10 +39,13 @@ abstract class AppTheme {
|
|||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
|
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: Colors.transparent,
|
|
||||||
snackBarTheme: const SnackBarThemeData(
|
snackBarTheme: const SnackBarThemeData(
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
|
scaffoldBackgroundColor: Colors.transparent,
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
fontFamily: 'Comfortaa',
|
fontFamily: 'Comfortaa',
|
||||||
fontFamilyFallback: [
|
fontFamilyFallback: [
|
||||||
'NotoSansSC',
|
'NotoSansSC',
|
||||||
@ -74,6 +77,7 @@ abstract class AppTheme {
|
|||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: Colors.transparent,
|
scaffoldBackgroundColor: Colors.transparent,
|
||||||
|
appBarTheme: const AppBarTheme(backgroundColor: Colors.transparent),
|
||||||
fontFamily: data.fontFamily ?? 'Comfortaa',
|
fontFamily: data.fontFamily ?? 'Comfortaa',
|
||||||
fontFamilyFallback: data.fontFamilyFallback ??
|
fontFamilyFallback: data.fontFamilyFallback ??
|
||||||
[
|
[
|
||||||
|
@ -43,6 +43,10 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
if (isAutoWarp) {
|
if (isAutoWarp) {
|
||||||
paragraph = paragraph.replaceAll('\n', '\\\n');
|
paragraph = paragraph.replaceAll('\n', '\\\n');
|
||||||
}
|
}
|
||||||
|
const charactersToTrim = '\\\n\t\r ';
|
||||||
|
final trimPattern =
|
||||||
|
RegExp('^[$charactersToTrim]+|[$charactersToTrim]+\$');
|
||||||
|
paragraph = paragraph.trim().replaceAll(trimPattern, '');
|
||||||
|
|
||||||
// Matching stickers
|
// Matching stickers
|
||||||
final stickerMatch = stickerRegex.allMatches(paragraph);
|
final stickerMatch = stickerRegex.allMatches(paragraph);
|
||||||
@ -184,7 +188,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (idx < paragraphs.length - 1) {
|
if (idx < paragraphs.length - 1) {
|
||||||
contentWidgets.add(const Gap(4));
|
contentWidgets.add(isAutoWarp ? const Gap(4) : const Gap(8));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user