Compare commits

..

No commits in common. "87029e3538b5ee58b871c9cb9110ead7dd73652f" and "19076f8136f1e0f4f949e871998feab81ab9afbf" have entirely different histories.

45 changed files with 1052 additions and 1206 deletions

View File

@ -193,9 +193,6 @@
"settingsColorSchemeDescription": "Set the application primary color.", "settingsColorSchemeDescription": "Set the application primary color.",
"settingsColorSeed": "Color Seed", "settingsColorSeed": "Color Seed",
"settingsColorSeedDescription": "Select one of the present color schemes.", "settingsColorSeedDescription": "Select one of the present color schemes.",
"settingsFeatures": "Features",
"settingsNotifyWithHaptic": "Haptic when Notified",
"settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
"settingsNetwork": "Network", "settingsNetwork": "Network",
"settingsNetworkServer": "HyperNet Server", "settingsNetworkServer": "HyperNet Server",
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",

View File

@ -191,9 +191,6 @@
"settingsColorSchemeDescription": "设置应用主题色。", "settingsColorSchemeDescription": "设置应用主题色。",
"settingsColorSeed": "预设色彩主题", "settingsColorSeed": "预设色彩主题",
"settingsColorSeedDescription": "选择一个预设色彩主题。", "settingsColorSeedDescription": "选择一个预设色彩主题。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知时振动",
"settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
"settingsNetwork": "网络", "settingsNetwork": "网络",
"settingsNetworkServer": "HyperNet 服务器", "settingsNetworkServer": "HyperNet 服务器",
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",

View File

@ -191,9 +191,6 @@
"settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題", "settingsColorSeed": "預設色彩主題",
"settingsColorSeedDescription": "選擇一個預設色彩主題。", "settingsColorSeedDescription": "選擇一個預設色彩主題。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知時振動",
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
"settingsNetwork": "網絡", "settingsNetwork": "網絡",
"settingsNetworkServer": "HyperNet 服務器", "settingsNetworkServer": "HyperNet 服務器",
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
@ -216,9 +213,8 @@
"sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentCollapsed": "敏感內容已摺疊。",
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
"sensitiveContentReveal": "顯示內容", "sensitiveContentReveal": "顯示內容",
"serverConnecting": "正在連接…", "serverConnecting": "正在連接服務器…",
"serverDisconnected": "已斷開連接", "serverDisconnected": "已與服務器斷開連接",
"serverConnected": "已連接",
"fieldChatAlias": "頻道別名", "fieldChatAlias": "頻道別名",
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
"fieldChatName": "名稱", "fieldChatName": "名稱",
@ -296,7 +292,6 @@
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻", "addAttachmentFromCameraVideo": "拍攝視頻",
"addAttachmentFromRandomId": "通過訪問 ID 鏈接", "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片", "attachmentPastedImage": "粘貼的圖片",
"attachmentInsertLink": "插入連接", "attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",

View File

@ -191,9 +191,6 @@
"settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題", "settingsColorSeed": "預設色彩主題",
"settingsColorSeedDescription": "選擇一個預設色彩主題。", "settingsColorSeedDescription": "選擇一個預設色彩主題。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知時振動",
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
"settingsNetwork": "網絡", "settingsNetwork": "網絡",
"settingsNetworkServer": "HyperNet 服務器", "settingsNetworkServer": "HyperNet 服務器",
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
@ -216,9 +213,8 @@
"sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentCollapsed": "敏感內容已摺疊。",
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
"sensitiveContentReveal": "顯示內容", "sensitiveContentReveal": "顯示內容",
"serverConnecting": "正在連接…", "serverConnecting": "正在連接服務器…",
"serverDisconnected": "已斷開連接", "serverDisconnected": "已與服務器斷開連接",
"serverConnected": "已連接",
"fieldChatAlias": "頻道別名", "fieldChatAlias": "頻道別名",
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
"fieldChatName": "名稱", "fieldChatName": "名稱",
@ -296,7 +292,6 @@
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻", "addAttachmentFromCameraVideo": "拍攝視頻",
"addAttachmentFromRandomId": "通過訪問 ID 鏈接", "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片", "attachmentPastedImage": "粘貼的圖片",
"attachmentInsertLink": "插入連接", "attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",

View File

@ -14,7 +14,6 @@ const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background'; const kAppBackgroundStoreKey = 'app_has_background';
const kAppColorSchemeStoreKey = 'app_color_scheme'; const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse'; const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,

View File

@ -4,10 +4,8 @@ import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_udid/flutter_udid.dart'; import 'package:flutter_udid/flutter_udid.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart'; import 'package:surface/providers/websocket.dart';
@ -17,13 +15,11 @@ class NotificationProvider extends ChangeNotifier {
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
late final UserProvider _ua; late final UserProvider _ua;
late final WebSocketProvider _ws; late final WebSocketProvider _ws;
late final ConfigProvider _cfg;
NotificationProvider(BuildContext context) { NotificationProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
_ua = context.read<UserProvider>(); _ua = context.read<UserProvider>();
_ws = context.read<WebSocketProvider>(); _ws = context.read<WebSocketProvider>();
_cfg = context.read<ConfigProvider>();
} }
Future<void> registerPushNotifications() async { Future<void> registerPushNotifications() async {
@ -79,8 +75,6 @@ class NotificationProvider extends ChangeNotifier {
final notification = SnNotification.fromJson(event.payload!); final notification = SnNotification.fromJson(event.payload!);
notifications.add(notification); notifications.add(notification);
notifyListeners(); notifyListeners();
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.lightImpact();
} }
}); });
} }

View File

