Compare commits

..

No commits in common. "a1c4e5eca04cef112f096a66953fbafcab12457f" and "f78d3f4fd5452f00f866c859947fcf34fa07f437" have entirely different histories.

31 changed files with 549 additions and 741 deletions

View File

@ -60,6 +60,11 @@ class NavigationProvider extends ChangeNotifier {
screen: 'chat', screen: 'chat',
label: 'screenChat', label: 'screenChat',
), ),
AppNavDestination(
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
screen: 'account',
label: 'screenAccount',
),
AppNavDestination( AppNavDestination(
icon: Icon(Symbols.group, weight: 400, opticalSize: 20), icon: Icon(Symbols.group, weight: 400, opticalSize: 20),
screen: 'realm', screen: 'realm',
@ -70,11 +75,6 @@ class NavigationProvider extends ChangeNotifier {
screen: 'news', screen: 'news',
label: 'screenNews', label: 'screenNews',
), ),
AppNavDestination(
icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
screen: 'settings',
label: 'screenSettings',
),
]; ];
static const List<String> kDefaultPinnedDestination = [ static const List<String> kDefaultPinnedDestination = [
'home', 'home',

View File

@ -72,8 +72,8 @@ final _appRoutes = [
), ),
GoRoute( GoRoute(
path: '/posts', path: '/posts',
name: 'posts', name: 'explore',
builder: (_, __) => const SizedBox.shrink(), builder: (context, state) => const ExploreScreen(),
routes: [ routes: [
GoRoute( GoRoute(
path: '/draft', path: '/draft',
@ -111,195 +111,157 @@ final _appRoutes = [
state.uri.queryParameters['categories']?.split(','), state.uri.queryParameters['categories']?.split(','),
), ),
), ),
],
),
ShellRoute(
builder: (context, state, child) => ResponsiveScaffold(
asideFlex: 2,
contentFlex: 3,
aside: const ExploreScreen(),
child: child,
),
routes: [
GoRoute(
path: '/explore',
name: 'explore',
builder: (context, state) => const ResponsiveScaffoldLanding(
child: ExploreScreen(),
),
),
GoRoute(
path: '/posts/:slug',
name: 'postDetail',
builder: (context, state) => PostDetailScreen(
key: ValueKey(state.pathParameters['slug']!),
slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
),
),
GoRoute( GoRoute(
path: '/publishers/:name', path: '/publishers/:name',
name: 'postPublisher', name: 'postPublisher',
builder: (context, state) => builder: (context, state) =>
PostPublisherScreen(name: state.pathParameters['name']!), PostPublisherScreen(name: state.pathParameters['name']!),
), ),
],
),
ShellRoute(
builder: (context, state, child) => ResponsiveScaffold(
aside: const AccountScreen(),
child: child,
),
routes: [
GoRoute( GoRoute(
path: '/account', path: '/:slug',
name: 'account', name: 'postDetail',
builder: (context, state) => builder: (context, state) => PostDetailScreen(
const ResponsiveScaffoldLanding(child: AccountScreen()), slug: state.pathParameters['slug']!,
routes: [ preload: state.extra as SnPost?,
GoRoute( ),
path: '/punishments',
name: 'accountPunishments',
builder: (context, state) => const PunishmentsScreen(),
),
GoRoute(
path: '/programs',
name: 'accountProgram',
builder: (context, state) => const AccountProgramScreen(),
),
GoRoute(
path: '/contacts',
name: 'accountContactMethods',
builder: (context, state) => const AccountContactMethod(),
),
GoRoute(
path: '/events',
name: 'accountActionEvents',
builder: (context, state) => const ActionEventScreen(),
),
GoRoute(
path: '/tickets',
name: 'accountAuthTickets',
builder: (context, state) => const AccountAuthTicket(),
),
GoRoute(
path: '/badges',
name: 'accountBadges',
builder: (context, state) => const AccountBadgesScreen(),
),
GoRoute(
path: '/wallet',
name: 'accountWallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: '/keypairs',
name: 'accountKeyPairs',
builder: (context, state) => const KeyPairScreen(),
),
GoRoute(
path: '/settings',
name: 'accountSettings',
builder: (context, state) => AccountSettingsScreen(),
routes: [
GoRoute(
path: '/notify',
name: 'accountSettingsNotify',
builder: (context, state) => const AccountNotifyPrefsScreen(),
),
GoRoute(
path: '/auth',
name: 'accountSettingsSecurity',
builder: (context, state) => const AccountSecurityPrefsScreen(),
),
],
),
GoRoute(
path: '/settings/factors',
name: 'factorSettings',
builder: (context, state) => FactorSettingsScreen(),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
),
],
), ),
], ],
), ),
GoRoute( GoRoute(
path: '/accounts/:name', path: '/account',
name: 'accountProfilePage', name: 'account',
pageBuilder: (context, state) => NoTransitionPage( builder: (context, state) => const AccountScreen(),
child: UserScreen(name: state.pathParameters['name']!),
),
),
ShellRoute(
builder: (context, state, child) =>
ResponsiveScaffold(aside: const ChatScreen(), child: child),
routes: [ routes: [
GoRoute( GoRoute(
path: '/chat', path: '/punishments',
name: 'chat', name: 'accountPunishments',
builder: (context, state) => const ResponsiveScaffoldLanding( builder: (context, state) => const PunishmentsScreen(),
child: ChatScreen(), ),
), GoRoute(
path: '/programs',
name: 'accountProgram',
builder: (context, state) => const AccountProgramScreen(),
),
GoRoute(
path: '/contacts',
name: 'accountContactMethods',
builder: (context, state) => const AccountContactMethod(),
),
GoRoute(
path: '/events',
name: 'accountActionEvents',
builder: (context, state) => const ActionEventScreen(),
),
GoRoute(
path: '/tickets',
name: 'accountAuthTickets',
builder: (context, state) => const AccountAuthTicket(),
),
GoRoute(
path: '/badges',
name: 'accountBadges',
builder: (context, state) => const AccountBadgesScreen(),
),
GoRoute(
path: '/wallet',
name: 'accountWallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: '/keypairs',
name: 'accountKeyPairs',
builder: (context, state) => const KeyPairScreen(),
),
GoRoute(
path: '/settings',
name: 'accountSettings',
builder: (context, state) => AccountSettingsScreen(),
routes: [ routes: [
GoRoute( GoRoute(
path: '/:scope/:alias', path: '/notify',
name: 'chatRoom', name: 'accountSettingsNotify',
builder: (context, state) => ChatRoomScreen( builder: (context, state) => const AccountNotifyPrefsScreen(),
key: ValueKey(
'${state.pathParameters['scope']!}:${state.pathParameters['alias']!}',
),
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
extra: state.extra as ChatRoomScreenExtra?,
),
), ),
GoRoute( GoRoute(
path: '/:scope/:alias/call', path: '/auth',
name: 'chatCallRoom', name: 'accountSettingsSecurity',
builder: (context, state) => CallRoomScreen( builder: (context, state) => const AccountSecurityPrefsScreen(),
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
GoRoute(
path: '/:scope/:alias/detail',
name: 'channelDetail',
builder: (context, state) => ChannelDetailScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
GoRoute(
path: '/manage',
name: 'chatManage',
builder: (context, state) => ChatManageScreen(
editingChannelAlias: state.uri.queryParameters['editing'],
),
), ),
], ],
), ),
GoRoute(
path: '/settings/factors',
name: 'factorSettings',
builder: (context, state) => FactorSettingsScreen(),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
),
GoRoute(
path: '/profile/:name',
name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
),
),
],
),
GoRoute(
path: '/chat',
name: 'chat',
builder: (context, state) => const ChatScreen(),
routes: [
GoRoute(
path: '/:scope/:alias',
name: 'chatRoom',
builder: (context, state) => ChatRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
extra: state.extra as ChatRoomScreenExtra?,
),
),
GoRoute(
path: '/:scope/:alias/call',
name: 'chatCallRoom',
builder: (context, state) => CallRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
GoRoute(
path: '/:scope/:alias/detail',
name: 'channelDetail',
builder: (context, state) => ChannelDetailScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
GoRoute(
path: '/manage',
name: 'chatManage',
builder: (context, state) => ChatManageScreen(
editingChannelAlias: state.uri.queryParameters['editing'],
),
),
], ],
), ),
GoRoute( GoRoute(

View File

@ -110,7 +110,6 @@ class AccountScreen extends StatelessWidget {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenAccount").tr(), title: Text("screenAccount").tr(),
@ -142,6 +141,15 @@ class AccountScreen extends StatelessWidget {
], ],
) )
: null, : null,
actions: [
IconButton(
icon: const Icon(Symbols.settings, fill: 1),
onPressed: () {
GoRouter.of(context).pushNamed('settings');
},
),
const Gap(8),
],
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: ua.isAuthorized child: ua.isAuthorized

View File

@ -59,7 +59,6 @@ class _ActionEventScreenState extends State<ActionEventScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountActionEvent').tr(), title: Text('accountActionEvent').tr(),

View File

@ -91,7 +91,6 @@ class _AccountAuthTicketState extends State<AccountAuthTicket> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountAuthTickets').tr(), title: Text('accountAuthTickets').tr(),

View File

@ -70,7 +70,6 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: Text('screenAccountBadges').tr(), title: Text('screenAccountBadges').tr(),
), ),

View File

@ -69,7 +69,6 @@ class _AccountContactMethodState extends State<AccountContactMethod> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountContactMethods').tr(), title: Text('accountContactMethods').tr(),

View File

@ -16,11 +16,7 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password), 0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email), 1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer), 2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
3: ( 3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
'authFactorInAppNotify',
'authFactorInAppNotifyDescription',
Symbols.notifications_active
),
}; };
class FactorSettingsScreen extends StatefulWidget { class FactorSettingsScreen extends StatefulWidget {
@ -40,10 +36,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/factors'); final resp = await sn.client.get('/cgi/id/users/me/factors');
_factors = List<SnAuthFactor>.from( _factors = List<SnAuthFactor>.from(
resp.data resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [],
?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
); );
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@ -62,7 +55,6 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), leading: PageBackButton(),
title: Text('screenFactorSettings').tr(), title: Text('screenFactorSettings').tr(),
@ -104,8 +96,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
return ListTile( return ListTile(
title: Text(kFactorTypes[ele.type]!.$1).tr(), title: Text(kFactorTypes[ele.type]!.$1).tr(),
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(), subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
contentPadding: contentPadding: const EdgeInsets.only(left: 24, right: 12),
const EdgeInsets.only(left: 24, right: 12),
leading: Icon(kFactorTypes[ele.type]!.$3), leading: Icon(kFactorTypes[ele.type]!.$3),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Symbols.close), icon: const Icon(Symbols.close),
@ -114,17 +105,14 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
context context
.showConfirmDialog( .showConfirmDialog(
'authFactorDelete'.tr(), 'authFactorDelete'.tr(),
'authFactorDeleteDescription'.tr( 'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
args: [kFactorTypes[ele.type]!.$1.tr()]),
) )
.then((val) async { .then((val) async {
if (!val) return; if (!val) return;
try { try {
if (!context.mounted) return; if (!context.mounted) return;
final sn = final sn = context.read<SnNetworkProvider>();
context.read<SnNetworkProvider>(); await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
await sn.client.delete(
'/cgi/id/users/me/factors/${ele.id}');
_fetchFactors(); _fetchFactors();
} catch (err) { } catch (err) {
if (!context.mounted) return; if (!context.mounted) return;
@ -203,9 +191,7 @@ class _FactorNewDialogState extends State<_FactorNewDialog> {
value: _factorType, value: _factorType,
items: kFactorTypes.entries.map( items: kFactorTypes.entries.map(
(ele) { (ele) {
final contains = widget.currentlyHave final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
.map((ele) => ele.type)
.contains(ele.key);
return DropdownMenuItem<int>( return DropdownMenuItem<int>(
enabled: !contains, enabled: !contains,
value: ele.key, value: ele.key,

View File

@ -37,7 +37,6 @@ class _KeyPairScreenState extends State<KeyPairScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: Text('screenKeyPairs').tr(), title: Text('screenKeyPairs').tr(),
), ),

View File

@ -75,7 +75,6 @@ class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountSettingsNotify').tr(), title: Text('accountSettingsNotify').tr(),

View File

@ -70,7 +70,6 @@ class _AccountSecurityPrefsScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountSettingsSecurity').tr(), title: Text('accountSettingsSecurity').tr(),

View File

@ -66,40 +66,37 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
_locationController.text = prof.profile!.location; _locationController.text = prof.profile!.location;
_avatar = prof.avatar; _avatar = prof.avatar;
_banner = prof.banner; _banner = prof.banner;
_links = _links = prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList();
prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList();
_birthday = prof.profile!.birthday?.toLocal(); _birthday = prof.profile!.birthday?.toLocal();
if (_birthday != null) { if (_birthday != null) {
_birthdayController.text = _birthdayController.text = DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
} }
} }
void _selectBirthday() async { void _selectBirthday() async {
await showCupertinoModalPopup<DateTime?>( await showCupertinoModalPopup<DateTime?>(
context: context, context: context,
builder: (BuildContext context) => Container( builder:
height: 216, (BuildContext context) => Container(
padding: const EdgeInsets.only(top: 6.0), height: 216,
margin: padding: const EdgeInsets.only(top: 6.0),
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: SafeArea( child: SafeArea(
top: false, top: false,
child: CupertinoDatePicker( child: CupertinoDatePicker(
initialDateTime: _birthday?.toLocal(), initialDateTime: _birthday?.toLocal(),
mode: CupertinoDatePickerMode.date, mode: CupertinoDatePickerMode.date,
use24hFormat: true, use24hFormat: true,
onDateTimeChanged: (DateTime newDate) { onDateTimeChanged: (DateTime newDate) {
setState(() { setState(() {
_birthday = newDate; _birthday = newDate;
_birthdayController.text = _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
DateFormat(_kDateFormat).format(_birthday!); });
}); },
}, ),
),
), ),
),
),
); );
} }
@ -112,32 +109,29 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
Uint8List? rawBytes; Uint8List? rawBytes;
if (!skipCrop) { if (!skipCrop) {
final ImageProvider imageProvider = final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); final aspectRatios =
final aspectRatios = place == 'banner' place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
? [CropAspectRatio(width: 16, height: 7)] final result =
: [CropAspectRatio(width: 1, height: 1)]; (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) ? await showCupertinoImageCropper(
? await showCupertinoImageCropper( // ignore: use_build_context_synchronously
// ignore: use_build_context_synchronously context,
context, allowedAspectRatios: aspectRatios,
allowedAspectRatios: aspectRatios, imageProvider: imageProvider,
imageProvider: imageProvider, )
) : await showMaterialImageCropper(
: await showMaterialImageCropper( // ignore: use_build_context_synchronously
// ignore: use_build_context_synchronously context,
context, allowedAspectRatios: aspectRatios,
allowedAspectRatios: aspectRatios, imageProvider: imageProvider,
imageProvider: imageProvider, );
);
if (result == null) return; if (result == null) return;
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))! rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
.buffer
.asUint8List();
} else { } else {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -158,8 +152,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
if (!mounted) return; if (!mounted) return;
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client await sn.client.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid});
.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid});
if (!mounted) return; if (!mounted) return;
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
@ -195,9 +188,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
'location': _locationController.value.text, 'location': _locationController.value.text,
'birthday': _birthday?.toUtc().toIso8601String(), 'birthday': _birthday?.toUtc().toIso8601String(),
'links': { 'links': {
for (final link in _links! for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2,
.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty))
link.$1: link.$2,
}, },
}, },
); );
@ -244,10 +235,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true, appBar: AppBar(leading: const PageBackButton(), title: Text('screenAccountProfileEdit').tr()),
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountProfileEdit').tr()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -265,14 +253,11 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: Container( child: Container(
color: Theme.of(context) color: Theme.of(context).colorScheme.surfaceContainerHigh,
.colorScheme child:
.surfaceContainerHigh, _banner != null
child: _banner != null ? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover)
? AutoResizeUniversalImage( : const SizedBox.shrink(),
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover)
: const SizedBox.shrink(),
), ),
), ),
), ),
@ -309,16 +294,12 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
labelText: 'fieldUsername'.tr(), labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(), helperText: 'fieldUsernameCannotEditHint'.tr(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextField( TextField(
controller: _nicknameController, controller: _nicknameController,
decoration: InputDecoration( decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr()),
border: const UnderlineInputBorder(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
labelText: 'fieldNickname'.tr()),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
Row( Row(
children: [ children: [
@ -330,8 +311,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldFirstName'.tr(), labelText: 'fieldFirstName'.tr(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(8), const Gap(8),
@ -343,8 +323,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldLastName'.tr(), labelText: 'fieldLastName'.tr(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
], ],
@ -359,8 +338,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldGender'.tr(), labelText: 'fieldGender'.tr(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(4), const Gap(4),
@ -372,8 +350,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldPronouns'.tr(), labelText: 'fieldPronouns'.tr(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
], ],
@ -383,11 +360,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
maxLines: null, maxLines: null,
minLines: 3, minLines: 3,
decoration: InputDecoration( decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldDescription'.tr()),
border: const UnderlineInputBorder(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
labelText: 'fieldDescription'.tr()),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -399,21 +373,18 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldTimeZone'.tr(), labelText: 'fieldTimeZone'.tr(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(4), const Gap(4),
StyledWidget( StyledWidget(
IconButton( IconButton(
icon: const Icon(Symbols.calendar_month), icon: const Icon(Symbols.calendar_month),
visualDensity: visualDensity: VisualDensity(horizontal: -4, vertical: -4),
VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () async { onPressed: () async {
_timezoneController.text = _timezoneController.text = await FlutterTimezone.getLocalTimezone();
await FlutterTimezone.getLocalTimezone();
}, },
), ),
).padding(top: 6), ).padding(top: 6),
@ -421,8 +392,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
StyledWidget( StyledWidget(
IconButton( IconButton(
icon: const Icon(Symbols.clear), icon: const Icon(Symbols.clear),
visualDensity: visualDensity: VisualDensity(horizontal: -4, vertical: -4),
VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () { onPressed: () {
@ -434,18 +404,13 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
), ),
TextField( TextField(
controller: _locationController, controller: _locationController,
decoration: InputDecoration( decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldLocation'.tr()),
border: const UnderlineInputBorder(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
labelText: 'fieldLocation'.tr()),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextField( TextField(
controller: _birthdayController, controller: _birthdayController,
readOnly: true, readOnly: true,
decoration: InputDecoration( decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr()),
border: const UnderlineInputBorder(),
labelText: 'fieldBirthday'.tr()),
onTap: () => _selectBirthday(), onTap: () => _selectBirthday(),
), ),
if (_links != null) if (_links != null)
@ -453,8 +418,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
margin: const EdgeInsets.only(top: 16, bottom: 4), margin: const EdgeInsets.only(top: 16, bottom: 4),
child: Container( child: Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
horizontal: 16, vertical: 8),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -463,17 +427,13 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
Expanded( Expanded(
child: Text( child: Text(
'fieldLinks'.tr(), 'fieldLinks'.tr(),
style: Theme.of(context) style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 17),
.textTheme
.titleMedium!
.copyWith(fontSize: 17),
), ),
), ),
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
visualDensity: visualDensity: VisualDensity(horizontal: -4, vertical: -4),
VisualDensity(horizontal: -4, vertical: -4),
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),
onPressed: () { onPressed: () {
setState(() => _links!.add(('', ''))); setState(() => _links!.add(('', '')));
@ -497,9 +457,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onChanged: (value) { onChanged: (value) {
_links![idx] = (value, _links![idx].$2); _links![idx] = (value, _links![idx].$2);
}, },
onTapOutside: (_) => FocusManager onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
.instance.primaryFocus
?.unfocus(),
), ),
), ),
const Gap(8), const Gap(8),
@ -515,9 +473,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onChanged: (value) { onChanged: (value) {
_links![idx] = (_links![idx].$1, value); _links![idx] = (_links![idx].$1, value);
}, },
onTapOutside: (_) => FocusManager onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
.instance.primaryFocus
?.unfocus(),
), ),
), ),
], ],

