💄 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:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
@ -16,132 +15,130 @@ class AboutScreen extends StatelessWidget {
|
||||
const denseButtonStyle =
|
||||
ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
||||
|
||||
return RootContainer(
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/logo.png', width: 120, height: 120),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Solian',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const Text(
|
||||
'The Solar Network',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/logo.png', width: 120, height: 120),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Solian',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const Text(
|
||||
'The Solar Network',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Text(
|
||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||
const Gap(16),
|
||||
CenteredContainer(
|
||||
maxWidth: 280,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('appDetails'.tr),
|
||||
onPressed: () async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
return Text(
|
||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||
const Gap(16),
|
||||
CenteredContainer(
|
||||
maxWidth: 280,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('appDetails'.tr),
|
||||
onPressed: () async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationVersion:
|
||||
'${info.version} (${info.buildNumber})',
|
||||
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.',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/logo.png',
|
||||
width: 60, height: 60),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('projectWebsite'.tr),
|
||||
onPressed: () {
|
||||
launchUrlString(
|
||||
'https://solsynth.dev/products/solar-network');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('termRelated'.tr),
|
||||
onPressed: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('serviceStatus'.tr),
|
||||
onPressed: () {
|
||||
launchUrlString('https://status.solsynth.dev');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationVersion:
|
||||
'${info.version} (${info.buildNumber})',
|
||||
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.',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/logo.png',
|
||||
width: 60, height: 60),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('projectWebsite'.tr),
|
||||
onPressed: () {
|
||||
launchUrlString(
|
||||
'https://solsynth.dev/products/solar-network');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('termRelated'.tr),
|
||||
onPressed: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('serviceStatus'.tr),
|
||||
onPressed: () {
|
||||
launchUrlString('https://status.solsynth.dev');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
const Text(
|
||||
'Open-sourced under AGPLv3',
|
||||
style: TextStyle(
|
||||
),
|
||||
const Gap(16),
|
||||
const Text(
|
||||
'Open-sourced under AGPLv3',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: SharedPreferences.getInstance(),
|
||||
builder: (context, snapshot) {
|
||||
const textStyle = TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: SharedPreferences.getInstance(),
|
||||
builder: (context, snapshot) {
|
||||
const textStyle = TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 12,
|
||||
);
|
||||
if (!snapshot.hasData ||
|
||||
!snapshot.data!.containsKey('first_boot_time')) {
|
||||
return Text(
|
||||
'firstBootTime'.trParams({'time': 'unknown'.tr}),
|
||||
style: textStyle,
|
||||
);
|
||||
if (!snapshot.hasData ||
|
||||
!snapshot.data!.containsKey('first_boot_time')) {
|
||||
return Text(
|
||||
'firstBootTime'.trParams({'time': 'unknown'.tr}),
|
||||
style: textStyle,
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
'firstBootTime'.trParams({
|
||||
'time': DateFormat('yyyy-MM-dd').format(
|
||||
DateTime.tryParse(
|
||||
snapshot.data!.getString('first_boot_time')!,
|
||||
)?.toLocal() ??
|
||||
DateTime.now(),
|
||||
),
|
||||
}),
|
||||
style: textStyle,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
} else {
|
||||
return Text(
|
||||
'firstBootTime'.trParams({
|
||||
'time': DateFormat('yyyy-MM-dd').format(
|
||||
DateTime.tryParse(
|
||||
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/router.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:badges/badges.dart' as badges;
|
||||
|
||||
@ -50,112 +49,110 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
return RootContainer(
|
||||
child: SafeArea(
|
||||
child: Obx(() {
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return Center(
|
||||
child: Column(
|
||||
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(
|
||||
return SafeArea(
|
||||
child: Obx(() {
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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(() {}));
|
||||
},
|
||||
_ActionCard(
|
||||
icon: Icon(
|
||||
Icons.login,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
)),
|
||||
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),
|
||||
title: 'signin'.tr,
|
||||
caption: 'signinCaption'.tr,
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
AppRouter.instance.pushNamed('signin').then((val) async {
|
||||
if (val == true) {
|
||||
await auth.refreshUserProfile();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
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');
|
||||
},
|
||||
_ActionCard(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
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),
|
||||
title: 'signup'.tr,
|
||||
caption: 'signupCaption'.tr,
|
||||
onTap: () {
|
||||
auth.signout();
|
||||
setState(() {});
|
||||
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: [
|
||||
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/exts.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
|
||||
class NotificationPreferencesScreen extends StatefulWidget {
|
||||
const NotificationPreferencesScreen({super.key});
|
||||
@ -75,44 +74,42 @@ class _NotificationPreferencesScreenState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RootContainer(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.save),
|
||||
title: Text('save'.tr),
|
||||
enabled: !_isBusy,
|
||||
onTap: () {
|
||||
_savePreferences();
|
||||
return Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.save),
|
||||
title: Text('save'.tr),
|
||||
enabled: !_isBusy,
|
||||
onTap: () {
|
||||
_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) {
|
||||
const double padding = 32;
|
||||
|
||||
return RootContainer(
|
||||
child: ListView(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
children: [
|
||||
AccountAvatar(content: _avatar, radius: 40),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 40,
|
||||
child: FloatingActionButton.small(
|
||||
heroTag: const Key('avatar-editor'),
|
||||
onPressed: () => _editImage('avatar'),
|
||||
child: const Icon(
|
||||
Icons.camera,
|
||||
),
|
||||
return ListView(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
children: [
|
||||
AccountAvatar(content: _avatar, radius: 40),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 40,
|
||||
child: FloatingActionButton.small(
|
||||
heroTag: const Key('avatar-editor'),
|
||||
onPressed: () => _editImage('avatar'),
|
||||
child: const Icon(
|
||||
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),
|
||||
TextField(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'birthday'.tr,
|
||||
],
|
||||
).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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () => _selectBirthday(),
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _syncWidget(),
|
||||
child: Text('reset'.tr),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: FloatingActionButton(
|
||||
heroTag: const Key('banner-editor'),
|
||||
onPressed: () => _editImage('banner'),
|
||||
child: const Icon(
|
||||
Icons.camera_alt,
|
||||
),
|
||||
),
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return RootContainer(
|
||||
child: CenteredContainer(
|
||||
maxWidth: 360,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: switch (_period % 3) {
|
||||
1 => ListView(
|
||||
shrinkWrap: true,
|
||||
key: const ValueKey<int>(1),
|
||||
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),
|
||||
return CenteredContainer(
|
||||
maxWidth: 360,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: switch (_period % 3) {
|
||||
1 => ListView(
|
||||
shrinkWrap: true,
|
||||
key: const ValueKey<int>(1),
|
||||
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(
|
||||
'signinPickFactor'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
Text(
|
||||
'signinPickFactor'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
).paddingOnly(left: 4, bottom: 16),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
children: _factors
|
||||
?.map(
|
||||
(x) => CheckboxListTile(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
).paddingOnly(left: 4, bottom: 16),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.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),
|
||||
],
|
||||
),
|
||||
),
|
||||
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),
|
||||
secondary: Icon(
|
||||
_factorLabelMap[x.type]?.$2 ??
|
||||
Icons.question_mark,
|
||||
),
|
||||
),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('termAcceptLink'.tr),
|
||||
const Gap(4),
|
||||
const Icon(Icons.launch, size: 14),
|
||||
],
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
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),
|
||||
);
|
||||
|
@ -3,7 +3,6 @@ import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
@ -66,147 +65,141 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RootContainer(
|
||||
child: CenteredContainer(
|
||||
maxWidth: 360,
|
||||
child: ListView(
|
||||
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),
|
||||
return CenteredContainer(
|
||||
maxWidth: 360,
|
||||
child: ListView(
|
||||
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(
|
||||
'signupGreeting'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
Text(
|
||||
'signupGreeting'.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,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).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,
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _nicknameController,
|
||||
autofillHints: const [AutofillHints.nickname],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'nickname'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _nicknameController,
|
||||
autofillHints: const [AutofillHints.nickname],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'nickname'.tr,
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _emailController,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'email'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _emailController,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'email'.tr,
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'password'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onSubmitted: (_) => _performAction(context),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'password'.tr,
|
||||
),
|
||||
const Gap(8),
|
||||
CheckboxListTile(
|
||||
value: _isTermAccepted,
|
||||
title: Text(
|
||||
'termAccept'.tr,
|
||||
style: const TextStyle(height: 1.2),
|
||||
).paddingOnly(bottom: 4),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onSubmitted: (_) => _performAction(context),
|
||||
),
|
||||
const Gap(8),
|
||||
CheckboxListTile(
|
||||
value: _isTermAccepted,
|
||||
title: Text(
|
||||
'termAccept'.tr,
|
||||
style: const TextStyle(height: 1.2),
|
||||
).paddingOnly(bottom: 4),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
subtitle: RichText(
|
||||
text: TextSpan(
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.75),
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: 'termAcceptDesc'.tr),
|
||||
WidgetSpan(
|
||||
child: 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');
|
||||
},
|
||||
),
|
||||
subtitle: RichText(
|
||||
text: TextSpan(
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.75),
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: 'termAcceptDesc'.tr),
|
||||
WidgetSpan(
|
||||
child: 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');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() => _isTermAccepted = value ?? false);
|
||||
},
|
||||
),
|
||||
const Gap(16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed:
|
||||
!_isTermAccepted ? null : () => _performAction(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('next'.tr),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() => _isTermAccepted = value ?? false);
|
||||
},
|
||||
),
|
||||
const Gap(16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed:
|
||||
!_isTermAccepted ? null : () => _performAction(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('next'.tr),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
).paddingAll(24),
|
||||
);
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ import 'package:solian/providers/database/database.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
|
||||
class SettingScreen extends StatefulWidget {
|
||||
const SettingScreen({super.key});
|
||||
@ -83,259 +82,258 @@ class _SettingScreenState extends State<SettingScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RootContainer(
|
||||
child: ListView(
|
||||
children: [
|
||||
_buildCaptionHeader('theme'.tr),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.palette),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('globalTheme'.tr),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SolianThemeData>(
|
||||
isExpanded: true,
|
||||
hint: Text(
|
||||
'theme'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
return ListView(
|
||||
children: [
|
||||
_buildCaptionHeader('theme'.tr),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.palette),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('globalTheme'.tr),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SolianThemeData>(
|
||||
isExpanded: true,
|
||||
hint: Text(
|
||||
'theme'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
items: _presentTheme
|
||||
.map((SolianThemeData item) =>
|
||||
DropdownMenuItem<SolianThemeData>(
|
||||
value: item,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.circle, color: item.seedColor),
|
||||
const Gap(8),
|
||||
Text(
|
||||
),
|
||||
items: _presentTheme
|
||||
.map((SolianThemeData item) =>
|
||||
DropdownMenuItem<SolianThemeData>(
|
||||
value: item,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.circle, color: item.seedColor),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.id.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
value: (_prefs?.containsKey('global_theme') ?? false)
|
||||
? SolianThemeData.fromJson(
|
||||
jsonDecode(_prefs!.getString('global_theme')!),
|
||||
)
|
||||
: null,
|
||||
onChanged: (SolianThemeData? value) {
|
||||
context.read<ThemeSwitcher>().setThemeData(value);
|
||||
setState(() {});
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
height: 40,
|
||||
width: 140,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
value: (_prefs?.containsKey('global_theme') ?? false)
|
||||
? SolianThemeData.fromJson(
|
||||
jsonDecode(_prefs!.getString('global_theme')!),
|
||||
)
|
||||
: null,
|
||||
onChanged: (SolianThemeData? value) {
|
||||
context.read<ThemeSwitcher>().setThemeData(value);
|
||||
setState(() {});
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
height: 40,
|
||||
width: 140,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Icons.military_tech),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('agedTheme'.tr),
|
||||
subtitle: Text('agedThemeDesc'.tr),
|
||||
value: _prefs?.getBool('aged_theme') ?? false,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
context.read<ThemeSwitcher>().setAgedTheme(value);
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Icons.military_tech),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('agedTheme'.tr),
|
||||
subtitle: Text('agedThemeDesc'.tr),
|
||||
value: _prefs?.getBool('aged_theme') ?? false,
|
||||
onChanged: (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(() {});
|
||||
},
|
||||
),
|
||||
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(() {});
|
||||
},
|
||||
),
|
||||
_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(
|
||||
_buildCaptionHeader('notification'.tr),
|
||||
Tooltip(
|
||||
message: 'settingsNotificationBgServiceDesc'.tr,
|
||||
child: CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
secondary: const Icon(Icons.sync_alt),
|
||||
title: Text('updateCheckStrictly'.tr),
|
||||
subtitle: Text('updateCheckStrictlyDesc'.tr),
|
||||
value: _prefs?.getBool('check_update_strictly') ?? false,
|
||||
onChanged: (value) {
|
||||
_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(
|
||||
secondary: const Icon(Icons.system_security_update_warning),
|
||||
enabled: PlatformInfo.isAndroid,
|
||||
title: Text('settingsNotificationBgService'.tr),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
Text('holdToSeeDetail'.tr),
|
||||
Text(
|
||||
'needRestartToApply'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
_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,
|
||||
),
|
||||
value: _prefs?.getBool('service_background_notification') ?? false,
|
||||
onChanged: (value) {
|
||||
_prefs
|
||||
?.setBool('non_animated_message_list', value ?? false)
|
||||
?.setBool('service_background_notification', 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(
|
||||
{'size': 'unknown'.tr},
|
||||
));
|
||||
}
|
||||
),
|
||||
_buildCaptionHeader('update'.tr),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
secondary: const Icon(Icons.sync_alt),
|
||||
title: Text('updateCheckStrictly'.tr),
|
||||
subtitle: Text('updateCheckStrictlyDesc'.tr),
|
||||
value: _prefs?.getBool('check_update_strictly') ?? false,
|
||||
onChanged: (value) {
|
||||
_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(
|
||||
{'size': snapshot.data!.formatBytes()},
|
||||
{'size': 'unknown'.tr},
|
||||
));
|
||||
},
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('localDatabaseWipe'.tr),
|
||||
onTap: () {
|
||||
AppDatabase.removeDatabase().then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
return Text('localDatabaseSize'.trParams(
|
||||
{'size': snapshot.data!.formatBytes()},
|
||||
));
|
||||
},
|
||||
),
|
||||
if (PlatformInfo.canRateTheApp)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.star),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('rateTheApp'.tr),
|
||||
subtitle: Text('rateTheAppDesc'.tr),
|
||||
onTap: () {
|
||||
final inAppReview = InAppReview.instance;
|
||||
|
||||
inAppReview.openStoreListing(
|
||||
appStoreId: '6499032345',
|
||||
);
|
||||
},
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('localDatabaseWipe'.tr),
|
||||
onTap: () {
|
||||
AppDatabase.removeDatabase().then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
if (PlatformInfo.canRateTheApp)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
leading: const Icon(Icons.star),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('about'.tr),
|
||||
title: Text('rateTheApp'.tr),
|
||||
subtitle: Text('rateTheAppDesc'.tr),
|
||||
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,
|
||||
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.transparent,
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.transparent,
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
fontFamily: 'Comfortaa',
|
||||
fontFamilyFallback: [
|
||||
'NotoSansSC',
|
||||
@ -74,6 +77,7 @@ abstract class AppTheme {
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.transparent,
|
||||
appBarTheme: const AppBarTheme(backgroundColor: Colors.transparent),
|
||||
fontFamily: data.fontFamily ?? 'Comfortaa',
|
||||
fontFamilyFallback: data.fontFamilyFallback ??
|
||||
[
|
||||
|
@ -43,6 +43,10 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
if (isAutoWarp) {
|
||||
paragraph = paragraph.replaceAll('\n', '\\\n');
|
||||
}
|
||||
const charactersToTrim = '\\\n\t\r ';
|
||||
final trimPattern =
|
||||
RegExp('^[$charactersToTrim]+|[$charactersToTrim]+\$');
|
||||
paragraph = paragraph.trim().replaceAll(trimPattern, '');
|
||||
|
||||
// Matching stickers
|
||||
final stickerMatch = stickerRegex.allMatches(paragraph);
|
||||
@ -184,7 +188,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
);
|
||||
|
||||
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