@ -36,7 +36,10 @@ import 'package:surface/widgets/navigation/app_scaffold.dart';
final _appRoutes = [ final _appRoutes = [
ShellRoute( ShellRoute(
builder: (context, state, child) => child, builder: (context, state, child) => AppPageScaffold(
body: child,
showAppBar: false,
),
routes: [ routes: [
GoRoute( GoRoute(
path: '/', path: '/',
@ -55,39 +58,47 @@ final _appRoutes = [
GoRoute( GoRoute(
path: '/write/:mode', path: '/write/:mode',
name: 'postEditor', name: 'postEditor',
builder: (context, state) => PostEditorScreen( builder: (context, state) => AppBackground(
mode: state.pathParameters['mode']!, child: PostEditorScreen(
postEditId: int.tryParse( mode: state.pathParameters['mode']!,
state.uri.queryParameters['editing'] ?? '', postEditId: int.tryParse(
state.uri.queryParameters['editing'] ?? '',
),
postReplyId: int.tryParse(
state.uri.queryParameters['replying'] ?? '',
),
postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '',
),
extraProps: state.extra as PostEditorExtraProps?,
), ),
postReplyId: int.tryParse(
state.uri.queryParameters['replying'] ?? '',
),
postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '',
),
extraProps: state.extra as PostEditorExtraProps?,
), ),
), ),
GoRoute( GoRoute(
path: '/search', path: '/search',
name: 'postSearch', name: 'postSearch',
builder: (context, state) => PostSearchScreen( builder: (context, state) => AppBackground(
initialTags: state.uri.queryParameters['tags']?.split(','), child: PostSearchScreen(
initialCategories: state.uri.queryParameters['categories']?.split(','), initialTags: state.uri.queryParameters['tags']?.split(','),
initialCategories: state.uri.queryParameters['categories']?.split(','),
),
), ),
), ),
GoRoute( GoRoute(
path: '/publishers/:name', path: '/publishers/:name',
name: 'postPublisher', name: 'postPublisher',
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!), builder: (context, state) => AppBackground(
child: PostPublisherScreen(name: state.pathParameters['name']!),
),
), ),
GoRoute( GoRoute(
path: '/:slug', path: '/:slug',
name: 'postDetail', name: 'postDetail',
builder: (context, state) => PostDetailScreen( builder: (context, state) => AppBackground(
slug: state.pathParameters['slug']!, child: PostDetailScreen(
preload: state.extra as SnPost?, slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
),
), ),
), ),
], ],
@ -95,15 +106,7 @@ final _appRoutes = [
GoRoute( GoRoute(
path: '/account', path: '/account',
name: 'account', name: 'account',
pageBuilder: (context, state) => CustomTransitionPage( pageBuilder: (context, state) => NoTransitionPage(
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: child,
);
},
child: const AccountScreen(), child: const AccountScreen(),
), ),
routes: [], routes: [],
@ -111,15 +114,7 @@ final _appRoutes = [
GoRoute( GoRoute(
path: '/chat', path: '/chat',
name: 'chat', name: 'chat',
pageBuilder: (context, state) => CustomTransitionPage( pageBuilder: (context, state) => NoTransitionPage(
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: child,
);
},
child: const ChatScreen(), child: const ChatScreen(),
), ),
routes: [ routes: [
@ -233,43 +228,57 @@ final _appRoutes = [
], ],
), ),
ShellRoute( ShellRoute(
builder: (context, state, child) => child, builder: (context, state, child) => AppPageScaffold(body: child),
routes: [ routes: [
GoRoute( GoRoute(
path: '/auth/login', path: '/auth/login',
name: 'authLogin', name: 'authLogin',
builder: (context, state) => LoginScreen(), builder: (context, state) => const AppBackground(
child: LoginScreen(),
),
), ),
GoRoute( GoRoute(
path: '/auth/register', path: '/auth/register',
name: 'authRegister', name: 'authRegister',
builder: (context, state) => RegisterScreen(), builder: (context, state) => const AppBackground(
child: RegisterScreen(),
),
), ),
GoRoute( GoRoute(
path: '/reports', path: '/reports',
name: 'abuseReport', name: 'abuseReport',
builder: (context, state) => AbuseReportScreen(), builder: (context, state) => const AppBackground(
child: AbuseReportScreen(),
),
), ),
GoRoute( GoRoute(
path: '/account/profile/edit', path: '/account/profile/edit',
name: 'accountProfileEdit', name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(), builder: (context, state) => const AppBackground(
child: ProfileEditScreen(),
),
), ),
GoRoute( GoRoute(
path: '/account/publishers', path: '/account/publishers',
name: 'accountPublishers', name: 'accountPublishers',
builder: (context, state) => PublisherScreen(), builder: (context, state) => const AppBackground(
child: PublisherScreen(),
),
), ),
GoRoute( GoRoute(
path: '/account/publishers/new', path: '/account/publishers/new',
name: 'accountPublisherNew', name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(), builder: (context, state) => const AppBackground(
child: AccountPublisherNewScreen(),
),
), ),
GoRoute( GoRoute(
path: '/account/publishers/edit/:name', path: '/account/publishers/edit/:name',
name: 'accountPublisherEdit', name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen( builder: (context, state) => AppBackground(
name: state.pathParameters['name']!, child: AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
), ),
), ),
], ],
@ -282,22 +291,26 @@ final _appRoutes = [
), ),
), ),
ShellRoute( ShellRoute(
builder: (context, state, child) => child, builder: (context, state, child) => AppPageScaffold(body: child),
routes: [ routes: [
GoRoute( GoRoute(
path: '/settings', path: '/settings',
name: 'settings', name: 'settings',
builder: (context, state) => SettingsScreen(), builder: (context, state) => const AppBackground(
child: SettingsScreen(),
),
), ),
], ],
), ),
ShellRoute( ShellRoute(
builder: (context, state, child) => child, builder: (context, state, child) => AppPageScaffold(body: child),
routes: [ routes: [
GoRoute( GoRoute(
path: '/about', path: '/about',
name: 'about', name: 'about',
builder: (context, state) => AboutScreen(), builder: (context, state) => const AppBackground(
child: AboutScreen(),
),
), ),
], ],
), ),

View File

@ -6,7 +6,6 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import '../types/account.dart'; import '../types/account.dart';
@ -57,11 +56,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return Scaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAbuseReport').tr(),
),
body: Column( body: Column(
children: [ children: [
ListTile( ListTile(
@ -78,7 +73,6 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
else else
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.only(top: 8),
itemCount: _reports.length, itemCount: _reports.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return ListTile( return ListTile(

View File

@ -12,7 +12,6 @@ import 'package:surface/providers/websocket.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.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/navigation/app_scaffold.dart';
class AccountScreen extends StatelessWidget { class AccountScreen extends StatelessWidget {
const AccountScreen({super.key}); const AccountScreen({super.key});
@ -21,7 +20,7 @@ class AccountScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenAccount").tr(), title: Text("screenAccount").tr(),

View File

@ -18,7 +18,6 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.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_scaffold.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
class ProfileEditScreen extends StatefulWidget { class ProfileEditScreen extends StatefulWidget {
@ -82,7 +81,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onDateTimeChanged: (DateTime newDate) { onDateTimeChanged: (DateTime newDate) {
setState(() { setState(() {
_birthday = newDate; _birthday = newDate;
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); _birthdayController.text =
DateFormat(_kDateFormat).format(_birthday!);
}); });
}, },
), ),
@ -96,9 +96,11 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
if (image == null) return; if (image == null) return;
if (!mounted) return; if (!mounted) return;
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); final ImageProvider imageProvider =
final aspectRatios = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; final aspectRatios = place == 'banner'
? [CropAspectRatio(width: 16, height: 7)]
: [CropAspectRatio(width: 1, height: 1)];
final result = (!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
@ -120,7 +122,10 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); final rawBytes =
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
try { try {
final attachment = await attach.directUploadOne( final attachment = await attach.directUploadOne(
@ -207,141 +212,136 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return SingleChildScrollView(
appBar: AppBar( child: Column(
leading: const PageBackButton(), crossAxisAlignment: CrossAxisAlignment.start,
title: Text('screenAccountProfileEdit').tr(), children: [
), LoadingIndicator(isActive: _isBusy),
body: SingleChildScrollView( const Gap(24),
child: Column( Stack(
crossAxisAlignment: CrossAxisAlignment.start, clipBehavior: Clip.none,
children: [ children: [
LoadingIndicator(isActive: _isBusy), Material(
const Gap(24), elevation: 0,
Stack( child: InkWell(
clipBehavior: Clip.none, child: ClipRRect(
children: [ borderRadius: const BorderRadius.all(Radius.circular(8)),
Material( child: AspectRatio(
elevation: 0, aspectRatio: 16 / 9,
child: InkWell( child: Container(
child: ClipRRect( color:
borderRadius: const BorderRadius.all(Radius.circular(8)), Theme.of(context).colorScheme.surfaceContainerHigh,
child: AspectRatio( child: _banner != null
aspectRatio: 16 / 9, ? AutoResizeUniversalImage(
child: Container( sn.getAttachmentUrl(_banner!),
color: Theme.of(context).colorScheme.surfaceContainerHigh, fit: BoxFit.cover,
child: _banner != null )
? AutoResizeUniversalImage( : const SizedBox.shrink(),
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
), ),
), ),
),
onTap: () {
_updateImage('banner');
},
),
),
Positioned(
bottom: -28,
left: 16,
child: Material(
elevation: 2,
borderRadius: const BorderRadius.all(Radius.circular(40)),
child: InkWell(
child: AccountImage(content: _avatar, radius: 40),
onTap: () { onTap: () {
_updateImage('banner'); _updateImage('avatar');
}, },
), ),
), ),
Positioned( ),
bottom: -28, ],
left: 16, ).padding(horizontal: padding),
child: Material( const Gap(8 + 28),
elevation: 2, Column(
borderRadius: const BorderRadius.all(Radius.circular(40)), children: [
child: InkWell( TextField(
child: AccountImage(content: _avatar, radius: 40), readOnly: true,
onTap: () { controller: _usernameController,
_updateImage('avatar'); decoration: InputDecoration(
}, border: const UnderlineInputBorder(),
), labelText: 'fieldUsername'.tr(),
), helperText: 'fieldUsernameCannotEditHint'.tr(),
), ),
], ),
).padding(horizontal: padding), const Gap(4),
const Gap(8 + 28), TextField(
Column( controller: _nicknameController,
children: [ decoration: InputDecoration(
TextField( border: const UnderlineInputBorder(),
readOnly: true, labelText: 'fieldNickname'.tr(),
controller: _usernameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(),
),
), ),
const Gap(4), ),
TextField( const Gap(4),
controller: _nicknameController, Row(
decoration: InputDecoration( children: [
border: const UnderlineInputBorder(), Flexible(
labelText: 'fieldNickname'.tr(), flex: 1,
), child: TextField(
), controller: _firstNameController,
const Gap(4), decoration: InputDecoration(
Row( border: const UnderlineInputBorder(),
children: [ labelText: 'fieldFirstName'.tr(),
Flexible(
flex: 1,
child: TextField(
controller: _firstNameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldFirstName'.tr(),
),
), ),
), ),
const Gap(8), ),
Flexible( const Gap(8),
flex: 1, Flexible(
child: TextField( flex: 1,
controller: _lastNameController, child: TextField(
decoration: InputDecoration( controller: _lastNameController,
border: const UnderlineInputBorder(), decoration: InputDecoration(
labelText: 'fieldLastName'.tr(), border: const UnderlineInputBorder(),
), labelText: 'fieldLastName'.tr(),
), ),
), ),
],
),
const Gap(4),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldDescription'.tr(),
), ),
],
),
const Gap(4),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldDescription'.tr(),
), ),
const Gap(4), ),
TextField( const Gap(4),
controller: _birthdayController, TextField(
readOnly: true, controller: _birthdayController,
decoration: InputDecoration( readOnly: true,
border: const UnderlineInputBorder(), decoration: InputDecoration(
labelText: 'fieldBirthday'.tr(), border: const UnderlineInputBorder(),
), labelText: 'fieldBirthday'.tr(),
onTap: () => _selectBirthday(),
), ),
], onTap: () => _selectBirthday(),
).padding(horizontal: padding + 8), ),
const Gap(12), ],
Row( ).padding(horizontal: padding + 8),
mainAxisAlignment: MainAxisAlignment.end, const Gap(12),
children: [ Row(
ElevatedButton.icon( mainAxisAlignment: MainAxisAlignment.end,
onPressed: _isBusy ? null : _updateUserInfo, children: [
icon: const Icon(Symbols.save), ElevatedButton.icon(
label: Text('apply').tr(), onPressed: _isBusy ? null : _updateUserInfo,
), icon: const Icon(Symbols.save),
], label: Text('apply').tr(),
).padding(horizontal: padding), ),
], ],
), ).padding(horizontal: padding),
],
), ),
); );
} }

View File

@ -19,7 +19,6 @@ import 'package:surface/types/check_in.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
const Map<String, (String, IconData, Color)> kBadgesMeta = { const Map<String, (String, IconData, Color)> kBadgesMeta = {
@ -242,7 +241,6 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return Scaffold(
backgroundColor: Colors.transparent,
body: CustomScrollView( body: CustomScrollView(
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [

View File

@ -18,7 +18,6 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.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_scaffold.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
class AccountPublisherEditScreen extends StatefulWidget { class AccountPublisherEditScreen extends StatefulWidget {
@ -177,7 +176,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return Scaffold(
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [

View File

@ -10,7 +10,6 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountPublisherNewScreen extends StatefulWidget { class AccountPublisherNewScreen extends StatefulWidget {
const AccountPublisherNewScreen({super.key}); const AccountPublisherNewScreen({super.key});
@ -25,11 +24,7 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return Scaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountPublisherNew').tr(),
),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [

View File

@ -10,7 +10,6 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.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_scaffold.dart';
class PublisherScreen extends StatefulWidget { class PublisherScreen extends StatefulWidget {
const PublisherScreen({super.key}); const PublisherScreen({super.key});
@ -33,7 +32,8 @@ 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(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); final List<SnPublisher> out = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
if (!mounted) return; if (!mounted) return;
@ -53,11 +53,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return Scaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountPublishers').tr(),
),
body: Column( body: Column(
children: [ children: [
ListTile( ListTile(
@ -66,7 +62,9 @@ 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).pushNamed('accountPublisherNew').then((value) { GoRouter.of(context)
.pushNamed('accountPublisherNew')
.then((value) {
if (value == true) { if (value == true) {
_publishers.clear(); _publishers.clear();
_fetchPublishers(); _fetchPublishers();
@ -77,52 +75,48 @@ class _PublisherScreenState extends State<PublisherScreen> {
const Divider(height: 1), const Divider(height: 1),
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
Expanded( Expanded(
child: MediaQuery.removePadding( child: RefreshIndicator(
context: context, onRefresh: () {
removeTop: true, _publishers.clear();
child: RefreshIndicator( return _fetchPublishers();
onRefresh: () { },
_publishers.clear(); child: ListView.builder(
return _fetchPublishers(); itemCount: _publishers.length,
}, itemBuilder: (context, idx) {
child: ListView.builder( final publisher = _publishers[idx];
itemCount: _publishers.length, return ListTile(
itemBuilder: (context, idx) { title: Text(publisher.nick),
final publisher = _publishers[idx]; subtitle: Text('@${publisher.name}'),
return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(publisher.nick), leading: AccountImage(content: publisher.avatar),
subtitle: Text('@${publisher.name}'), trailing: PopupMenuButton(
contentPadding: const EdgeInsets.symmetric(horizontal: 16), itemBuilder: (BuildContext context) => [
leading: AccountImage(content: publisher.avatar), PopupMenuItem(
trailing: PopupMenuButton( child: Row(
itemBuilder: (BuildContext context) => [ children: [
PopupMenuItem( const Icon(Symbols.edit),
child: Row( const Gap(16),
children: [ Text('edit').tr(),
const Icon(Symbols.edit), ],
const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'accountPublisherEdit',
pathParameters: {
'name': publisher.name,
},
).then((value) {
if (value == true) {
_publishers.clear();
_fetchPublishers();
}
});
},
), ),
], onTap: () {
), GoRouter.of(context).pushNamed(
); 'accountPublisherEdit',
}, pathParameters: {
), 'name': publisher.name,
},
).then((value) {
if (value == true) {
_publishers.clear();
_fetchPublishers();
}
});
},
),
],
),
);
},
), ),
), ),
), ),

View File

@ -11,7 +11,6 @@ import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/attachment/attachment_zoom.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class AlbumScreen extends StatefulWidget { class AlbumScreen extends StatefulWidget {
@ -83,7 +82,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return Scaffold(
body: CustomScrollView( body: CustomScrollView(
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [

View File

@ -9,7 +9,6 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/auth.dart'; import 'package:surface/types/auth.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import '../../providers/websocket.dart'; import '../../providers/websocket.dart';
@ -36,73 +35,67 @@ class _LoginScreenState extends State<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return Theme(
appBar: AppBar( data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
leading: const PageBackButton(), child: SingleChildScrollView(
title: Text('screenAuthLogin').tr(), child: PageTransitionSwitcher(
), transitionBuilder: (
body: Theme( Widget child,
data: Theme.of(context).copyWith(canvasColor: Colors.transparent), Animation<double> primaryAnimation,
child: SingleChildScrollView( Animation<double> secondaryAnimation,
child: PageTransitionSwitcher( ) {
transitionBuilder: ( return SharedAxisTransition(
Widget child, animation: primaryAnimation,
Animation<double> primaryAnimation, secondaryAnimation: secondaryAnimation,
Animation<double> secondaryAnimation, transitionType: SharedAxisTransitionType.horizontal,
) { child: Container(
return SharedAxisTransition( constraints: BoxConstraints(maxWidth: 380),
animation: primaryAnimation, child: child,
secondaryAnimation: secondaryAnimation, ),
transitionType: SharedAxisTransitionType.horizontal, );
child: Container( },
constraints: BoxConstraints(maxWidth: 380), child: switch (_period % 3) {
child: child, 1 => _LoginPickerScreen(
), key: const ValueKey(1),
); ticket: _currentTicket,
}, factors: _factors,
child: switch (_period % 3) { onTicket: (p0) => setState(() {
1 => _LoginPickerScreen( _currentTicket = p0;
key: const ValueKey(1), }),
ticket: _currentTicket, onPickFactor: (p0) => setState(() {
factors: _factors, _factorPicked = p0;
onTicket: (p0) => setState(() { }),
_currentTicket = p0; onNext: () => setState(() {
}), _period++;
onPickFactor: (p0) => setState(() { }),
_factorPicked = p0; ),
}), 2 => _LoginCheckScreen(
onNext: () => setState(() { key: const ValueKey(2),
_period++; ticket: _currentTicket,
}), factor: _factorPicked,
), onTicket: (p0) => setState(() {
2 => _LoginCheckScreen( _currentTicket = p0;
key: const ValueKey(2), }),
ticket: _currentTicket, onNext: () => setState(() {
factor: _factorPicked, _period = 1;
onTicket: (p0) => setState(() { }),
_currentTicket = p0; ),
}), _ => _LoginLookupScreen(
onNext: () => setState(() { key: const ValueKey(0),
_period = 1; ticket: _currentTicket,
}), onTicket: (p0) => setState(() {
), _currentTicket = p0;
_ => _LoginLookupScreen( }),
key: const ValueKey(0), onFactor: (p0) => setState(() {
ticket: _currentTicket, _factors = p0;
onTicket: (p0) => setState(() { }),
_currentTicket = p0; onNext: () => setState(() {
}), _period++;
onFactor: (p0) => setState(() { }),
_factors = p0; ),
}), },
onNext: () => setState(() { ).padding(all: 24),
_period++; ).center(),
}),
),
},
).padding(all: 24),
).center(),
),
); );
} }
} }
@ -448,7 +441,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
widget.onNext(); widget.onNext();
} catch (err) { } catch (err) {
if (mounted) context.showErrorDialog(err); if(mounted) context.showErrorDialog(err);
return; return;
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isBusy = false);

View File

@ -8,7 +8,6 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class RegisterScreen extends StatefulWidget { class RegisterScreen extends StatefulWidget {
@ -55,178 +54,175 @@ class _RegisterScreenState extends State<RegisterScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return StyledWidget(Container(
appBar: AppBar( constraints: const BoxConstraints(maxWidth: 380),
leading: const PageBackButton(), child: SingleChildScrollView(
title: Text('screenAuthRegister').tr(), child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
body: StyledWidget(Container( children: [
constraints: const BoxConstraints(maxWidth: 380), Align(
child: SingleChildScrollView( alignment: Alignment.centerLeft,
child: Column( child: CircleAvatar(
crossAxisAlignment: CrossAxisAlignment.start, radius: 26,
children: [ child: const Icon(
Align( Symbols.person_add,
alignment: Alignment.centerLeft, size: 28,
child: CircleAvatar(
radius: 26,
child: const Icon(
Symbols.person_add,
size: 28,
),
).padding(bottom: 8),
),
Text(
'screenAuthRegister',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w900,
), ),
).tr().padding(left: 4, bottom: 16), ).padding(bottom: 8),
Form( ),
key: _formKey, Text(
autovalidateMode: AutovalidateMode.onUserInteraction, 'screenAuthRegister',
child: Column( style: const TextStyle(
children: [ fontSize: 28,
TextFormField( fontWeight: FontWeight.w900,
validator: (value) {
if (value == null || value.length < 4 || value.length > 32) {
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
}
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
return 'fieldUsernameAlphanumOnly'.tr();
}
return null;
},
autocorrect: false,
enableSuggestions: false,
controller: _usernameController,
autofillHints: const [AutofillHints.username],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldUsername'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.length < 4 || value.length > 32) {
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
}
return null;
},
autocorrect: false,
enableSuggestions: false,
controller: _nicknameController,
autofillHints: const [AutofillHints.nickname],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
if (!EmailValidator.validate(value)) {
return 'fieldEmailAddressMustBeValid'.tr();
}
return null;
},
autocorrect: false,
enableSuggestions: false,
controller: _emailController,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldEmail'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
return null;
},
obscureText: true,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.password],
controller: _passwordController,
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldPassword'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
).padding(horizontal: 7),
), ),
const Gap(16), ).tr().padding(left: 4, bottom: 16),
Align( Form(
alignment: Alignment.centerRight, key: _formKey,
child: StyledWidget( autovalidateMode: AutovalidateMode.onUserInteraction,
Container( child: Column(
constraints: const BoxConstraints(maxWidth: 290), children: [
child: Column( TextFormField(
crossAxisAlignment: CrossAxisAlignment.end, validator: (value) {
children: [ if (value == null || value.length < 4 || value.length > 32) {
Text( return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
'termAcceptNextWithAgree'.tr(), }
textAlign: TextAlign.end, if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
style: Theme.of(context).textTheme.bodySmall!.copyWith( return 'fieldUsernameAlphanumOnly'.tr();
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()), }
), return null;
), },
Material( autocorrect: false,
color: Colors.transparent, enableSuggestions: false,
child: InkWell( controller: _usernameController,
child: Row( autofillHints: const [AutofillHints.username],
mainAxisSize: MainAxisSize.min, decoration: InputDecoration(
children: [ isDense: true,
Text('termAcceptLink'.tr()), border: const UnderlineInputBorder(),
const Gap(4), labelText: 'fieldUsername'.tr(),
const Icon(Symbols.launch, size: 14),
],
),
onTap: () {
launchUrlString('https://solsynth.dev/terms');
},
),
),
],
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
).padding(horizontal: 16), const Gap(12),
), TextFormField(
Align( validator: (value) {
alignment: Alignment.centerRight, if (value == null || value.length < 4 || value.length > 32) {
child: TextButton( return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
onPressed: () => _performAction(context), }
child: Row( return null;
mainAxisSize: MainAxisSize.min, },
autocorrect: false,
enableSuggestions: false,
controller: _nicknameController,
autofillHints: const [AutofillHints.nickname],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
if (!EmailValidator.validate(value)) {
return 'fieldEmailAddressMustBeValid'.tr();
}
return null;
},
autocorrect: false,
enableSuggestions: false,
controller: _emailController,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldEmail'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
return null;
},
obscureText: true,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.password],
controller: _passwordController,
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldPassword'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
).padding(horizontal: 7),
),
const Gap(16),
Align(
alignment: Alignment.centerRight,
child: StyledWidget(
Container(
constraints: const BoxConstraints(maxWidth: 290),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text('next').tr(), Text(
const Icon(Symbols.chevron_right), 'termAcceptNextWithAgree'.tr(),
textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha((255 * 0.75).round()),
),
),
Material(
color: Colors.transparent,
child: InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('termAcceptLink'.tr()),
const Gap(4),
const Icon(Symbols.launch, size: 14),
],
),
onTap: () {
launchUrlString('https://solsynth.dev/terms');
},
),
),
], ],
), ),
), ),
).padding(horizontal: 16),
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => _performAction(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('next').tr(),
const Icon(Symbols.chevron_right),
],
),
), ),
], ),
), ],
), ),
)).padding(all: 24).center(), ),
); )).padding(all: 24).center();
} }
} }

View File

@ -13,7 +13,6 @@ 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_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';
@ -121,7 +120,7 @@ class _ChatScreenState extends State<ChatScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@ -132,7 +131,7 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@ -196,58 +195,22 @@ class _ChatScreenState extends State<ChatScreen> {
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
Expanded( Expanded(
child: MediaQuery.removePadding( child: RefreshIndicator(
context: context, onRefresh: () => Future.sync(() => _refreshChannels()),
removeTop: true, child: ListView.builder(
child: RefreshIndicator( itemCount: _channels?.length ?? 0,
onRefresh: () => Future.sync(() => _refreshChannels()), itemBuilder: (context, idx) {
child: ListView.builder( final channel = _channels![idx];
itemCount: _channels?.length ?? 0, final lastMessage = _lastMessages?[channel.id];
itemBuilder: (context, idx) {
final channel = _channels![idx];
final lastMessage = _lastMessages?[channel.id];
if (channel.type == 1) { if (channel.type == 1) {
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
(ele) => ele?.accountId != ua.user?.id, (ele) => ele?.accountId != ua.user?.id,
orElse: () => null, orElse: () => null,
); );
return ListTile(
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
'channelDirectMessageDescription'.tr(args: [
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (mounted) _refreshChannels();
});
},
);
}
return ListTile( return ListTile(
title: Text(channel.name), title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
subtitle: lastMessage != null subtitle: lastMessage != null
? Text( ? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
@ -255,14 +218,15 @@ class _ChatScreenState extends State<ChatScreen> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
) )
: Text( : Text(
channel.description, 'channelDirectMessageDescription'.tr(args: [
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
]),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 16), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage( leading: AccountImage(
content: null, content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
fallbackWidget: const Icon(Symbols.chat, size: 20),
), ),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
@ -272,12 +236,43 @@ class _ChatScreenState extends State<ChatScreen> {
'alias': channel.alias, 'alias': channel.alias,
}, },
).then((value) { ).then((value) {
if (value == true) _refreshChannels(); if (mounted) _refreshChannels();
}); });
}, },
); );
}, }
),
return ListTile(
title: Text(channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
channel.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (value == true) _refreshChannels();
});
},
);
},
), ),
), ),
), ),

View File

@ -9,7 +9,6 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/chat_call.dart';
import 'package:surface/widgets/chat/call/call_controls.dart'; import 'package:surface/widgets/chat/call/call_controls.dart';
import 'package:surface/widgets/chat/call/call_participant.dart'; import 'package:surface/widgets/chat/call/call_participant.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class CallRoomScreen extends StatefulWidget { class CallRoomScreen extends StatefulWidget {
final String scope; final String scope;
@ -153,7 +152,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
return ListenableBuilder( return ListenableBuilder(
listenable: call, listenable: call,
builder: (context, _) { builder: (context, _) {
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: RichText( title: RichText(
textAlign: TextAlign.center, textAlign: TextAlign.center,

View File

@ -14,7 +14,6 @@ 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/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_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class ChannelDetailScreen extends StatefulWidget { class ChannelDetailScreen extends StatefulWidget {
@ -190,7 +189,7 @@ 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 Scaffold(
appBar: AppBar( appBar: AppBar(
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
), ),

View File

@ -12,7 +12,6 @@ import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.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_scaffold.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatManageScreen extends StatefulWidget { class ChatManageScreen extends StatefulWidget {
@ -122,7 +121,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: widget.editingChannelAlias != null title: widget.editingChannelAlias != null
? Text('screenChatManage').tr() ? Text('screenChatManage').tr()

View File

@ -20,7 +20,6 @@ import 'package:surface/widgets/chat/chat_message_input.dart';
import 'package:surface/widgets/chat/chat_typing_indicator.dart'; import 'package:surface/widgets/chat/chat_typing_indicator.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_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../../providers/user_directory.dart'; import '../../providers/user_directory.dart';
@ -212,7 +211,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
final call = context.watch<ChatCallProvider>(); final call = context.watch<ChatCallProvider>();
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
_channel?.type == 1 _channel?.type == 1

View File

@ -13,7 +13,6 @@ import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.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/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -96,7 +95,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return Scaffold(
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
key: _fabKey, key: _fabKey,
@ -213,7 +212,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
), ),
), ),
), ),
const SliverGap(12), const SliverGap(8),
SliverInfiniteList( SliverInfiniteList(
itemCount: _posts.length, itemCount: _posts.length,
isLoading: _isBusy, isLoading: _isBusy,
@ -243,10 +242,10 @@ class _ExploreScreenState extends State<ExploreScreen> {
), ),
openColor: Colors.transparent, openColor: Colors.transparent,
openElevation: 0, openElevation: 0,
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75), closedColor: Theme.of(context).colorScheme.surface,
transitionType: ContainerTransitionType.fade, transitionType: ContainerTransitionType.fade,
closedShape: const RoundedRectangleBorder( closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
), ),
); );

View File

@ -11,7 +11,6 @@ import 'package:surface/widgets/account/account_image.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_scaffold.dart';
import '../providers/userinfo.dart'; import '../providers/userinfo.dart';
import '../widgets/unauthorized_hint.dart'; import '../widgets/unauthorized_hint.dart';
@ -181,7 +180,7 @@ class _FriendScreenState extends State<FriendScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenFriend').tr(), title: Text('screenFriend').tr(),
@ -192,7 +191,7 @@ class _FriendScreenState extends State<FriendScreen> {
); );
} }
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenFriend').tr(), title: Text('screenFriend').tr(),
@ -234,56 +233,52 @@ class _FriendScreenState extends State<FriendScreen> {
if (_requests.isNotEmpty || _blocks.isNotEmpty) if (_requests.isNotEmpty || _blocks.isNotEmpty)
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: MediaQuery.removePadding( child: RefreshIndicator(
context: context, onRefresh: () => Future.wait([
removeTop: true, _fetchRelations(),
child: RefreshIndicator( _fetchRequests(),
onRefresh: () => Future.wait([ ]),
_fetchRelations(), child: ListView.builder(
_fetchRequests(), itemCount: _relations.length,
]), itemBuilder: (context, index) {
child: ListView.builder( final relation = _relations[index];
itemCount: _relations.length, final other = relation.related;
itemBuilder: (context, index) { return ListTile(
final relation = _relations[index]; contentPadding: const EdgeInsets.only(right: 24, left: 16),
final other = relation.related; leading: AccountImage(content: other?.avatar),
return ListTile( title: Text(other?.nick ?? 'unknown'),
contentPadding: const EdgeInsets.only(right: 24, left: 16), subtitle: Text(other?.nick ?? 'unknown'),
leading: AccountImage(content: other?.avatar), trailing: SizedBox(
title: Text(other?.nick ?? 'unknown'), height: 48,
subtitle: Text(other?.nick ?? 'unknown'), width: 120,
trailing: SizedBox( child: Column(
height: 48, mainAxisSize: MainAxisSize.min,
width: 120, mainAxisAlignment: MainAxisAlignment.center,
child: Column( crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min, children: [
mainAxisAlignment: MainAxisAlignment.center, Row(
crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Row( InkWell(
mainAxisAlignment: MainAxisAlignment.end, onTap: _isUpdating
children: [ ? null
InkWell( : () => _changeRelation(relation, 2),
onTap: _isUpdating child: Text('friendBlock').tr(),
? null ),
: () => _changeRelation(relation, 2), const Gap(8),
child: Text('friendBlock').tr(), InkWell(
), onTap: _isUpdating
const Gap(8), ? null
InkWell( : () => _deleteRelation(relation),
onTap: _isUpdating child: Text('friendDeleteAction').tr(),
? null ),
: () => _deleteRelation(relation), ],
child: Text('friendDeleteAction').tr(), ),
), ],
],
),
],
),
), ),
); ),
}, );
), },
), ),
), ),
), ),

View File

@ -25,7 +25,6 @@ import 'package:surface/types/check_in.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.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/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
class HomeScreenDashEntry { class HomeScreenDashEntry {
@ -68,7 +67,7 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenHome").tr(), title: Text("screenHome").tr(),
@ -388,8 +387,6 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
Text( Text(
'dailyCheckInNone', 'dailyCheckInNone',
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
maxLines: 2,
overflow: TextOverflow.ellipsis,
).tr(), ).tr(),
], ],
) )

View File

@ -14,7 +14,6 @@ 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/markdown_content.dart'; import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -138,7 +137,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenNotification').tr(), title: Text('screenNotification').tr(),
@ -149,7 +148,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
); );
} }
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenNotification').tr(), title: Text('screenNotification').tr(),

