💄 More transparency

This commit is contained in:
LittleSheep 2024-10-06 23:06:33 +08:00
parent 2e9c4d166e
commit d7e6fe2d8f
9 changed files with 1019 additions and 1041 deletions

View File

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

View File

@ -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(() {});
},
),
],
),
);
}),
); );
} }
} }

View File

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

View File

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

View File

@ -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),
); );

View File

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

View File

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

View File

@ -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 ??
[ [

View File

@ -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));
} }
} }