View File

@ -70,7 +70,6 @@ class _AccountProgramScreenState extends State<AccountProgramScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: Text('accountProgram').tr(), title: Text('accountProgram').tr(),
), ),

View File

@ -27,12 +27,10 @@ class AccountPublisherEditScreen extends StatefulWidget {
const AccountPublisherEditScreen({super.key, required this.name}); const AccountPublisherEditScreen({super.key, required this.name});
@override @override
State<AccountPublisherEditScreen> createState() => State<AccountPublisherEditScreen> createState() => _AccountPublisherEditScreenState();
_AccountPublisherEditScreenState();
} }
class _AccountPublisherEditScreenState class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> {
extends State<AccountPublisherEditScreen> {
bool _isBusy = false; bool _isBusy = false;
SnPublisher? _publisher; SnPublisher? _publisher;
@ -117,32 +115,29 @@ class _AccountPublisherEditScreenState
Uint8List? rawBytes; Uint8List? rawBytes;
if (!skipCrop) { if (!skipCrop) {
final ImageProvider imageProvider = final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); final aspectRatios =
final aspectRatios = place == 'banner' place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
? [CropAspectRatio(width: 16, height: 7)] final result =
: [CropAspectRatio(width: 1, height: 1)]; (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) ? await showCupertinoImageCropper(
? await showCupertinoImageCropper( // ignore: use_build_context_synchronously
// ignore: use_build_context_synchronously context,
context, allowedAspectRatios: aspectRatios,
allowedAspectRatios: aspectRatios, imageProvider: imageProvider,
imageProvider: imageProvider, )
) : await showMaterialImageCropper(
: await showMaterialImageCropper( // ignore: use_build_context_synchronously
// ignore: use_build_context_synchronously context,
context, allowedAspectRatios: aspectRatios,
allowedAspectRatios: aspectRatios, imageProvider: imageProvider,
imageProvider: imageProvider, );
);
if (result == null) return; if (result == null) return;
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))! rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
.buffer
.asUint8List();
} else { } else {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -196,10 +191,7 @@ class _AccountPublisherEditScreenState
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true, appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountPublisherEdit').tr()),
appBar: AppBar(
leading: PageBackButton(),
title: Text('screenAccountPublisherEdit').tr()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
@ -216,14 +208,11 @@ class _AccountPublisherEditScreenState
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: Container( child: Container(
color: Theme.of(context) color: Theme.of(context).colorScheme.surfaceContainerHigh,
.colorScheme child:
.surfaceContainerHigh, _banner != null
child: _banner != null ? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover)
? AutoResizeUniversalImage( : const SizedBox.shrink(),
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover)
: const SizedBox.shrink(),
), ),
), ),
), ),
@ -256,15 +245,13 @@ class _AccountPublisherEditScreenState
labelText: 'fieldUsername'.tr(), labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(), helperText: 'fieldUsernameCannotEditHint'.tr(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
controller: _nickController, controller: _nickController,
decoration: InputDecoration(labelText: 'fieldNickname'.tr()), decoration: InputDecoration(labelText: 'fieldNickname'.tr()),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
@ -272,8 +259,7 @@ class _AccountPublisherEditScreenState
maxLines: null, maxLines: null,
minLines: 3, minLines: 3,
decoration: InputDecoration(labelText: 'fieldDescription'.tr()), decoration: InputDecoration(labelText: 'fieldDescription'.tr()),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(12),
Row( Row(

View File

@ -25,8 +25,7 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('screenAccountPublisherNew').tr(), title: Text('screenAccountPublisherNew').tr(),

View File

@ -33,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
try { try {
final resp = await sn.client.get('/cgi/co/publishers/me'); final resp = await sn.client.get('/cgi/co/publishers/me');
final List<SnPublisher> out = List<SnPublisher>.from( final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
if (!mounted) return; if (!mounted) return;
@ -82,7 +81,6 @@ class _PublisherScreenState extends State<PublisherScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('screenAccountPublishers').tr(), title: Text('screenAccountPublishers').tr(),
@ -95,9 +93,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.add_circle), leading: const Icon(Symbols.add_circle),
onTap: () { onTap: () {
GoRouter.of(context) GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
.pushNamed('accountPublisherNew')
.then((value) {
if (value == true) { if (value == true) {
_publishers.clear(); _publishers.clear();
_fetchPublishers(); _fetchPublishers();
@ -123,8 +119,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
return ListTile( return ListTile(
title: Text(publisher.nick), title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'), subtitle: Text('@${publisher.name}'),
contentPadding: contentPadding: const EdgeInsets.symmetric(horizontal: 16),
const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(content: publisher.avatar), leading: AccountImage(content: publisher.avatar),
trailing: PopupMenuButton( trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [ itemBuilder: (BuildContext context) => [

View File

@ -55,7 +55,6 @@ class _PunishmentsScreenState extends State<PunishmentsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: Text('accountPunishments').tr(), title: Text('accountPunishments').tr(),
leading: PageBackButton(), leading: PageBackButton(),

View File

@ -37,7 +37,6 @@ class AccountSettingsScreen extends StatelessWidget {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), leading: PageBackButton(),
title: Text('screenAccountSettings').tr(), title: Text('screenAccountSettings').tr(),

View File

@ -6,16 +6,19 @@ import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/chat/room.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/account/account_select.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -127,6 +130,8 @@ class _ChatScreenState extends State<ChatScreen> {
} }
} }
SnChannel? _focusChannel;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -135,8 +140,13 @@ class _ChatScreenState extends State<ChatScreen> {
} }
void _onTapChannel(SnChannel channel) { void _onTapChannel(SnChannel channel) {
setState(() => _unreadCounts?[channel.id] = 0); final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
GoRouter.of(context).pushReplacementNamed(
if (doExpand) {
setState(() => _focusChannel = channel);
return;
}
GoRouter.of(context).pushNamed(
'chatRoom', 'chatRoom',
pathParameters: { pathParameters: {
'scope': channel.realm?.alias ?? 'global', 'scope': channel.realm?.alias ?? 'global',
@ -144,6 +154,7 @@ class _ChatScreenState extends State<ChatScreen> {
}, },
).then((value) { ).then((value) {
if (mounted) { if (mounted) {
_unreadCounts?[channel.id] = 0;
setState(() => _unreadCounts?[channel.id] = 0); setState(() => _unreadCounts?[channel.id] = 0);
_refreshChannels(noRemote: true); _refreshChannels(noRemote: true);
} }
@ -166,8 +177,10 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
return AppScaffold( final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
noBackground: true,
final chatList = AppScaffold(
noBackground: doExpand,
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@ -255,6 +268,11 @@ class _ChatScreenState extends State<ChatScreen> {
lastMessage: lastMessage, lastMessage: lastMessage,
unreadCount: _unreadCounts?[channel.id], unreadCount: _unreadCounts?[channel.id],
onTap: () { onTap: () {
if (doExpand) {
_unreadCounts?[channel.id] = 0;
setState(() => _focusChannel = channel);
return;
}
_onTapChannel(channel); _onTapChannel(channel);
}, },
); );
@ -266,6 +284,28 @@ class _ChatScreenState extends State<ChatScreen> {
], ],
), ),
); );
if (doExpand) {
return AppBackground(
isRoot: true,
child: Row(
children: [
SizedBox(width: 340, child: chatList),
const VerticalDivider(width: 1),
if (_focusChannel != null)
Expanded(
child: ChatRoomScreen(
key: ValueKey(_focusChannel!.id),
scope: _focusChannel!.realm?.alias ?? 'global',
alias: _focusChannel!.alias,
),
),
],
),
);
}
return chatList;
} }
} }

View File

@ -37,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
return Stack( return Stack(
children: [ children: [
Container( Container(
color: color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
child: call.focusTrack != null child: call.focusTrack != null
? InteractiveParticipantWidget( ? InteractiveParticipantWidget(
isFixedAvatar: false, isFixedAvatar: false,
@ -73,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
color: Theme.of(context).cardColor, color: Theme.of(context).cardColor,
participant: track, participant: track,
onTap: () { onTap: () {
if (track.participant.sid != if (track.participant.sid != call.focusTrack?.participant.sid) {
call.focusTrack?.participant.sid) {
call.setFocusTrack(track); call.setFocusTrack(track);
} }
}, },
@ -116,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget( child: InteractiveParticipantWidget(
color: Theme.of(context) color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
.colorScheme
.surfaceContainerHigh
.withOpacity(0.75),
participant: track, participant: track,
onTap: () { onTap: () {
if (track.participant.sid != if (track.participant.sid != call.focusTrack?.participant.sid) {
call.focusTrack?.participant.sid) {
call.setFocusTrack(track); call.setFocusTrack(track);
} }
}, },
@ -155,7 +149,6 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
listenable: call, listenable: call,
builder: (context, _) { builder: (context, _) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: RichText( title: RichText(
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -190,8 +183,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
Builder(builder: (context) { Builder(builder: (context) {
final call = context.read<ChatCallProvider>(); final call = context.read<ChatCallProvider>();
final connectionQuality = final connectionQuality =
call.room.localParticipant?.connectionQuality ?? call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
livekit.ConnectionQuality.unknown;
return Expanded( return Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -213,35 +205,24 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
children: [ children: [
Text( Text(
{ {
livekit.ConnectionState.disconnected: livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(),
'callStatusDisconnected'.tr(), livekit.ConnectionState.connected: 'callStatusConnected'.tr(),
livekit.ConnectionState.connected: livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(),
'callStatusConnected'.tr(), livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(),
livekit.ConnectionState.connecting:
'callStatusConnecting'.tr(),
livekit.ConnectionState.reconnecting:
'callStatusReconnecting'.tr(),
}[call.room.connectionState]!, }[call.room.connectionState]!,
), ),
const Gap(6), const Gap(6),
if (connectionQuality != if (connectionQuality != livekit.ConnectionQuality.unknown)
livekit.ConnectionQuality.unknown)
Icon( Icon(
{ {
livekit.ConnectionQuality.excellent: livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt,
Icons.signal_cellular_alt, livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
livekit.ConnectionQuality.good: livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
Icons.signal_cellular_alt_2_bar,
livekit.ConnectionQuality.poor:
Icons.signal_cellular_alt_1_bar,
}[connectionQuality], }[connectionQuality],
color: { color: {
livekit.ConnectionQuality.excellent: livekit.ConnectionQuality.excellent: Colors.green,
Colors.green, livekit.ConnectionQuality.good: Colors.orange,
livekit.ConnectionQuality.good: livekit.ConnectionQuality.poor: Colors.red,
Colors.orange,
livekit.ConnectionQuality.poor:
Colors.red,
}[connectionQuality], }[connectionQuality],
size: 16, size: 16,
) )
@ -263,9 +244,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
Row( Row(
children: [ children: [
IconButton( IconButton(
icon: _layoutMode == 0 icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
? const Icon(Icons.view_list)
: const Icon(Icons.grid_view),
onPressed: () { onPressed: () {
_switchLayout(); _switchLayout();
}, },

View File

@ -220,7 +220,6 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id; final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
), ),

View File

@ -49,8 +49,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [], resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
); );
if (_editingChannel != null) { if (_editingChannel != null) {
_belongToRealm = _belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
_realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
} }
} catch (err) { } catch (err) {
if (mounted) context.showErrorDialog(err); if (mounted) context.showErrorDialog(err);
@ -98,8 +97,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
'is_community': _isCommunity, 'is_community': _isCommunity,
if (_editingChannel != null && _belongToRealm == null) if (_editingChannel != null && _belongToRealm == null)
'new_belongs_realm': 'global' 'new_belongs_realm': 'global'
else if (_editingChannel != null && else if (_editingChannel != null && _belongToRealm?.id != _editingChannel?.realm?.id)
_belongToRealm?.id != _editingChannel?.realm?.id)
'new_belongs_realm': _belongToRealm!.alias, 'new_belongs_realm': _belongToRealm!.alias,
}; };
@ -141,11 +139,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: widget.editingChannelAlias != null title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
? Text('screenChatManage').tr()
: Text('screenChatNew').tr(),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
@ -157,8 +152,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
leadingPadding: const EdgeInsets.only(left: 10, right: 20), leadingPadding: const EdgeInsets.only(left: 10, right: 20),
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
content: Text( content: Text(
'channelEditingNotice' 'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
.tr(args: ['#${_editingChannel!.alias}']),
), ),
actions: [ actions: [
TextButton( TextButton(
@ -198,15 +192,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(item.name).textStyle(Theme.of(context) Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
.textTheme
.bodyMedium!),
Text( Text(
item.description, item.description,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).textStyle( ).textStyle(Theme.of(context).textTheme.bodySmall!),
Theme.of(context).textTheme.bodySmall!),
], ],
), ),
), ),
@ -222,8 +213,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
CircleAvatar( CircleAvatar(
radius: 16, radius: 16,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
foregroundColor: foregroundColor: Theme.of(context).colorScheme.onSurface,
Theme.of(context).colorScheme.onSurface,
child: const Icon(Symbols.clear), child: const Icon(Symbols.clear),
), ),
const Gap(12), const Gap(12),
@ -232,9 +222,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('fieldChatBelongToRealmUnset') Text('fieldChatBelongToRealmUnset').tr().textStyle(
.tr()
.textStyle(
Theme.of(context).textTheme.bodyMedium!, Theme.of(context).textTheme.bodyMedium!,
), ),
], ],
@ -269,8 +257,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
helperText: 'fieldChatAliasHint'.tr(), helperText: 'fieldChatAliasHint'.tr(),
helperMaxLines: 2, helperMaxLines: 2,
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
@ -279,8 +266,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldChatName'.tr(), labelText: 'fieldChatName'.tr(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
@ -291,8 +277,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldChatDescription'.tr(), labelText: 'fieldChatDescription'.tr(),
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(12),
CheckboxListTile( CheckboxListTile(

View File

@ -304,7 +304,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
_channel?.type == 1 _channel?.type == 1

View File

@ -157,7 +157,6 @@ class _ExploreScreenState extends State<ExploreScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.watch<ConfigProvider>(); final cfg = context.watch<ConfigProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
key: _fabKey, key: _fabKey,
@ -244,7 +243,6 @@ class _ExploreScreenState extends State<ExploreScreen>
GoRouter.of(context).pushNamed('postShuffle'); GoRouter.of(context).pushNamed('postShuffle');
}, },
), ),
const Gap(48),
Expanded( Expanded(
child: Center( child: Center(
child: IconButton( child: IconButton(
@ -536,7 +534,6 @@ class _PostListWidgetState extends State<_PostListWidget> {
switch (ele.type) { switch (ele.type) {
case 'interactive.post': case 'interactive.post':
return OpenablePostItem( return OpenablePostItem(
useReplace: true,
data: SnPost.fromJson(ele.data), data: SnPost.fromJson(ele.data),
maxWidth: 640, maxWidth: 640,
onChanged: (data) { onChanged: (data) {

View File

@ -12,6 +12,7 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_comment_list.dart'; import 'package:surface/widgets/post/post_comment_list.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
@ -65,111 +66,115 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
final double maxWidth = _data?.type == 'video' ? double.infinity : 640; final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
return AppScaffold( return AppBackground(
noBackground: true, isRoot: widget.onBack != null,
appBar: AppBar( child: AppScaffold(
leading: BackButton( appBar: AppBar(
onPressed: () { leading: BackButton(
if (widget.onBack != null) { onPressed: () {
widget.onBack!.call(); if (widget.onBack != null) {
} widget.onBack!.call();
if (GoRouter.of(context).canPop()) { }
GoRouter.of(context).pop(context); if (GoRouter.of(context).canPop()) {
return; GoRouter.of(context).pop(context);
} return;
GoRouter.of(context).replaceNamed('explore'); }
}, GoRouter.of(context).replaceNamed('explore');
), },
title: _data?.body['title'] != null
? RichText(
textAlign: TextAlign.center,
text: TextSpan(children: [
TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: 'postDetail'.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: Text('postDetail').tr(),
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: LoadingIndicator(isActive: _isBusy),
), ),
if (_data != null) title: _data?.body['title'] != null
? RichText(
textAlign: TextAlign.center,
text: TextSpan(children: [
TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color:
Theme.of(context).appBarTheme.foregroundColor!,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: 'postDetail'.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color:
Theme.of(context).appBarTheme.foregroundColor!,
),
),
]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: Text('postDetail').tr(),
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: PostItem( child: LoadingIndicator(isActive: _isBusy),
data: _data!, ),
maxWidth: maxWidth, if (_data != null)
showComments: false, SliverToBoxAdapter(
showFullPost: true, child: PostItem(
onChanged: (data) { data: _data!,
setState(() => _data = data); maxWidth: maxWidth,
}, showComments: false,
onDeleted: () { showFullPost: true,
Navigator.pop(context); onChanged: (data) {
}, setState(() => _data = data);
},
onDeleted: () {
Navigator.pop(context);
},
),
), ),
), if (_data != null)
if (_data != null) SliverToBoxAdapter(
SliverToBoxAdapter( child: Divider(height: 1).padding(top: 8),
child: Divider(height: 1).padding(top: 8),
),
if (_data != null)
SliverToBoxAdapter(
child: Container(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.comment, size: 24),
const Gap(16),
Text('postCommentsDetailed')
.plural(_data!.metric.replyCount)
.textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, vertical: 12).center(),
), ),
), if (_data != null)
if (_data != null && ua.isAuthorized) SliverToBoxAdapter(
SliverToBoxAdapter( child: Container(
child: PostCommentQuickAction( constraints: BoxConstraints(maxWidth: maxWidth),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.comment, size: 24),
const Gap(16),
Text('postCommentsDetailed')
.plural(_data!.metric.replyCount)
.textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, vertical: 12).center(),
),
),
if (_data != null && ua.isAuthorized)
SliverToBoxAdapter(
child: PostCommentQuickAction(
parentPost: _data!,
maxWidth: maxWidth,
onPosted: () {
setState(() {
_data = _data!.copyWith(
metric: _data!.metric.copyWith(
replyCount: _data!.metric.replyCount + 1,
),
);
});
_childListKey.currentState!.refresh();
},
),
),
if (_data != null) SliverGap(8),
if (_data != null)
PostCommentSliverList(
key: _childListKey,
parentPost: _data!, parentPost: _data!,
maxWidth: maxWidth, maxWidth: maxWidth,
onPosted: () {
setState(() {
_data = _data!.copyWith(
metric: _data!.metric.copyWith(
replyCount: _data!.metric.replyCount + 1,
),
);
});
_childListKey.currentState!.refresh();
},
), ),
), if (_data != null)
if (_data != null) SliverGap(8), SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
if (_data != null) ],
PostCommentSliverList( ),
key: _childListKey,
parentPost: _data!,
maxWidth: maxWidth,
),
if (_data != null)
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
],
), ),
); );
} }

View File

@ -286,7 +286,6 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
body: NestedScrollView( body: NestedScrollView(
controller: _scrollController, controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {

View File

@ -45,9 +45,7 @@ class _WalletScreenState extends State<WalletScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true, appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
appBar: AppBar(
leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
body: Column( body: Column(
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
@ -68,9 +66,7 @@ class _WalletScreenState extends State<WalletScreen> {
SizedBox(width: double.infinity), SizedBox(width: double.infinity),
Text( Text(
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: EasyLocalization.of(context)! locale: EasyLocalization.of(context)!.currentLocale.toString(),
.currentLocale
.toString(),
symbol: '${'walletCurrencyShort'.tr()} ', symbol: '${'walletCurrencyShort'.tr()} ',
decimalDigits: 2, decimalDigits: 2,
).format(double.parse(_wallet!.balance)), ).format(double.parse(_wallet!.balance)),
@ -80,21 +76,17 @@ class _WalletScreenState extends State<WalletScreen> {
const Gap(16), const Gap(16),
Text( Text(
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: EasyLocalization.of(context)! locale: EasyLocalization.of(context)!.currentLocale.toString(),
.currentLocale
.toString(),
symbol: '${'walletCurrencyGoldenShort'.tr()} ', symbol: '${'walletCurrencyGoldenShort'.tr()} ',
decimalDigits: 2, decimalDigits: 2,
).format(double.parse(_wallet!.goldenBalance)), ).format(double.parse(_wallet!.goldenBalance)),
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
Text('walletCurrencyGolden' Text('walletCurrencyGolden'.plural(double.parse(_wallet!.goldenBalance))),
.plural(double.parse(_wallet!.goldenBalance))),
], ],
).padding(horizontal: 20, vertical: 24), ).padding(horizontal: 20, vertical: 24),
).padding(horizontal: 8, top: 16, bottom: 4), ).padding(horizontal: 8, top: 16, bottom: 4),
if (_wallet != null) if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)),
Expanded(child: _WalletTransactionList(myself: _wallet!)),
], ],
), ),
); );
@ -124,10 +116,7 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
queryParameters: {'take': 10, 'offset': _transactions.length}, queryParameters: {'take': 10, 'offset': _transactions.length},
); );
_totalCount = resp.data['count']; _totalCount = resp.data['count'];
_transactions.addAll(resp.data['data'] _transactions.addAll(resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? []);
?.map((e) => SnTransaction.fromJson(e))
.cast<SnTransaction>() ??
[]);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -152,8 +141,7 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
child: InfiniteList( child: InfiniteList(
itemCount: _transactions.length, itemCount: _transactions.length,
isLoading: _isBusy, isLoading: _isBusy,
hasReachedMax: hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!,
_totalCount != null && _transactions.length >= _totalCount!,
onFetchData: () { onFetchData: () {
_fetchTransactions(); _fetchTransactions();
}, },
@ -161,9 +149,7 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
final ele = _transactions[idx]; final ele = _transactions[idx];
final isIncoming = ele.payeeId == widget.myself.id; final isIncoming = ele.payeeId == widget.myself.id;
return ListTile( return ListTile(
leading: isIncoming leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made),
? const Icon(Symbols.call_received)
: const Icon(Symbols.call_made),
title: Text( title: Text(
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}', '${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
style: TextStyle(color: isIncoming ? Colors.green : Colors.red), style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
@ -176,20 +162,12 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
Row( Row(
children: [ children: [
Text( Text(
'walletTransactionType${ele.currency.capitalize()}' 'walletTransactionType${ele.currency.capitalize()}'.tr(),
.tr(),
style: Theme.of(context).textTheme.labelSmall, style: Theme.of(context).textTheme.labelSmall,
), ),
Text(' · ') Text(' · ').textStyle(Theme.of(context).textTheme.labelSmall!).padding(right: 4),
.textStyle(Theme.of(context).textTheme.labelSmall!)
.padding(right: 4),
Text( Text(
DateFormat( DateFormat(null, EasyLocalization.of(context)!.currentLocale.toString()).format(ele.createdAt),
null,
EasyLocalization.of(context)!
.currentLocale
.toString())
.format(ele.createdAt),
style: Theme.of(context).textTheme.labelSmall, style: Theme.of(context).textTheme.labelSmall,
), ),
], ],
@ -221,34 +199,33 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
final TextEditingController passwordController = TextEditingController(); final TextEditingController passwordController = TextEditingController();
final password = await showDialog<String?>( final password = await showDialog<String?>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder:
title: Text('walletCreate').tr(), (ctx) => AlertDialog(
content: Column( title: Text('walletCreate').tr(),
crossAxisAlignment: CrossAxisAlignment.start, content: Column(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: [ mainAxisSize: MainAxisSize.min,
Text('walletCreatePassword').tr(), children: [
const Gap(8), Text('walletCreatePassword').tr(),
TextField( const Gap(8),
autofocus: true, TextField(
obscureText: true, autofocus: true,
controller: passwordController, obscureText: true,
decoration: InputDecoration(labelText: 'fieldPassword'.tr()), controller: passwordController,
decoration: InputDecoration(labelText: 'fieldPassword'.tr()),
),
],
), ),
], actions: [
), TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text('cancel').tr()),
actions: [ TextButton(
TextButton( onPressed: () {
onPressed: () => Navigator.of(ctx).pop(), Navigator.of(ctx).pop(passwordController.text);
child: Text('cancel').tr()), },
TextButton( child: Text('next').tr(),
onPressed: () { ),
Navigator.of(ctx).pop(passwordController.text); ],
},
child: Text('next').tr(),
), ),
],
),
); );
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
passwordController.dispose(); passwordController.dispose();
@ -280,18 +257,12 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
children: [ children: [
CircleAvatar(radius: 28, child: Icon(Symbols.add, size: 28)), CircleAvatar(radius: 28, child: Icon(Symbols.add, size: 28)),
const Gap(12), const Gap(12),
Text('walletCreate', Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
style: Theme.of(context).textTheme.titleLarge) Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
.tr(),
Text('walletCreateSubtitle',
style: Theme.of(context).textTheme.bodyMedium)
.tr(),
const Gap(8), const Gap(8),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton( child: TextButton(onPressed: _isBusy ? null : () => _createWallet(), child: Text('next').tr()),
onPressed: _isBusy ? null : () => _createWallet(),
child: Text('next').tr()),
), ),
], ],
).padding(horizontal: 20, vertical: 24), ).padding(horizontal: 20, vertical: 24),

View File

@ -1,12 +1,10 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/account/account_image.dart';
class AppRailNavigation extends StatefulWidget { class AppRailNavigation extends StatefulWidget {
const AppRailNavigation({super.key}); const AppRailNavigation({super.key});
@ -20,59 +18,43 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
.read<NavigationProvider>()
.autoDetectIndex(GoRouter.maybeOf(context));
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>();
final nav = context.watch<NavigationProvider>(); final nav = context.watch<NavigationProvider>();
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
builder: (context, _) { builder: (context, _) {
final destinations = nav.destinations.toList(); final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
return SizedBox( return SizedBox(
width: 80, width: 80,
child: NavigationRail( child: NavigationRail(
labelType: NavigationRailLabelType.selected, selectedIndex:
backgroundColor: Theme.of(context) nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
.colorScheme
.surfaceContainerLow
.withOpacity(0.5),
selectedIndex: nav.currentIndex != null &&
nav.currentIndex! < nav.destinations.length
? nav.currentIndex
: null,
destinations: [ destinations: [
...destinations.map((ele) { ...destinations.where((ele) => ele.isPinned).map((ele) {
return NavigationRailDestination( return NavigationRailDestination(
icon: ele.icon, icon: ele.icon,
label: Text(ele.label).tr(), label: Text(ele.label).tr(),
); );
}), }),
], ],
leading: const Gap(4),
trailing: Expanded( trailing: Expanded(
child: Align( child: Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Padding( child: StyledWidget(
padding: EdgeInsets.only(bottom: 24), IconButton(
child: GestureDetector( icon: const Icon(Symbols.menu),
child: AccountImage( onPressed: () {
content: ua.user?.avatar, Scaffold.of(context).openDrawer();
fallbackWidget:
ua.isAuthorized ? null : const Icon(Symbols.login),
),
onTap: () {
GoRouter.of(context).goNamed('account');
}, },
), ),
), ).padding(bottom: 16),
), ),
), ),
onDestinationSelected: (idx) { onDestinationSelected: (idx) {

View File

@ -13,6 +13,7 @@ import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/connection_indicator.dart'; import 'package:surface/widgets/connection_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
import 'package:surface/widgets/navigation/app_rail_navigation.dart'; import 'package:surface/widgets/navigation/app_rail_navigation.dart';
import 'package:surface/widgets/notify_indicator.dart'; import 'package:surface/widgets/notify_indicator.dart';
@ -65,9 +66,7 @@ class AppScaffold extends StatelessWidget {
return Scaffold( return Scaffold(
extendBody: true, extendBody: true,
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
backgroundColor: noBackground backgroundColor: Theme.of(context).scaffoldBackgroundColor,
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
body: SizedBox.expand( body: SizedBox.expand(
child: noBackground child: noBackground
? content ? content
@ -112,6 +111,7 @@ class AppRootScaffold extends StatelessWidget {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final isCollapseDrawer = cfg.drawerIsCollapsed; final isCollapseDrawer = cfg.drawerIsCollapsed;
final isExpandedDrawer = cfg.drawerIsExpanded;
final routeName = GoRouter.of(context) final routeName = GoRouter.of(context)
.routerDelegate .routerDelegate
@ -132,7 +132,19 @@ class AppRootScaffold extends StatelessWidget {
? body ? body
: Row( : Row(
children: [ children: [
AppRailNavigation(), Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
),
child: isExpandedDrawer
? AppNavigationDrawer(elevation: 0)
: AppRailNavigation(),
),
Expanded(child: body), Expanded(child: body),
], ],
); );
@ -220,71 +232,10 @@ class AppRootScaffold extends StatelessWidget {
), ),
], ],
), ),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null, drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: bottomNavigationBar:
isShowBottomNavigation ? AppBottomNavigationBar() : null, isShowBottomNavigation ? AppBottomNavigationBar() : null,
); );
} }
} }
class ResponsiveScaffold extends StatelessWidget {
final Widget aside;
final Widget? child;
final int asideFlex;
final int contentFlex;
const ResponsiveScaffold({
super.key,
required this.aside,
required this.child,
this.asideFlex = 1,
this.contentFlex = 2,
});
static bool getIsExpand(BuildContext context) {
return ResponsiveBreakpoints.of(context).largerOrEqualTo(TABLET);
}
@override
Widget build(BuildContext context) {
if (getIsExpand(context)) {
return AppBackground(
isRoot: true,
child: Row(
children: [
Flexible(
flex: asideFlex,
child: aside,
),
VerticalDivider(width: 1),
if (child != null && child != aside)
Flexible(flex: contentFlex, child: child!)
else
Flexible(
flex: contentFlex,
child: ResponsiveScaffoldLanding(child: null),
),
],
),
);
}
return AppBackground(isRoot: true, child: child ?? aside);
}
}
class ResponsiveScaffoldLanding extends StatelessWidget {
final Widget? child;
const ResponsiveScaffoldLanding({super.key, required this.child});
@override
Widget build(BuildContext context) {
if (ResponsiveScaffold.getIsExpand(context) || child == null) {
return AppScaffold(
noBackground: true,
appBar: AppBar(),
body: const SizedBox.shrink(),
);
}
return child!;
}
}

View File

@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@ -29,14 +30,24 @@ class PostCommentQuickAction extends StatelessWidget {
return Container( return Container(
height: 240, height: 240,
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? const EdgeInsets.symmetric(vertical: 8)
: EdgeInsets.zero,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.zero, borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
border: Border.symmetric( ? const BorderRadius.all(Radius.circular(8))
horizontal: BorderSide( : BorderRadius.zero,
color: Theme.of(context).dividerColor, border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
width: 1 / devicePixelRatio, ? Border.all(
), color: Theme.of(context).dividerColor,
), width: 1 / devicePixelRatio,
)
: Border.symmetric(
horizontal: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
), ),
child: PostMiniEditor( child: PostMiniEditor(
postReplyId: parentPost.id, postReplyId: parentPost.id,

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:animations/animations.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart'; import 'package:file_saver/file_saver.dart';
@ -25,6 +26,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/translation.dart'; import 'package:surface/providers/translation.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/reaction.dart'; import 'package:surface/types/reaction.dart';
@ -51,7 +53,6 @@ class OpenablePostItem extends StatelessWidget {
final bool showMenu; final bool showMenu;
final bool showFullPost; final bool showFullPost;
final bool showExpandableComments; final bool showExpandableComments;
final bool useReplace;
final double? maxWidth; final double? maxWidth;
final Function(SnPost data)? onChanged; final Function(SnPost data)? onChanged;
final Function()? onDeleted; final Function()? onDeleted;
@ -65,7 +66,6 @@ class OpenablePostItem extends StatelessWidget {
this.showMenu = true, this.showMenu = true,
this.showFullPost = false, this.showFullPost = false,
this.showExpandableComments = false, this.showExpandableComments = false,
this.useReplace = false,
this.maxWidth, this.maxWidth,
this.onChanged, this.onChanged,
this.onDeleted, this.onDeleted,
@ -74,32 +74,40 @@ class OpenablePostItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();
return Container( return Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Center( child: Center(
child: GestureDetector( child: OpenContainer(
child: PostItem( closedBuilder: (_, __) => Container(
data: data, constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
maxWidth: maxWidth, child: PostItem(
showComments: showComments, data: data,
showFullPost: showFullPost, maxWidth: maxWidth,
showExpandableComments: showExpandableComments, showComments: showComments,
onChanged: onChanged, showFullPost: showFullPost,
onDeleted: onDeleted, showExpandableComments: showExpandableComments,
onSelectAnswer: onSelectAnswer, onChanged: onChanged,
onDeleted: onDeleted,
onSelectAnswer: onSelectAnswer,
),
),
openBuilder: (_, close) => PostDetailScreen(
slug: data.id.toString(),
preload: data,
onBack: close,
),
openColor: Colors.transparent,
openElevation: 0,
transitionType: ContainerTransitionType.fade,
closedElevation: 0,
closedColor: Theme.of(context).colorScheme.surface.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0 : 1,
),
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
onTap: () {
if (useReplace) {
GoRouter.of(context)
.pushReplacementNamed('postDetail', pathParameters: {
'slug': data.id.toString(),
});
} else {
GoRouter.of(context).pushNamed('postDetail', pathParameters: {
'slug': data.id.toString(),
});
}
},
), ),
), ),
); );