View File

@ -14,7 +14,6 @@ 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_background.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';
import 'package:surface/widgets/post/post_mini_editor.dart'; import 'package:surface/widgets/post/post_mini_editor.dart';
@ -68,7 +67,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
return AppBackground( return AppBackground(
isRoot: widget.onBack != null, isRoot: widget.onBack != null,
child: AppScaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
leading: BackButton( leading: BackButton(
onPressed: () { onPressed: () {

View File

@ -13,7 +13,6 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart'; import 'package:surface/widgets/post/post_media_pending_list.dart';
import 'package:surface/widgets/post/post_meta_editor.dart'; import 'package:surface/widgets/post/post_meta_editor.dart';
@ -129,7 +128,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
return ListenableBuilder( return ListenableBuilder(
listenable: _writeController, listenable: _writeController,
builder: (context, _) { builder: (context, _) {
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: BackButton( leading: BackButton(
onPressed: () { onPressed: () {

View File

@ -8,7 +8,6 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.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/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_tags_field.dart'; import 'package:surface/widgets/post/post_tags_field.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -120,7 +119,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
), ),
]; ];
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('screenPostSearch').tr(), title: Text('screenPostSearch').tr(),
actions: [ actions: [

View File

@ -17,7 +17,6 @@ import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -275,7 +274,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return Scaffold(
body: NestedScrollView( body: NestedScrollView(
controller: _scrollController, controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {

View File

@ -12,7 +12,6 @@ import 'package:surface/widgets/account/account_image.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_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
@ -84,7 +83,7 @@ class _RealmScreenState extends State<RealmScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenRealm').tr(), title: Text('screenRealm').tr(),
@ -95,7 +94,7 @@ class _RealmScreenState extends State<RealmScreen> {
); );
} }
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenRealm').tr(), title: Text('screenRealm').tr(),
@ -119,61 +118,113 @@ class _RealmScreenState extends State<RealmScreen> {
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
Expanded( Expanded(
child: MediaQuery.removePadding( child: RefreshIndicator(
context: context, onRefresh: _fetchRealms,
removeTop: true, child: ListView.builder(
child: RefreshIndicator( itemCount: _realms?.length ?? 0,
onRefresh: _fetchRealms, itemBuilder: (context, idx) {
child: ListView.builder( final realm = _realms![idx];
itemCount: _realms?.length ?? 0, if (_isCompactView) {
itemBuilder: (context, idx) { return ListTile(
final realm = _realms![idx]; contentPadding: const EdgeInsets.symmetric(horizontal: 16),
if (_isCompactView) { leading: AccountImage(
return ListTile( content: realm.avatar,
contentPadding: const EdgeInsets.symmetric(horizontal: 16), fallbackWidget: const Icon(Symbols.group, size: 20),
leading: AccountImage( ),
content: realm.avatar, title: Text(realm.name),
fallbackWidget: const Icon(Symbols.group, size: 20), subtitle: Text(
), realm.description,
title: Text(realm.name), maxLines: 1,
subtitle: Text( overflow: TextOverflow.ellipsis,
realm.description, ),
maxLines: 1, trailing: PopupMenuButton(
overflow: TextOverflow.ellipsis, itemBuilder: (BuildContext context) => [
), PopupMenuItem(
trailing: PopupMenuButton( child: Row(
itemBuilder: (BuildContext context) => [ children: [
PopupMenuItem( const Icon(Symbols.edit),
child: Row( const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': realm.alias},
).then((value) {
if (value != null) {
_fetchRealms();
}
});
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
);
},
);
}
return Container(
constraints: BoxConstraints(maxWidth: 640),
child: Card(
margin: const EdgeInsets.all(12),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [ children: [
const Icon(Symbols.edit), Container(
const Gap(16), color: Theme.of(context).colorScheme.surfaceContainer,
Text('edit').tr(), child: (realm.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
], ],
), ),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': realm.alias},
).then((value) {
if (value != null) {
_fetchRealms();
}
});
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
), ),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
], ],
), ),
onTap: () { onTap: () {
@ -182,69 +233,10 @@ class _RealmScreenState extends State<RealmScreen> {
pathParameters: {'alias': realm.alias}, pathParameters: {'alias': realm.alias},
); );
}, },
);
}
return Container(
constraints: BoxConstraints(maxWidth: 640),
child: Card(
margin: const EdgeInsets.all(12),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: (realm.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
],
),
),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
);
},
),
), ),
).center(); ),
}, ).center();
), },
), ),
), ),
), ),

View File

@ -18,7 +18,6 @@ import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.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_scaffold.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -180,7 +179,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: widget.editingRealmAlias != null title: widget.editingRealmAlias != null
? Text('screenRealmManage').tr() ? Text('screenRealmManage').tr()

View File

@ -8,13 +8,13 @@ import 'package:styled_widget/styled_widget.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/types/post.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../../types/post.dart';
class RealmDetailScreen extends StatefulWidget { class RealmDetailScreen extends StatefulWidget {
final String alias; final String alias;
@ -70,11 +70,19 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DefaultTabController( return DefaultTabController(
length: 3, length: 3,
child: AppScaffold( child: Scaffold(
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[ return <Widget>[
SliverOverlapAbsorber( SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar( sliver: SliverAppBar(
title: Text(_realm?.name ?? 'loading'.tr()), title: Text(_realm?.name ?? 'loading'.tr()),
@ -420,7 +428,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
return Column( return Column(
children: [ children: [
const Gap(8), const Gap(16),
ListTile( ListTile(
leading: const Icon(Symbols.edit), leading: const Icon(Symbols.edit),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),

View File

@ -18,7 +18,6 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/theme.dart'; import 'package:surface/providers/theme.dart';
import 'package:surface/theme.dart'; import 'package:surface/theme.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
const Map<String, Color> kColorSchemes = { const Map<String, Color> kColorSchemes = {
'colorSchemeIndigo': Colors.indigo, 'colorSchemeIndigo': Colors.indigo,
@ -68,11 +67,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return Scaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenSettings').tr(),
),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
spacing: 16, spacing: 16,
@ -260,24 +255,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
], ],
), ),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
CheckboxListTile(
secondary: const Icon(Symbols.vibration),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
title: Text('settingsNotifyWithHaptic').tr(),
subtitle: Text('settingsNotifyWithHapticDescription').tr(),
value: _prefs.getBool(kAppNotifyWithHaptic) ?? true,
onChanged: (value) {
setState(() {
_prefs.setBool(kAppNotifyWithHaptic, value ?? false);
});
},
),
],
),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@ -20,7 +20,7 @@ Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3
Future<ThemeData> createAppTheme( Future<ThemeData> createAppTheme(
Brightness brightness, { Brightness brightness, {
Color? seedColorOverride, Color? seedColorOverride,
bool? useMaterial3, bool? useMaterial3,
}) async { }) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -34,10 +34,9 @@ Future<ThemeData> createAppTheme(
); );
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
return ThemeData( return ThemeData(
useMaterial3: useM3, useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true),
colorScheme: colorScheme, colorScheme: colorScheme,
brightness: brightness, brightness: brightness,
iconTheme: IconThemeData( iconTheme: IconThemeData(
@ -46,19 +45,17 @@ Future<ThemeData> createAppTheme(
opticalSize: 20, opticalSize: 20,
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
snackBarTheme: SnackBarThemeData(
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
),
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
centerTitle: true, centerTitle: true,
elevation: hasAppBarBlurry ? 0 : null, elevation: hasAppBarBlurry ? 0 : null,
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary, backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
), ),
scaffoldBackgroundColor: Colors.transparent,
pageTransitionsTheme: PageTransitionsTheme( pageTransitionsTheme: PageTransitionsTheme(
builders: { builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
TargetPlatform.macOS: ZoomPageTransitionsBuilder(), TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(), TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
TargetPlatform.linux: ZoomPageTransitionsBuilder(), TargetPlatform.linux: ZoomPageTransitionsBuilder(),

View File

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class AboutScreen extends StatelessWidget { class AboutScreen extends StatelessWidget {
@ -13,103 +12,97 @@ class AboutScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4)); const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
return AppScaffold( return SizedBox(
appBar: AppBar( width: double.infinity,
leading: const PageBackButton(), child: Column(
title: Text('screenAbout').tr(), mainAxisAlignment: MainAxisAlignment.center,
), children: [
body: SizedBox( ClipRRect(
width: double.infinity, borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Column( child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
mainAxisAlignment: MainAxisAlignment.center, ),
children: [ const Gap(8),
ClipRRect( Text(
borderRadius: const BorderRadius.all(Radius.circular(16)), 'Solian',
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120), style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
), ),
const Gap(8), const Text(
Text( 'The Solar Network',
'Solian', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36), ),
), const Gap(8),
const Text( FutureBuilder(
'The Solar Network', future: PackageInfo.fromPlatform(),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), builder: (context, snapshot) {
), if (!snapshot.hasData) {
const Gap(8), return const SizedBox.shrink();
FutureBuilder( }
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
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),
Container( Container(
constraints: const BoxConstraints(maxWidth: 280), constraints: const BoxConstraints(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();
if (!context.mounted) return; if (!context.mounted) return;
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationName: 'Solian', applicationName: 'Solian',
applicationVersion: '${info.version}+${info.buildNumber}', applicationVersion: '${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: const BorderRadius.all(Radius.circular(16)), borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Image.asset( child: Image.asset(
'assets/icon/icon-light-radius.png', 'assets/icon/icon-light-radius.png',
width: 60, width: 60,
height: 60, height: 60,
),
), ),
); ),
}, );
), },
TextButton( ),
style: denseButtonStyle, TextButton(
child: Text('termRelated').tr(), style: denseButtonStyle,
onPressed: () { child: Text('termRelated').tr(),
launchUrlString('https://solsynth.dev/terms'); onPressed: () {
}, launchUrlString('https://solsynth.dev/terms');
), },
TextButton( ),
style: denseButtonStyle, TextButton(
child: Text('serviceStatus').tr(), style: denseButtonStyle,
onPressed: () { child: Text('serviceStatus').tr(),
launchUrlString('https://status.solsynth.dev'); onPressed: () {
}, launchUrlString('https://status.solsynth.dev');
), },
], ),
), ],
).center(),
const Gap(16),
const Text(
'Open-sourced under AGPLv3',
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 12,
),
), ),
], ).center(),
), const Gap(16),
const Text(
'Open-sourced under AGPLv3',
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 12,
),
),
],
), ),
); );
} }

View File

@ -15,7 +15,6 @@ class AttachmentList extends StatefulWidget {
final List<SnAttachment?> data; final List<SnAttachment?> data;
final bool bordered; final bool bordered;
final bool gridded; final bool gridded;
final bool columned;
final BoxFit fit; final BoxFit fit;
final double? maxHeight; final double? maxHeight;
final double? minWidth; final double? minWidth;
@ -27,7 +26,6 @@ class AttachmentList extends StatefulWidget {
required this.data, required this.data,
this.bordered = false, this.bordered = false,
this.gridded = false, this.gridded = false,
this.columned = false,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.maxHeight, this.maxHeight,
this.minWidth, this.minWidth,
@ -107,10 +105,45 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
final fullOfImage = if (widget.gridded) {
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length; final fullOfImage =
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
if (widget.gridded && fullOfImage) { if(!fullOfImage) {
return Container(
margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: Column(
spacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: AspectRatio(
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
),
),
),
),
)
.toList(),
),
),
);
}
return Container( return Container(
margin: widget.padding ?? EdgeInsets.zero, margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -158,44 +191,6 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
if ((!fullOfImage && widget.gridded) || widget.columned) {
return Container(
margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: Column(
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: AspectRatio(
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
),
),
),
),
)
.expand((ele) => [ele, const Divider(height: 1)])
.toList()
..removeLast(),
),
),
);
}
return Container( return Container(
constraints: BoxConstraints(maxHeight: constraints.maxHeight), constraints: BoxConstraints(maxHeight: constraints.maxHeight),
child: ScrollConfiguration( child: ScrollConfiguration(

View File

@ -365,7 +365,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
), ),
onVerticalDragUpdate: (details) { onVerticalDragUpdate: (details) {
if (_showDetail) return; if (_showDetail) return;
if (details.delta.dy <= -40) { if (details.delta.dy < 0) {
_showDetail = true; _showDetail = true;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -415,79 +415,77 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
], ],
).padding(horizontal: 20, top: 16, bottom: 12), ).padding(horizontal: 20, top: 16, bottom: 12),
Expanded( Expanded(
child: SingleChildScrollView( child: Table(
child: Table( columnWidths: {
columnWidths: { 0: IntrinsicColumnWidth(),
0: IntrinsicColumnWidth(), 1: FlexColumnWidth(),
1: FlexColumnWidth(), },
}, children: [
children: [ TableRow(
TableRow( children: [
children: [ TableCell(
TableCell( child: Text('attachmentUploadBy').tr().padding(right: 16),
child: Text('attachmentUploadBy').tr().padding(right: 16),
),
TableCell(
child: Row(
children: [
if (data.accountId > 0)
AccountImage(
content: account?.avatar,
radius: 8,
),
const Gap(8),
Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()),
const Gap(8),
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
],
),
),
],
),
tableGap,
TableRow(
children: [
TableCell(child: Text('Mimetype').padding(right: 16)),
TableCell(child: Text(data.mimetype)),
],
),
TableRow(
children: [
TableCell(child: Text('Size').padding(right: 16)),
TableCell(
child: Row(
children: [
Text(data.size.formatBytes()),
const Gap(12),
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
],
)),
],
),
TableRow(
children: [
TableCell(child: Text('Name').padding(right: 16)),
TableCell(child: Text(data.name)),
],
),
if (data.hash.isNotEmpty)
TableRow(
children: [
TableCell(child: Text('Hash').padding(right: 16)),
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
],
), ),
tableGap, TableCell(
...(data.metadata['exif']?.keys.map((k) => TableRow( child: Row(
children: [ children: [
TableCell(child: Text(k).padding(right: 16)), if (data.accountId > 0)
TableCell(child: Text(data.metadata['exif'][k].toString())), AccountImage(
content: account?.avatar,
radius: 8,
),
const Gap(8),
Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()),
const Gap(8),
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
], ],
)) ?? ),
[]), ),
], ],
).padding(horizontal: 20, vertical: 8), ),
), tableGap,
TableRow(
children: [
TableCell(child: Text('Mimetype').padding(right: 16)),
TableCell(child: Text(data.mimetype)),
],
),
TableRow(
children: [
TableCell(child: Text('Size').padding(right: 16)),
TableCell(
child: Row(
children: [
Text(data.size.formatBytes()),
const Gap(12),
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
],
)),
],
),
TableRow(
children: [
TableCell(child: Text('Name').padding(right: 16)),
TableCell(child: Text(data.name)),
],
),
if (data.hash.isNotEmpty)
TableRow(
children: [
TableCell(child: Text('Hash').padding(right: 16)),
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
],
),
tableGap,
...(data.metadata['exif']?.keys.map((k) => TableRow(
children: [
TableCell(child: Text(k).padding(right: 16)),
TableCell(child: Text(data.metadata['exif'][k].toString())),
],
)) ??
[]),
],
).padding(horizontal: 20, vertical: 8),
), ),
], ],
), ),

