Compare commits
9 Commits
19076f8136
...
87029e3538
Author | SHA1 | Date | |
---|---|---|---|
87029e3538 | |||
127d9adc09 | |||
c82dc7ad85 | |||
36bcff7a7c | |||
38201b547a | |||
ed0334fcda | |||
fbb486b90b | |||
9b34f385d5 | |||
bb7b731602 |
@ -193,6 +193,9 @@
|
||||
"settingsColorSchemeDescription": "Set the application primary color.",
|
||||
"settingsColorSeed": "Color Seed",
|
||||
"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",
|
||||
"settingsNetworkServer": "HyperNet Server",
|
||||
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
||||
|
@ -191,6 +191,9 @@
|
||||
"settingsColorSchemeDescription": "设置应用主题色。",
|
||||
"settingsColorSeed": "预设色彩主题",
|
||||
"settingsColorSeedDescription": "选择一个预设色彩主题。",
|
||||
"settingsFeatures": "功能",
|
||||
"settingsNotifyWithHaptic": "新通知时振动",
|
||||
"settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
|
||||
"settingsNetwork": "网络",
|
||||
"settingsNetworkServer": "HyperNet 服务器",
|
||||
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
||||
|
@ -191,6 +191,9 @@
|
||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||
"settingsColorSeed": "預設色彩主題",
|
||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||
"settingsFeatures": "功能",
|
||||
"settingsNotifyWithHaptic": "新通知時振動",
|
||||
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
|
||||
"settingsNetwork": "網絡",
|
||||
"settingsNetworkServer": "HyperNet 服務器",
|
||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||
@ -213,8 +216,9 @@
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
"sensitiveContentReveal": "顯示內容",
|
||||
"serverConnecting": "正在連接服務器…",
|
||||
"serverDisconnected": "已與服務器斷開連接",
|
||||
"serverConnecting": "正在連接…",
|
||||
"serverDisconnected": "已斷開連接",
|
||||
"serverConnected": "已連接",
|
||||
"fieldChatAlias": "頻道別名",
|
||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||
"fieldChatName": "名稱",
|
||||
@ -292,6 +296,7 @@
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||
"attachmentDetailInfo": "附件詳細信息",
|
||||
"attachmentPastedImage": "粘貼的圖片",
|
||||
"attachmentInsertLink": "插入連接",
|
||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||
|
@ -191,6 +191,9 @@
|
||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||
"settingsColorSeed": "預設色彩主題",
|
||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||
"settingsFeatures": "功能",
|
||||
"settingsNotifyWithHaptic": "新通知時振動",
|
||||
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
|
||||
"settingsNetwork": "網絡",
|
||||
"settingsNetworkServer": "HyperNet 服務器",
|
||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||
@ -213,8 +216,9 @@
|
||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||
"sensitiveContentReveal": "顯示內容",
|
||||
"serverConnecting": "正在連接服務器…",
|
||||
"serverDisconnected": "已與服務器斷開連接",
|
||||
"serverConnecting": "正在連接…",
|
||||
"serverDisconnected": "已斷開連接",
|
||||
"serverConnected": "已連接",
|
||||
"fieldChatAlias": "頻道別名",
|
||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||
"fieldChatName": "名稱",
|
||||
@ -292,6 +296,7 @@
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||
"attachmentDetailInfo": "附件詳細信息",
|
||||
"attachmentPastedImage": "粘貼的圖片",
|
||||
"attachmentInsertLink": "插入連接",
|
||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||
|
@ -14,6 +14,7 @@ const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
||||
const kAppBackgroundStoreKey = 'app_has_background';
|
||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
||||
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
|
||||
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
||||
|
||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||
'settingsImageQualityLowest': FilterQuality.none,
|
||||
|
@ -4,8 +4,10 @@ import 'dart:io';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
@ -15,11 +17,13 @@ class NotificationProvider extends ChangeNotifier {
|
||||
late final SnNetworkProvider _sn;
|
||||
late final UserProvider _ua;
|
||||
late final WebSocketProvider _ws;
|
||||
late final ConfigProvider _cfg;
|
||||
|
||||
NotificationProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_ua = context.read<UserProvider>();
|
||||
_ws = context.read<WebSocketProvider>();
|
||||
_cfg = context.read<ConfigProvider>();
|
||||
}
|
||||
|
||||
Future<void> registerPushNotifications() async {
|
||||
@ -75,6 +79,8 @@ class NotificationProvider extends ChangeNotifier {
|
||||
final notification = SnNotification.fromJson(event.payload!);
|
||||
notifications.add(notification);
|
||||
notifyListeners();
|
||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||
if (doHaptic) HapticFeedback.lightImpact();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
113
lib/router.dart
113
lib/router.dart
@ -36,10 +36,7 @@ import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
final _appRoutes = [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(
|
||||
body: child,
|
||||
showAppBar: false,
|
||||
),
|
||||
builder: (context, state, child) => child,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
@ -58,47 +55,39 @@ final _appRoutes = [
|
||||
GoRoute(
|
||||
path: '/write/:mode',
|
||||
name: 'postEditor',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: PostEditorScreen(
|
||||
mode: state.pathParameters['mode']!,
|
||||
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?,
|
||||
builder: (context, state) => PostEditorScreen(
|
||||
mode: state.pathParameters['mode']!,
|
||||
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?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/search',
|
||||
name: 'postSearch',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: PostSearchScreen(
|
||||
initialTags: state.uri.queryParameters['tags']?.split(','),
|
||||
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
||||
),
|
||||
builder: (context, state) => PostSearchScreen(
|
||||
initialTags: state.uri.queryParameters['tags']?.split(','),
|
||||
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/:name',
|
||||
name: 'postPublisher',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: PostPublisherScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:slug',
|
||||
name: 'postDetail',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: PostDetailScreen(
|
||||
slug: state.pathParameters['slug']!,
|
||||
preload: state.extra as SnPost?,
|
||||
),
|
||||
builder: (context, state) => PostDetailScreen(
|
||||
slug: state.pathParameters['slug']!,
|
||||
preload: state.extra as SnPost?,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -106,7 +95,15 @@ final _appRoutes = [
|
||||
GoRoute(
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: const AccountScreen(),
|
||||
),
|
||||
routes: [],
|
||||
@ -114,7 +111,15 @@ final _appRoutes = [
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: const ChatScreen(),
|
||||
),
|
||||
routes: [
|
||||
@ -228,57 +233,43 @@ final _appRoutes = [
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
builder: (context, state, child) => child,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/auth/login',
|
||||
name: 'authLogin',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: LoginScreen(),
|
||||
),
|
||||
builder: (context, state) => LoginScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/register',
|
||||
name: 'authRegister',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: RegisterScreen(),
|
||||
),
|
||||
builder: (context, state) => RegisterScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/reports',
|
||||
name: 'abuseReport',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AbuseReportScreen(),
|
||||
),
|
||||
builder: (context, state) => AbuseReportScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: ProfileEditScreen(),
|
||||
),
|
||||
builder: (context, state) => ProfileEditScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: PublisherScreen(),
|
||||
),
|
||||
builder: (context, state) => PublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AccountPublisherNewScreen(),
|
||||
),
|
||||
builder: (context, state) => AccountPublisherNewScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AppBackground(
|
||||
child: AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
builder: (context, state) => AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -291,26 +282,22 @@ final _appRoutes = [
|
||||
),
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
builder: (context, state, child) => child,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: SettingsScreen(),
|
||||
),
|
||||
builder: (context, state) => SettingsScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppPageScaffold(body: child),
|
||||
builder: (context, state, child) => child,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
builder: (context, state) => const AppBackground(
|
||||
child: AboutScreen(),
|
||||
),
|
||||
builder: (context, state) => AboutScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
import '../types/account.dart';
|
||||
|
||||
@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAbuseReport').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
itemCount: _reports.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return ListTile(
|
||||
|
@ -12,6 +12,7 @@ import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class AccountScreen extends StatelessWidget {
|
||||
const AccountScreen({super.key});
|
||||
@ -20,7 +21,7 @@ class AccountScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text("screenAccount").tr(),
|
||||
|
@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class ProfileEditScreen extends StatefulWidget {
|
||||
@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
setState(() {
|
||||
_birthday = newDate;
|
||||
_birthdayController.text =
|
||||
DateFormat(_kDateFormat).format(_birthday!);
|
||||
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
if (image == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final ImageProvider imageProvider =
|
||||
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios = place == 'banner'
|
||||
? [CropAspectRatio(width: 16, height: 7)]
|
||||
: [CropAspectRatio(width: 1, height: 1)];
|
||||
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios =
|
||||
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
|
||||
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 {
|
||||
final attachment = await attach.directUploadOne(
|
||||
@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Material(
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(_banner!),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountProfileEdit').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Material(
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? AutoResizeUniversalImage(
|
||||
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: () {
|
||||
_updateImage('avatar');
|
||||
_updateImage('banner');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
const Gap(8 + 28),
|
||||
Column(
|
||||
children: [
|
||||
TextField(
|
||||
readOnly: true,
|
||||
controller: _usernameController,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
controller: _nicknameController,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldNickname'.tr(),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _firstNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldFirstName'.tr(),
|
||||
),
|
||||
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: () {
|
||||
_updateImage('avatar');
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _lastNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldLastName'.tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
const Gap(8 + 28),
|
||||
Column(
|
||||
children: [
|
||||
TextField(
|
||||
readOnly: true,
|
||||
controller: _usernameController,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
controller: _nicknameController,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldNickname'.tr(),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _firstNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldFirstName'.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _lastNameController,
|
||||
decoration: InputDecoration(
|
||||
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(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldBirthday'.tr(),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldBirthday'.tr(),
|
||||
),
|
||||
onTap: () => _selectBirthday(),
|
||||
),
|
||||
onTap: () => _selectBirthday(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding + 8),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isBusy ? null : _updateUserInfo,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text('apply').tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
],
|
||||
],
|
||||
).padding(horizontal: padding + 8),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isBusy ? null : _updateUserInfo,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text('apply').tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: padding),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import 'package:surface/types/check_in.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
const Map<String, (String, IconData, Color)> kBadgesMeta = {
|
||||
@ -241,6 +242,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
|
@ -18,6 +18,7 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountPublisherEditScreen extends StatefulWidget {
|
||||
@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
|
@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class AccountPublisherNewScreen extends StatefulWidget {
|
||||
const AccountPublisherNewScreen({super.key});
|
||||
@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublisherNew').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
|
@ -10,6 +10,7 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class PublisherScreen extends StatefulWidget {
|
||||
const PublisherScreen({super.key});
|
||||
@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
|
||||
try {
|
||||
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;
|
||||
|
||||
@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublishers').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.add_circle),
|
||||
onTap: () {
|
||||
GoRouter.of(context)
|
||||
.pushNamed('accountPublisherNew')
|
||||
.then((value) {
|
||||
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
||||
if (value == true) {
|
||||
_publishers.clear();
|
||||
_fetchPublishers();
|
||||
@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
const Divider(height: 1),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_publishers.clear();
|
||||
return _fetchPublishers();
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: _publishers.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final publisher = _publishers[idx];
|
||||
return ListTile(
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(content: publisher.avatar),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
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();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_publishers.clear();
|
||||
return _fetchPublishers();
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: _publishers.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final publisher = _publishers[idx];
|
||||
return ListTile(
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(content: publisher.avatar),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
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();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -11,6 +11,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AlbumScreen extends StatefulWidget {
|
||||
@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
|
@ -9,6 +9,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/auth.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../../providers/websocket.dart';
|
||||
@ -35,67 +36,73 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: SingleChildScrollView(
|
||||
child: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 380),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: switch (_period % 3) {
|
||||
1 => _LoginPickerScreen(
|
||||
key: const ValueKey(1),
|
||||
ticket: _currentTicket,
|
||||
factors: _factors,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onPickFactor: (p0) => setState(() {
|
||||
_factorPicked = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
2 => _LoginCheckScreen(
|
||||
key: const ValueKey(2),
|
||||
ticket: _currentTicket,
|
||||
factor: _factorPicked,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period = 1;
|
||||
}),
|
||||
),
|
||||
_ => _LoginLookupScreen(
|
||||
key: const ValueKey(0),
|
||||
ticket: _currentTicket,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onFactor: (p0) => setState(() {
|
||||
_factors = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
},
|
||||
).padding(all: 24),
|
||||
).center(),
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAuthLogin').tr(),
|
||||
),
|
||||
body: Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: SingleChildScrollView(
|
||||
child: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 380),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: switch (_period % 3) {
|
||||
1 => _LoginPickerScreen(
|
||||
key: const ValueKey(1),
|
||||
ticket: _currentTicket,
|
||||
factors: _factors,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onPickFactor: (p0) => setState(() {
|
||||
_factorPicked = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
2 => _LoginCheckScreen(
|
||||
key: const ValueKey(2),
|
||||
ticket: _currentTicket,
|
||||
factor: _factorPicked,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period = 1;
|
||||
}),
|
||||
),
|
||||
_ => _LoginLookupScreen(
|
||||
key: const ValueKey(0),
|
||||
ticket: _currentTicket,
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onFactor: (p0) => setState(() {
|
||||
_factors = p0;
|
||||
}),
|
||||
onNext: () => setState(() {
|
||||
_period++;
|
||||
}),
|
||||
),
|
||||
},
|
||||
).padding(all: 24),
|
||||
).center(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -441,7 +448,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||
|
||||
widget.onNext();
|
||||
} catch (err) {
|
||||
if(mounted) context.showErrorDialog(err);
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
return;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
|
@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class RegisterScreen extends StatefulWidget {
|
||||
@ -54,175 +55,178 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StyledWidget(Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
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,
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAuthRegister').tr(),
|
||||
),
|
||||
body: StyledWidget(Container(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: CircleAvatar(
|
||||
radius: 26,
|
||||
child: const Icon(
|
||||
Symbols.person_add,
|
||||
size: 28,
|
||||
),
|
||||
).padding(bottom: 8),
|
||||
),
|
||||
).tr().padding(left: 4, bottom: 16),
|
||||
Form(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
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),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: StyledWidget(
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 290),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'termAcceptNextWithAgree'.tr(),
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha((255 * 0.75).round()),
|
||||
),
|
||||
Text(
|
||||
'screenAuthRegister',
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
).tr().padding(left: 4, bottom: 16),
|
||||
Form(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
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(),
|
||||
),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('termAcceptLink'.tr()),
|
||||
const Gap(4),
|
||||
const Icon(Symbols.launch, size: 14),
|
||||
],
|
||||
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),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: StyledWidget(
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 290),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'termAcceptNextWithAgree'.tr(),
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.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');
|
||||
},
|
||||
),
|
||||
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(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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import 'package:surface/widgets/account/account_select.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.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:uuid/uuid.dart';
|
||||
|
||||
@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenChat').tr(),
|
||||
@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenChat').tr(),
|
||||
@ -195,22 +196,58 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||
child: ListView.builder(
|
||||
itemCount: _channels?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final channel = _channels![idx];
|
||||
final lastMessage = _lastMessages?[channel.id];
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||
child: ListView.builder(
|
||||
itemCount: _channels?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final channel = _channels![idx];
|
||||
final lastMessage = _lastMessages?[channel.id];
|
||||
|
||||
if (channel.type == 1) {
|
||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||
(ele) => ele?.accountId != ua.user?.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (channel.type == 1) {
|
||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||
(ele) => ele?.accountId != ua.user?.id,
|
||||
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(
|
||||
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
||||
title: Text(channel.name),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
@ -218,15 +255,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(
|
||||
'channelDirectMessageDescription'.tr(args: [
|
||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
||||
]),
|
||||
channel.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
||||
content: null,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
@ -236,43 +272,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (mounted) _refreshChannels();
|
||||
if (value == true) _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();
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -9,6 +9,7 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/widgets/chat/call/call_controls.dart';
|
||||
import 'package:surface/widgets/chat/call/call_participant.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class CallRoomScreen extends StatefulWidget {
|
||||
final String scope;
|
||||
@ -152,7 +153,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
return ListenableBuilder(
|
||||
listenable: call,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
|
@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.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';
|
||||
|
||||
class ChannelDetailScreen extends StatefulWidget {
|
||||
@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
|
||||
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
||||
),
|
||||
|
@ -12,6 +12,7 @@ import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatManageScreen extends StatefulWidget {
|
||||
@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.editingChannelAlias != null
|
||||
? Text('screenChatManage').tr()
|
||||
|
@ -20,6 +20,7 @@ import 'package:surface/widgets/chat/chat_message_input.dart';
|
||||
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
||||
import 'package:surface/widgets/dialog.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 '../../providers/user_directory.dart';
|
||||
@ -211,7 +212,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
final call = context.watch<ChatCallProvider>();
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_channel?.type == 1
|
||||
|
@ -13,6 +13,7 @@ import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.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:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@ -95,7 +96,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
key: _fabKey,
|
||||
@ -212,7 +213,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(8),
|
||||
const SliverGap(12),
|
||||
SliverInfiniteList(
|
||||
itemCount: _posts.length,
|
||||
isLoading: _isBusy,
|
||||
@ -242,10 +243,10 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
),
|
||||
openColor: Colors.transparent,
|
||||
openElevation: 0,
|
||||
closedColor: Theme.of(context).colorScheme.surface,
|
||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75),
|
||||
transitionType: ContainerTransitionType.fade,
|
||||
closedShape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -11,6 +11,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
import '../providers/userinfo.dart';
|
||||
import '../widgets/unauthorized_hint.dart';
|
||||
@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenFriend').tr(),
|
||||
@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenFriend').tr(),
|
||||
@ -233,52 +234,56 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.wait([
|
||||
_fetchRelations(),
|
||||
_fetchRequests(),
|
||||
]),
|
||||
child: ListView.builder(
|
||||
itemCount: _relations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final relation = _relations[index];
|
||||
final other = relation.related;
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||
leading: AccountImage(content: other?.avatar),
|
||||
title: Text(other?.nick ?? 'unknown'),
|
||||
subtitle: Text(other?.nick ?? 'unknown'),
|
||||
trailing: SizedBox(
|
||||
height: 48,
|
||||
width: 120,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _changeRelation(relation, 2),
|
||||
child: Text('friendBlock').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _deleteRelation(relation),
|
||||
child: Text('friendDeleteAction').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.wait([
|
||||
_fetchRelations(),
|
||||
_fetchRequests(),
|
||||
]),
|
||||
child: ListView.builder(
|
||||
itemCount: _relations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final relation = _relations[index];
|
||||
final other = relation.related;
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||
leading: AccountImage(content: other?.avatar),
|
||||
title: Text(other?.nick ?? 'unknown'),
|
||||
subtitle: Text(other?.nick ?? 'unknown'),
|
||||
trailing: SizedBox(
|
||||
height: 48,
|
||||
width: 120,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _changeRelation(relation, 2),
|
||||
child: Text('friendBlock').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
InkWell(
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _deleteRelation(relation),
|
||||
child: Text('friendDeleteAction').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -25,6 +25,7 @@ import 'package:surface/types/check_in.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
|
||||
class HomeScreenDashEntry {
|
||||
@ -67,7 +68,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text("screenHome").tr(),
|
||||
@ -387,6 +388,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
Text(
|
||||
'dailyCheckInNone',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
)
|
||||
|
@ -14,6 +14,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.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:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
@ -137,7 +138,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNotification').tr(),
|
||||
@ -148,7 +149,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNotification').tr(),
|
||||
|
@ -14,6 +14,7 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
@ -67,7 +68,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
|
||||
return AppBackground(
|
||||
isRoot: widget.onBack != null,
|
||||
child: Scaffold(
|
||||
child: AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
|
@ -13,6 +13,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.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_media_pending_list.dart';
|
||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||
@ -128,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
return ListenableBuilder(
|
||||
listenable: _writeController,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
|
@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/types/post.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_tags_field.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
@ -119,7 +120,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('screenPostSearch').tr(),
|
||||
actions: [
|
||||
|
@ -17,6 +17,7 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.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/universal_image.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
@ -274,7 +275,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
body: NestedScrollView(
|
||||
controller: _scrollController,
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
|
@ -12,6 +12,7 @@ import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.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/universal_image.dart';
|
||||
|
||||
@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenRealm').tr(),
|
||||
@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenRealm').tr(),
|
||||
@ -118,113 +119,61 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchRealms,
|
||||
child: ListView.builder(
|
||||
itemCount: _realms?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final realm = _realms![idx];
|
||||
if (_isCompactView) {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: realm.avatar,
|
||||
fallbackWidget: const Icon(Symbols.group, size: 20),
|
||||
),
|
||||
title: Text(realm.name),
|
||||
subtitle: Text(
|
||||
realm.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
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,
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchRealms,
|
||||
child: ListView.builder(
|
||||
itemCount: _realms?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final realm = _realms![idx];
|
||||
if (_isCompactView) {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: realm.avatar,
|
||||
fallbackWidget: const Icon(Symbols.group, size: 20),
|
||||
),
|
||||
title: Text(realm.name),
|
||||
subtitle: Text(
|
||||
realm.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
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 Icon(Symbols.edit),
|
||||
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);
|
||||
},
|
||||
),
|
||||
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: () {
|
||||
@ -233,10 +182,69 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
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();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.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:uuid/uuid.dart';
|
||||
|
||||
@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.editingRealmAlias != null
|
||||
? Text('screenRealmManage').tr()
|
||||
|
@ -8,13 +8,13 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.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 '../../types/post.dart';
|
||||
|
||||
class RealmDetailScreen extends StatefulWidget {
|
||||
final String alias;
|
||||
|
||||
@ -70,19 +70,11 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
child: AppScaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
// These are the slivers that show up in the "outer" scroll view.
|
||||
return <Widget>[
|
||||
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),
|
||||
sliver: SliverAppBar(
|
||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||
@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const Gap(16),
|
||||
const Gap(8),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
|
@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/theme.dart';
|
||||
import 'package:surface/theme.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
const Map<String, Color> kColorSchemes = {
|
||||
'colorSchemeIndigo': Colors.indigo,
|
||||
@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return Scaffold(
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenSettings').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
@ -255,6 +260,24 @@ 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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -20,7 +20,7 @@ Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3
|
||||
|
||||
Future<ThemeData> createAppTheme(
|
||||
Brightness brightness, {
|
||||
Color? seedColorOverride,
|
||||
Color? seedColorOverride,
|
||||
bool? useMaterial3,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme(
|
||||
);
|
||||
|
||||
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true),
|
||||
useMaterial3: useM3,
|
||||
colorScheme: colorScheme,
|
||||
brightness: brightness,
|
||||
iconTheme: IconThemeData(
|
||||
@ -45,17 +46,19 @@ Future<ThemeData> createAppTheme(
|
||||
opticalSize: 20,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: hasAppBarBlurry ? 0 : null,
|
||||
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
|
||||
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.transparent,
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AboutScreen extends StatelessWidget {
|
||||
@ -12,97 +13,103 @@ class AboutScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Solian',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
|
||||
),
|
||||
const Text(
|
||||
'The Solar Network',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAbout').tr(),
|
||||
),
|
||||
body: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Solian',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
|
||||
),
|
||||
const Text(
|
||||
'The Solar Network',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Text(
|
||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||
const Gap(16),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('appDetails').tr(),
|
||||
onPressed: () async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
return Text(
|
||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||
const Gap(16),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('appDetails').tr(),
|
||||
onPressed: () async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
|
||||
if (!context.mounted) return;
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'Solian',
|
||||
applicationVersion: '${info.version}+${info.buildNumber}',
|
||||
applicationLegalese:
|
||||
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset(
|
||||
'assets/icon/icon-light-radius.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
if (!context.mounted) return;
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'Solian',
|
||||
applicationVersion: '${info.version}+${info.buildNumber}',
|
||||
applicationLegalese:
|
||||
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset(
|
||||
'assets/icon/icon-light-radius.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('termRelated').tr(),
|
||||
onPressed: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('serviceStatus').tr(),
|
||||
onPressed: () {
|
||||
launchUrlString('https://status.solsynth.dev');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('termRelated').tr(),
|
||||
onPressed: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('serviceStatus').tr(),
|
||||
onPressed: () {
|
||||
launchUrlString('https://status.solsynth.dev');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
).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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ class AttachmentList extends StatefulWidget {
|
||||
final List<SnAttachment?> data;
|
||||
final bool bordered;
|
||||
final bool gridded;
|
||||
final bool columned;
|
||||
final BoxFit fit;
|
||||
final double? maxHeight;
|
||||
final double? minWidth;
|
||||
@ -26,6 +27,7 @@ class AttachmentList extends StatefulWidget {
|
||||
required this.data,
|
||||
this.bordered = false,
|
||||
this.gridded = false,
|
||||
this.columned = false,
|
||||
this.fit = BoxFit.cover,
|
||||
this.maxHeight,
|
||||
this.minWidth,
|
||||
@ -105,45 +107,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.gridded) {
|
||||
final fullOfImage =
|
||||
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
|
||||
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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final fullOfImage =
|
||||
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
|
||||
|
||||
if (widget.gridded && fullOfImage) {
|
||||
return Container(
|
||||
margin: widget.padding ?? EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
@ -191,6 +158,44 @@ 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(
|
||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||
child: ScrollConfiguration(
|
||||
|
@ -365,7 +365,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
),
|
||||
onVerticalDragUpdate: (details) {
|
||||
if (_showDetail) return;
|
||||
if (details.delta.dy < 0) {
|
||||
if (details.delta.dy <= -40) {
|
||||
_showDetail = true;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@ -415,77 +415,79 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
child: Table(
|
||||
columnWidths: {
|
||||
0: IntrinsicColumnWidth(),
|
||||
1: FlexColumnWidth(),
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
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)
|
||||
child: SingleChildScrollView(
|
||||
child: Table(
|
||||
columnWidths: {
|
||||
0: IntrinsicColumnWidth(),
|
||||
1: FlexColumnWidth(),
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
TableCell(child: Text('Hash').padding(right: 16)),
|
||||
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
|
||||
TableCell(
|
||||
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,
|
||||
...(data.metadata['exif']?.keys.map((k) => TableRow(
|
||||
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(k).padding(right: 16)),
|
||||
TableCell(child: Text(data.metadata['exif'][k].toString())),
|
||||
TableCell(child: Text('Hash').padding(right: 16)),
|
||||
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
|
||||
],
|
||||
)) ??
|
||||
[]),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 8),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -18,45 +18,49 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
listenable: ws,
|
||||
builder: (context, _) {
|
||||
final ua = context.read<UserProvider>();
|
||||
final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized;
|
||||
|
||||
return GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (ws.isBusy)
|
||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else if (!ws.isConnected)
|
||||
Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else
|
||||
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
const Gap(8),
|
||||
if (ws.isBusy)
|
||||
const CircularProgressIndicator(strokeWidth: 2.5)
|
||||
.width(12)
|
||||
.height(12)
|
||||
.padding(horizontal: 4, right: 4)
|
||||
else if (!ws.isConnected)
|
||||
const Icon(Symbols.power_off, size: 18)
|
||||
else
|
||||
const Icon(Symbols.power, size: 18),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity((ws.isBusy || !ws.isConnected) && ua.isAuthorized ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
),
|
||||
onTap: () {
|
||||
if (!ws.isConnected && !ws.isBusy) {
|
||||
ws.connect();
|
||||
}
|
||||
},
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (ws.isBusy)
|
||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else if (!ws.isConnected)
|
||||
Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
else
|
||||
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
const Gap(8),
|
||||
if (ws.isBusy)
|
||||
const CircularProgressIndicator(strokeWidth: 2.5)
|
||||
.width(12)
|
||||
.height(12)
|
||||
.padding(horizontal: 4, right: 4)
|
||||
else if (!ws.isConnected)
|
||||
const Icon(Symbols.power_off, size: 18)
|
||||
else
|
||||
const Icon(Symbols.power, size: 18),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity(show ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
),
|
||||
onTap: () {
|
||||
if (!ws.isConnected && !ws.isBusy) {
|
||||
ws.connect();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -94,11 +94,14 @@ class _LinkPreviewEntry extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (meta.icon?.isNotEmpty ?? false)
|
||||
StyledWidget(
|
||||
meta.icon!.endsWith('.svg')
|
||||
? SvgPicture.network(meta.icon!)
|
||||
SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: meta.icon!.endsWith('.svg')
|
||||
? SvgPicture.network(meta.icon!, width: 36, height: 36)
|
||||
: UniversalImage(
|
||||
meta.icon!,
|
||||
noErrorWidget: true,
|
||||
width: 36,
|
||||
height: 36,
|
||||
cacheHeight: 36,
|
||||
|
@ -21,34 +21,76 @@ import 'package:surface/widgets/notify_indicator.dart';
|
||||
|
||||
final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
class AppPageScaffold extends StatelessWidget {
|
||||
final String? title;
|
||||
class AppScaffold extends StatelessWidget {
|
||||
final Widget? body;
|
||||
final bool showAppBar;
|
||||
final bool showBottomNavigation;
|
||||
final PreferredSizeWidget? bottomNavigationBar;
|
||||
final PreferredSizeWidget? bottomSheet;
|
||||
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 AppPageScaffold({
|
||||
const AppScaffold({
|
||||
super.key,
|
||||
this.title,
|
||||
this.appBar,
|
||||
this.body,
|
||||
this.showAppBar = true,
|
||||
this.showBottomNavigation = false,
|
||||
this.floatingActionButton,
|
||||
this.floatingActionButtonLocation,
|
||||
this.floatingActionButtonAnimator,
|
||||
this.bottomNavigationBar,
|
||||
this.bottomSheet,
|
||||
this.drawer,
|
||||
this.endDrawer,
|
||||
this.onDrawerChanged,
|
||||
this.onEndDrawerChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = GoRouter.maybeOf(context);
|
||||
final routeName = state?.routerDelegate.currentConfiguration.last.route.name;
|
||||
|
||||
final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen';
|
||||
final appBarHeight = appBar?.preferredSize.height ?? 0;
|
||||
final safeTop = MediaQuery.of(context).padding.top;
|
||||
|
||||
return Scaffold(
|
||||
appBar: showAppBar
|
||||
? AppBar(
|
||||
title: Text(title ?? autoTitle.tr()),
|
||||
)
|
||||
: null,
|
||||
body: body,
|
||||
extendBody: true,
|
||||
extendBodyBehindAppBar: true,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SizedBox.expand(
|
||||
child: AppBackground(
|
||||
child: Column(
|
||||
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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -101,16 +143,16 @@ class AppRootScaffold extends StatelessWidget {
|
||||
|
||||
final safeTop = MediaQuery.of(context).padding.top;
|
||||
|
||||
return AppBackground(
|
||||
isRoot: true,
|
||||
child: Scaffold(
|
||||
key: globalRootScaffoldKey,
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
|
||||
Container(
|
||||
return Scaffold(
|
||||
key: globalRootScaffoldKey,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
|
||||
WindowTitleBarBox(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
@ -119,49 +161,44 @@ class AppRootScaffold extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||
children: [
|
||||
WindowTitleBarBox(
|
||||
child: MoveWindow(
|
||||
child: Text(
|
||||
'Solar Network',
|
||||
style: GoogleFonts.spaceGrotesk(),
|
||||
).padding(horizontal: 12, vertical: 5),
|
||||
),
|
||||
),
|
||||
if (!Platform.isMacOS)
|
||||
Expanded(
|
||||
child: WindowTitleBarBox(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: MoveWindow()),
|
||||
Row(
|
||||
children: [
|
||||
MinimizeWindowButton(colors: windowButtonColor),
|
||||
MaximizeWindowButton(colors: windowButtonColor),
|
||||
CloseWindowButton(colors: windowButtonColor),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
child: MoveWindow(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Solar Network',
|
||||
style: GoogleFonts.spaceGrotesk(),
|
||||
).padding(horizontal: 12, vertical: 5),
|
||||
if (!Platform.isMacOS)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(child: MoveWindow()),
|
||||
Row(
|
||||
children: [
|
||||
MinimizeWindowButton(colors: windowButtonColor),
|
||||
MaximizeWindowButton(colors: windowButtonColor),
|
||||
CloseWindowButton(colors: windowButtonColor),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: innerWidget),
|
||||
],
|
||||
),
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
|
||||
],
|
||||
),
|
||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||
drawerEdgeDragWidth: isPopable ? 0 : null,
|
||||
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
||||
),
|
||||
Expanded(child: innerWidget),
|
||||
],
|
||||
),
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
|
||||
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
|
||||
],
|
||||
),
|
||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||
drawerEdgeDragWidth: isPopable ? 0 : null,
|
||||
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -15,43 +15,48 @@ class NotifyIndicator extends StatelessWidget {
|
||||
final ua = context.read<UserProvider>();
|
||||
final nty = context.watch<NotificationProvider>();
|
||||
|
||||
final show = nty.notifications.isNotEmpty && ua.isAuthorized;
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: nty,
|
||||
builder: (context, _) {
|
||||
return GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
nty.notifications.lastOrNull?.title ??
|
||||
'notificationUnreadCount'.plural(nty.notifications.length),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (nty.notifications.lastOrNull?.body != null)
|
||||
return IgnorePointer(
|
||||
ignoring: !show,
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
nty.notifications.lastOrNull!.body,
|
||||
nty.notifications.lastOrNull?.title ??
|
||||
'notificationUnreadCount'.plural(nty.notifications.length),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(left: 4),
|
||||
const Gap(8),
|
||||
const Icon(Symbols.notifications_unread, size: 18),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity(nty.notifications.isNotEmpty && ua.isAuthorized ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
),
|
||||
onTap: () {
|
||||
nty.clear();
|
||||
},
|
||||
),
|
||||
if (nty.notifications.lastOrNull?.body != null)
|
||||
Text(
|
||||
nty.notifications.lastOrNull!.body,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(left: 4),
|
||||
const Gap(8),
|
||||
const Icon(Symbols.notifications_unread, size: 18),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity(show ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
),
|
||||
onTap: () {
|
||||
nty.clear();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -256,9 +256,8 @@ class PostItem extends StatelessWidget {
|
||||
AttachmentList(
|
||||
data: displayableAttachments!,
|
||||
bordered: true,
|
||||
gridded: true,
|
||||
maxHeight: showFullPost ? null : 480,
|
||||
minWidth: 640,
|
||||
maxWidth: MediaQuery.of(context).size.width - 20,
|
||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
@ -344,7 +343,7 @@ class PostShareImageWidget extends StatelessWidget {
|
||||
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
|
||||
StyledWidget(AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
gridded: true,
|
||||
columned: true,
|
||||
)).padding(horizontal: 16, bottom: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -12,6 +13,7 @@ import 'package:surface/widgets/dialog.dart';
|
||||
class PostReactionPopup extends StatefulWidget {
|
||||
final SnPost data;
|
||||
final Function(Map<String, int> value, int attr, int delta)? onChanged;
|
||||
|
||||
const PostReactionPopup({super.key, required this.data, this.onChanged});
|
||||
|
||||
@override
|
||||
@ -59,6 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
||||
);
|
||||
}
|
||||
}
|
||||
HapticFeedback.mediumImpact();
|
||||
} catch (err) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
@ -84,9 +87,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
||||
children: [
|
||||
const Icon(Symbols.mood, size: 24),
|
||||
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),
|
||||
Container(
|
||||
@ -102,9 +103,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
||||
Text('postReactionDownvote').plural(widget.data.totalDownvote),
|
||||
const Gap(24),
|
||||
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,
|
||||
),
|
||||
const Gap(8),
|
||||
|
@ -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
|
||||
# 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.
|
||||
version: 2.2.2+54
|
||||
version: 2.2.2+55
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
|
Loading…
x
Reference in New Issue
Block a user