View File

@ -18,49 +18,45 @@ class ConnectionIndicator extends StatelessWidget {
listenable: ws, listenable: ws,
builder: (context, _) { builder: (context, _) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized;
return IgnorePointer( return GestureDetector(
ignoring: !show, child: Material(
child: GestureDetector( elevation: 2,
child: Material( shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
elevation: 2, color: Theme.of(context).colorScheme.secondaryContainer,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), child: ua.isAuthorized
color: Theme.of(context).colorScheme.secondaryContainer, ? Row(
child: ua.isAuthorized mainAxisAlignment: MainAxisAlignment.center,
? Row( crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
crossAxisAlignment: CrossAxisAlignment.center, if (ws.isBusy)
children: [ Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
if (ws.isBusy) else if (!ws.isConnected)
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
else if (!ws.isConnected) else
Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
else const Gap(8),
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), if (ws.isBusy)
const Gap(8), const CircularProgressIndicator(strokeWidth: 2.5)
if (ws.isBusy) .width(12)
const CircularProgressIndicator(strokeWidth: 2.5) .height(12)
.width(12) .padding(horizontal: 4, right: 4)
.height(12) else if (!ws.isConnected)
.padding(horizontal: 4, right: 4) const Icon(Symbols.power_off, size: 18)
else if (!ws.isConnected) else
const Icon(Symbols.power_off, size: 18) const Icon(Symbols.power, size: 18),
else ],
const Icon(Symbols.power, size: 18), ).padding(horizontal: 8, vertical: 4)
], : const SizedBox.shrink(),
).padding(horizontal: 8, vertical: 4) ).opacity((ws.isBusy || !ws.isConnected) && ua.isAuthorized ? 1 : 0, animate: true).animate(
: const SizedBox.shrink(), const Duration(milliseconds: 300),
).opacity(show ? 1 : 0, animate: true).animate( Curves.easeInOut,
const Duration(milliseconds: 300), ),
Curves.easeInOut, onTap: () {
), if (!ws.isConnected && !ws.isBusy) {
onTap: () { ws.connect();
if (!ws.isConnected && !ws.isBusy) { }
ws.connect(); },
}
},
),
); );
}, },
); );

View File

@ -94,14 +94,11 @@ class _LinkPreviewEntry extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (meta.icon?.isNotEmpty ?? false) if (meta.icon?.isNotEmpty ?? false)
SizedBox( StyledWidget(
width: 36, meta.icon!.endsWith('.svg')
height: 36, ? SvgPicture.network(meta.icon!)
child: meta.icon!.endsWith('.svg')
? SvgPicture.network(meta.icon!, width: 36, height: 36)
: UniversalImage( : UniversalImage(
meta.icon!, meta.icon!,
noErrorWidget: true,
width: 36, width: 36,
height: 36, height: 36,
cacheHeight: 36, cacheHeight: 36,

View File

@ -21,76 +21,34 @@ import 'package:surface/widgets/notify_indicator.dart';
final globalRootScaffoldKey = GlobalKey<ScaffoldState>(); final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
class AppScaffold extends StatelessWidget { class AppPageScaffold extends StatelessWidget {
final String? title;
final Widget? body; final Widget? body;
final PreferredSizeWidget? bottomNavigationBar; final bool showAppBar;
final PreferredSizeWidget? bottomSheet; final bool showBottomNavigation;
final Drawer? drawer;
final Widget? endDrawer;
final FloatingActionButtonAnimator? floatingActionButtonAnimator;
final FloatingActionButtonLocation? floatingActionButtonLocation;
final Widget? floatingActionButton;
final AppBar? appBar;
final DrawerCallback? onDrawerChanged;
final DrawerCallback? onEndDrawerChanged;
const AppScaffold({ const AppPageScaffold({
super.key, super.key,
this.appBar, this.title,
this.body, this.body,
this.floatingActionButton, this.showAppBar = true,
this.floatingActionButtonLocation, this.showBottomNavigation = false,
this.floatingActionButtonAnimator,
this.bottomNavigationBar,
this.bottomSheet,
this.drawer,
this.endDrawer,
this.onDrawerChanged,
this.onEndDrawerChanged,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appBarHeight = appBar?.preferredSize.height ?? 0; final state = GoRouter.maybeOf(context);
final safeTop = MediaQuery.of(context).padding.top; final routeName = state?.routerDelegate.currentConfiguration.last.route.name;
final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen';
return Scaffold( return Scaffold(
extendBody: true, appBar: showAppBar
extendBodyBehindAppBar: true, ? AppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, title: Text(title ?? autoTitle.tr()),
body: SizedBox.expand( )
child: AppBackground( : null,
child: Column( body: body,
children: [
IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)),
if (body != null) Expanded(child: body!),
],
),
),
),
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
drawer: drawer,
endDrawer: endDrawer,
floatingActionButton: floatingActionButton,
floatingActionButtonAnimator: floatingActionButtonAnimator,
floatingActionButtonLocation: floatingActionButtonLocation,
onDrawerChanged: onDrawerChanged,
onEndDrawerChanged: onEndDrawerChanged,
);
}
}
class PageBackButton extends StatelessWidget {
const PageBackButton({super.key});
@override
Widget build(BuildContext context) {
return BackButton(
onPressed: () {
GoRouter.of(context).pop();
},
); );
} }
} }
@ -143,16 +101,16 @@ class AppRootScaffold extends StatelessWidget {
final safeTop = MediaQuery.of(context).padding.top; final safeTop = MediaQuery.of(context).padding.top;
return Scaffold( return AppBackground(
key: globalRootScaffoldKey, isRoot: true,
backgroundColor: Theme.of(context).colorScheme.surface, child: Scaffold(
body: Stack( key: globalRootScaffoldKey,
children: [ body: Stack(
Column( children: [
children: [ Column(
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) children: [
WindowTitleBarBox( if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
child: Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
@ -161,44 +119,49 @@ class AppRootScaffold extends StatelessWidget {
), ),
), ),
), ),
child: MoveWindow( child: Row(
child: Row( crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, children: [
children: [ WindowTitleBarBox(
Text( child: MoveWindow(
'Solar Network', child: Text(
style: GoogleFonts.spaceGrotesk(), 'Solar Network',
).padding(horizontal: 12, vertical: 5), style: GoogleFonts.spaceGrotesk(),
if (!Platform.isMacOS) ).padding(horizontal: 12, vertical: 5),
Row( ),
mainAxisSize: MainAxisSize.min, ),
children: [ if (!Platform.isMacOS)
Expanded(child: MoveWindow()), Expanded(
Row( child: WindowTitleBarBox(
children: [ child: Row(
MinimizeWindowButton(colors: windowButtonColor), children: [
MaximizeWindowButton(colors: windowButtonColor), Expanded(child: MoveWindow()),
CloseWindowButton(colors: windowButtonColor), Row(
], children: [
), MinimizeWindowButton(colors: windowButtonColor),
], MaximizeWindowButton(colors: windowButtonColor),
CloseWindowButton(colors: windowButtonColor),
],
),
],
),
), ),
], ),
), ],
), ),
), ),
), Expanded(child: innerWidget),
Expanded(child: innerWidget), ],
], ),
), Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()), Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()), ],
], ),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
), ),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
); );
} }
} }

View File

@ -15,48 +15,43 @@ class NotifyIndicator extends StatelessWidget {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final nty = context.watch<NotificationProvider>(); final nty = context.watch<NotificationProvider>();
final show = nty.notifications.isNotEmpty && ua.isAuthorized;
return ListenableBuilder( return ListenableBuilder(
listenable: nty, listenable: nty,
builder: (context, _) { builder: (context, _) {
return IgnorePointer( return GestureDetector(
ignoring: !show, child: Material(
child: GestureDetector( elevation: 2,
child: Material( shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
elevation: 2, color: Theme.of(context).colorScheme.secondaryContainer,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), child: ua.isAuthorized
color: Theme.of(context).colorScheme.secondaryContainer, ? Row(
child: ua.isAuthorized mainAxisAlignment: MainAxisAlignment.center,
? Row( crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
crossAxisAlignment: CrossAxisAlignment.center, Text(
children: [ nty.notifications.lastOrNull?.title ??
'notificationUnreadCount'.plural(nty.notifications.length),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (nty.notifications.lastOrNull?.body != null)
Text( Text(
nty.notifications.lastOrNull?.title ?? nty.notifications.lastOrNull!.body,
'notificationUnreadCount'.plural(nty.notifications.length),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ).padding(left: 4),
if (nty.notifications.lastOrNull?.body != null) const Gap(8),
Text( const Icon(Symbols.notifications_unread, size: 18),
nty.notifications.lastOrNull!.body, ],
maxLines: 1, ).padding(horizontal: 8, vertical: 4)
overflow: TextOverflow.ellipsis, : const SizedBox.shrink(),
).padding(left: 4), ).opacity(nty.notifications.isNotEmpty && ua.isAuthorized ? 1 : 0, animate: true).animate(
const Gap(8), const Duration(milliseconds: 300),
const Icon(Symbols.notifications_unread, size: 18), Curves.easeInOut,
], ),
).padding(horizontal: 8, vertical: 4) onTap: () {
: const SizedBox.shrink(), nty.clear();
).opacity(show ? 1 : 0, animate: true).animate( },
const Duration(milliseconds: 300),
Curves.easeInOut,
),
onTap: () {
nty.clear();
},
),
); );
}); });
} }

View File

@ -256,8 +256,9 @@ class PostItem extends StatelessWidget {
AttachmentList( AttachmentList(
data: displayableAttachments!, data: displayableAttachments!,
bordered: true, bordered: true,
gridded: true,
maxHeight: showFullPost ? null : 480, maxHeight: showFullPost ? null : 480,
maxWidth: MediaQuery.of(context).size.width - 20, minWidth: 640,
fit: showFullPost ? BoxFit.cover : BoxFit.contain, fit: showFullPost ? BoxFit.cover : BoxFit.contain,
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
), ),
@ -343,7 +344,7 @@ class PostShareImageWidget extends StatelessWidget {
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
StyledWidget(AttachmentList( StyledWidget(AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
columned: true, gridded: true,
)).padding(horizontal: 16, bottom: 8), )).padding(horizontal: 16, bottom: 8),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -1,6 +1,5 @@
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:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.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';
@ -13,7 +12,6 @@ import 'package:surface/widgets/dialog.dart';
class PostReactionPopup extends StatefulWidget { class PostReactionPopup extends StatefulWidget {
final SnPost data; final SnPost data;
final Function(Map<String, int> value, int attr, int delta)? onChanged; final Function(Map<String, int> value, int attr, int delta)? onChanged;
const PostReactionPopup({super.key, required this.data, this.onChanged}); const PostReactionPopup({super.key, required this.data, this.onChanged});
@override @override
@ -61,7 +59,6 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
); );
} }
} }
HapticFeedback.mediumImpact();
} catch (err) { } catch (err) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err); if (context.mounted) context.showErrorDialog(err);
@ -87,7 +84,9 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
children: [ children: [
const Icon(Symbols.mood, size: 24), const Icon(Symbols.mood, size: 24),
const Gap(16), const Gap(16),
Text('postReactions').tr().textStyle(Theme.of(context).textTheme.titleLarge!), Text('postReactions')
.tr()
.textStyle(Theme.of(context).textTheme.titleLarge!),
], ],
).padding(horizontal: 20, top: 16, bottom: 12), ).padding(horizontal: 20, top: 16, bottom: 12),
Container( Container(
@ -103,7 +102,9 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
Text('postReactionDownvote').plural(widget.data.totalDownvote), Text('postReactionDownvote').plural(widget.data.totalDownvote),
const Gap(24), const Gap(24),
Icon( Icon(
widget.data.totalUpvote >= widget.data.totalDownvote ? Symbols.trending_up : Symbols.trending_down, widget.data.totalUpvote >= widget.data.totalDownvote
? Symbols.trending_up
: Symbols.trending_down,
size: 16, size: 16,
), ),
const Gap(8), const Gap(8),

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.2.2+55 version: 2.2.2+54
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4