Compare commits
13 Commits
b166a6e85c
...
2.0.0+2
| Author | SHA1 | Date | |
|---|---|---|---|
| 654a71e852 | |||
| 455ffcac19 | |||
| 9c8dad0176 | |||
| 2c6b1feca6 | |||
| af044a86bc | |||
| 4884d04a51 | |||
| b9ad6d4fd0 | |||
| 468d1377af | |||
| 9851093a1e | |||
| 5368f8ebb0 | |||
| e5239a6ca0 | |||
| 5b198412f6 | |||
| 7087c41f07 |
13
.roadsignrc
Normal file
13
.roadsignrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"sync": {
|
||||
"region": "solian-next",
|
||||
"configPath": "roadsign.toml"
|
||||
},
|
||||
"deployments": [
|
||||
{
|
||||
"region": "solian-next",
|
||||
"site": "solian-next-web",
|
||||
"path": "build/web"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
"screenAccountPublisherEdit": "Edit Publisher",
|
||||
"screenAccountProfileEdit": "Edit Profile",
|
||||
"screenSettings": "Settings",
|
||||
"screenAlbum": "Album",
|
||||
"screenChat": "Chat",
|
||||
"dialogOkay": "Okay",
|
||||
"dialogCancel": "Cancel",
|
||||
"dialogConfirm": "Confirm",
|
||||
@@ -26,8 +28,8 @@
|
||||
"errorRequestNotFound": "The resource that you looking for is not found.",
|
||||
"errorRequestConnection": "Network connection error, please check your network or the service status.",
|
||||
"errorRequestUnknown": "Unknown request error, maybe you want to take screenshot and report it to us.",
|
||||
"prev": "Next",
|
||||
"next": "Previous",
|
||||
"prev": "Previous",
|
||||
"next": "Next",
|
||||
"edit": "Edit",
|
||||
"apply": "Apply",
|
||||
"create": "Create",
|
||||
@@ -86,12 +88,21 @@
|
||||
"fieldPostTitle": "Title",
|
||||
"fieldPostDescription": "Description",
|
||||
"postPublish": "Publish",
|
||||
"postPosted": "Post has been posted.",
|
||||
"postPublishedAt": "Published At",
|
||||
"postPublishedUntil": "Published Until",
|
||||
"postEditingNotice": "You're about to editing a post that posted {}.",
|
||||
"postReplyingNotice": "You're about to reply to a post that posted {}.",
|
||||
"postRepostingNotice": "You're about to repost a post that posted {}.",
|
||||
"postReact": "React",
|
||||
"postReactions": "Reactions of Post",
|
||||
"postReactionPoints": {
|
||||
"zero": "{} pt",
|
||||
"one": "{} pt",
|
||||
"other": "{} pts"
|
||||
},
|
||||
"postReactCompleted": "Reaction has been added.",
|
||||
"postReactUncompleted": "Reaction has been removed.",
|
||||
"postComments": {
|
||||
"zero": "Comment",
|
||||
"one": "{} comment",
|
||||
@@ -116,5 +127,9 @@
|
||||
"settingsNetworkServerResetDescription": "Reset to the official server address of Solar Network.",
|
||||
"settingsNetworkServerPreset": "Present HyperNet Server",
|
||||
"settingsNetworkServerPresetDescription": "You can choose one of our preset HyperNet server addresses from the list on the right.",
|
||||
"settingsNetworkServerSaved": "Server address saved."
|
||||
"settingsNetworkServerSaved": "Server address saved.",
|
||||
"sensitiveContent": "Sensitive Content",
|
||||
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
|
||||
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
|
||||
"sensitiveContentReveal": "Reveal"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
"screenAccountPublisherEdit": "编辑发布者",
|
||||
"screenAccountProfileEdit": "编辑资料",
|
||||
"screenSettings": "设置",
|
||||
"screenAlbum": "相册",
|
||||
"screenChat": "聊天",
|
||||
"dialogOkay": "好的",
|
||||
"dialogCancel": "取消",
|
||||
"dialogConfirm": "确认",
|
||||
@@ -92,6 +94,15 @@
|
||||
"postReplyingNotice": "你正在回复由 {} 发布的帖子。",
|
||||
"postRepostingNotice": "你正在转发由 {} 发布的帖子。",
|
||||
"postReact": "反应",
|
||||
"postPosted": "帖子已经发表。",
|
||||
"postReactions": "帖子的反应",
|
||||
"postReactionPoints": {
|
||||
"zero": "{} 点",
|
||||
"one": "{} 点",
|
||||
"other": "{} 点"
|
||||
},
|
||||
"postReactCompleted": "反应已被添加。",
|
||||
"postReactUncompleted": "反应已被移除。",
|
||||
"postComments": {
|
||||
"zero": "评论",
|
||||
"one": "{} 条评论",
|
||||
@@ -116,5 +127,9 @@
|
||||
"settingsNetworkServerResetDescription": "重设为 Solar Network 的服务器地址。",
|
||||
"settingsNetworkServerPreset": "预设的 HyperNet 服务器",
|
||||
"settingsNetworkServerPresetDescription": "你可以在旁边的列表中选择我们提供的预设 HyperNet 服务器地址。",
|
||||
"settingsNetworkServerSaved": "服务器地址已保存。"
|
||||
"settingsNetworkServerSaved": "服务器地址已保存。",
|
||||
"sensitiveContent": "敏感内容",
|
||||
"sensitiveContentCollapsed": "敏感内容已折叠。",
|
||||
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
|
||||
"sensitiveContentReveal": "显示内容"
|
||||
}
|
||||
|
||||
@@ -161,7 +161,6 @@
|
||||
64FBE78F9C282712818D6D95 /* Pods-RunnerTests.release.xcconfig */,
|
||||
96081771773FA019A97CCC3F /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -474,6 +473,7 @@
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -657,6 +657,7 @@
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -680,6 +681,7 @@
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Surface</string>
|
||||
<string>Solian</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>surface</string>
|
||||
<string>Solian</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -234,7 +234,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void post(BuildContext context) async {
|
||||
Future<void> post(BuildContext context) async {
|
||||
if (isBusy || publisher == null) return;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
@@ -294,8 +294,9 @@ class PostWriteController extends ChangeNotifier {
|
||||
data: {
|
||||
'publisher': publisher!.id,
|
||||
'content': contentController.text,
|
||||
'title': titleController.text,
|
||||
'description': descriptionController.text,
|
||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||
if (descriptionController.text.isNotEmpty)
|
||||
'description': descriptionController.text,
|
||||
'attachments': attachments
|
||||
.where((e) => e.attachment != null)
|
||||
.map((e) => e.attachment!.rid)
|
||||
@@ -322,8 +323,6 @@ class PostWriteController extends ChangeNotifier {
|
||||
method: editingPost != null ? 'PUT' : 'POST',
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@@ -363,6 +362,25 @@ class PostWriteController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setIsBusy(bool value) {
|
||||
isBusy = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
publishedAt = null;
|
||||
publishedUntil = null;
|
||||
titleController.clear();
|
||||
descriptionController.clear();
|
||||
contentController.clear();
|
||||
attachments.clear();
|
||||
editingPost = null;
|
||||
replyingPost = null;
|
||||
repostingPost = null;
|
||||
mode = kTitleMap.keys.first;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
contentController.dispose();
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:surface/providers/navigation.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/theme.dart';
|
||||
@@ -39,6 +40,7 @@ class SolianApp extends StatelessWidget {
|
||||
providers: [
|
||||
Provider(create: (_) => SnNetworkProvider()),
|
||||
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => NavigationProvider()),
|
||||
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (_) => ThemeProvider()),
|
||||
],
|
||||
@@ -59,6 +61,7 @@ class AppMainContent extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
context.read<NavigationProvider>();
|
||||
context.read<UserProvider>();
|
||||
|
||||
final th = context.watch<ThemeProvider>();
|
||||
|
||||
112
lib/providers/navigation.dart
Normal file
112
lib/providers/navigation.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AppNavDestination {
|
||||
final String label;
|
||||
final String screen;
|
||||
final Widget icon;
|
||||
final bool isPinned;
|
||||
|
||||
const AppNavDestination({
|
||||
required this.label,
|
||||
required this.screen,
|
||||
required this.icon,
|
||||
this.isPinned = false,
|
||||
});
|
||||
}
|
||||
|
||||
class NavigationProvider extends ChangeNotifier {
|
||||
int? _currentIndex;
|
||||
|
||||
int? get currentIndex => _currentIndex;
|
||||
|
||||
static const List<AppNavDestination> kAllDestination = [
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.home, weight: 400, opticalSize: 20),
|
||||
screen: 'home',
|
||||
label: 'screenHome',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.explore, weight: 400, opticalSize: 20),
|
||||
screen: 'explore',
|
||||
label: 'screenExplore',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
|
||||
screen: 'account',
|
||||
label: 'screenAccount',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.album, weight: 400, opticalSize: 20),
|
||||
screen: 'album',
|
||||
label: 'screenAlbum',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.chat, weight: 400, opticalSize: 20),
|
||||
screen: 'chat',
|
||||
label: 'screenChat',
|
||||
),
|
||||
];
|
||||
static const List<String> kDefaultPinnedDestination = [
|
||||
'home',
|
||||
'explore',
|
||||
'account'
|
||||
];
|
||||
|
||||
List<AppNavDestination> destinations = [];
|
||||
|
||||
int get pinnedDestinationCount =>
|
||||
destinations.where((ele) => ele.isPinned).length;
|
||||
|
||||
NavigationProvider() {
|
||||
buildDestinations(kDefaultPinnedDestination);
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
final pinned = prefs.getStringList("app_pinned_navigation");
|
||||
if (pinned != null) buildDestinations(pinned);
|
||||
});
|
||||
}
|
||||
|
||||
void buildDestinations(List<String> pinned) {
|
||||
destinations = kAllDestination
|
||||
.map(
|
||||
(ele) => AppNavDestination(
|
||||
label: ele.label,
|
||||
screen: ele.screen,
|
||||
icon: ele.icon,
|
||||
isPinned: pinned.contains(ele.screen),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int getIndexInRange(int min, int max) {
|
||||
return math.max(min, math.min(_currentIndex ?? 0, max));
|
||||
}
|
||||
|
||||
bool isIndexInRange(int min, int max) {
|
||||
return _currentIndex != null &&
|
||||
_currentIndex! >= min &&
|
||||
_currentIndex! < max;
|
||||
}
|
||||
|
||||
void autoDetectIndex(GoRouter? state) {
|
||||
if (state == null) return;
|
||||
final idx = destinations.indexWhere(
|
||||
(ele) =>
|
||||
ele.screen ==
|
||||
state.routerDelegate.currentConfiguration.last.route.name,
|
||||
);
|
||||
_currentIndex = idx == -1 ? null : idx;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setIndex(int idx) {
|
||||
_currentIndex = idx;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -44,12 +44,19 @@ class SnAttachmentProvider {
|
||||
'take': pendingFetch.length,
|
||||
'id': pendingFetch.join(','),
|
||||
});
|
||||
final out = resp.data['data'].map((e) => SnAttachment.fromJson(e)).toList();
|
||||
final out = resp.data['data']
|
||||
.where((e) => e['id'] != 0)
|
||||
.map((e) => SnAttachment.fromJson(e))
|
||||
.toList();
|
||||
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
_cache[pendingFetch[i]] = out[i];
|
||||
for (final item in out) {
|
||||
_cache[item.rid] = item;
|
||||
}
|
||||
return rids.map((rid) => _cache[rid]!).toList();
|
||||
|
||||
return rids
|
||||
.where((rid) => _cache.containsKey(rid))
|
||||
.map((rid) => _cache[rid]!)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static Map<String, String> mimetypeOverrides = {
|
||||
|
||||
@@ -100,7 +100,7 @@ class SnNetworkProvider {
|
||||
}
|
||||
|
||||
String getAttachmentUrl(String ky) {
|
||||
if (ky.startsWith("http://")) return ky;
|
||||
if (ky.startsWith("http")) return ky;
|
||||
return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import 'package:surface/screens/account/profile_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||
import 'package:surface/screens/account/publishers/publisher_new.dart';
|
||||
import 'package:surface/screens/account/publishers/publishers.dart';
|
||||
import 'package:surface/screens/album.dart';
|
||||
import 'package:surface/screens/auth/login.dart';
|
||||
import 'package:surface/screens/auth/register.dart';
|
||||
import 'package:surface/screens/chat.dart';
|
||||
import 'package:surface/screens/explore.dart';
|
||||
import 'package:surface/screens/home.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
@@ -20,6 +22,7 @@ final appRouter = GoRouter(
|
||||
builder: (context, state, child) => AppScaffold(
|
||||
body: child,
|
||||
showBottomNavigation: true,
|
||||
showDrawer: true,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
@@ -37,6 +40,16 @@ final appRouter = GoRouter(
|
||||
name: 'account',
|
||||
builder: (context, state) => const AccountScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
builder: (context, state) => const ChatScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/album',
|
||||
name: 'album',
|
||||
builder: (context, state) => const AlbumScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
@@ -74,6 +87,7 @@ final appRouter = GoRouter(
|
||||
builder: (context, state, child) => AppScaffold(
|
||||
body: child,
|
||||
autoImplyAppBar: true,
|
||||
showDrawer: true,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
|
||||
@@ -232,7 +232,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? UniversalImage(
|
||||
? AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(_banner!),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
|
||||
@@ -210,7 +210,7 @@ class _AccountPublisherEditScreenState
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? UniversalImage(
|
||||
? AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(_banner!),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
|
||||
10
lib/screens/album.dart
Normal file
10
lib/screens/album.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AlbumScreen extends StatelessWidget {
|
||||
const AlbumScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
onTicket: (p0) => setState(() {
|
||||
_currentTicket = p0;
|
||||
}),
|
||||
onNext: (p0) => setState(() {
|
||||
onNext: () => setState(() {
|
||||
_period = 1;
|
||||
}),
|
||||
),
|
||||
@@ -271,13 +271,14 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||
|
||||
try {
|
||||
// Request one-time-password code
|
||||
sn.client.post('/cgi/id/auth/factors/$_factorPicked');
|
||||
await sn.client.post('/cgi/id/auth/factors/$_factorPicked');
|
||||
widget.onPickFactor(
|
||||
widget.factors!.where((x) => x.id == _factorPicked).first,
|
||||
);
|
||||
widget.onNext();
|
||||
} catch (err) {
|
||||
context.showErrorDialog(err);
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
return;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
|
||||
10
lib/screens/chat.dart
Normal file
10
lib/screens/chat.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChatScreen extends StatelessWidget {
|
||||
const ChatScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
@@ -14,6 +15,7 @@ import 'package:surface/widgets/loading_indicator.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';
|
||||
|
||||
class PostDetailScreen extends StatefulWidget {
|
||||
final String slug;
|
||||
@@ -45,13 +47,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
resp.data['body']['attachments']?.cast<String>() ?? [],
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_data = SnPost.fromJson(resp.data).copyWith(
|
||||
preload: SnPostPreload(
|
||||
attachments: attachments,
|
||||
),
|
||||
);
|
||||
});
|
||||
_data = SnPost.fromJson(resp.data).copyWith(
|
||||
preload: SnPostPreload(
|
||||
attachments: attachments,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
@@ -68,8 +68,12 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
_fetchPost();
|
||||
}
|
||||
|
||||
final GlobalKey<PostCommentSliverListState> _childListKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
@@ -81,13 +85,19 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
},
|
||||
),
|
||||
flexibleSpace: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(_data?.body['title'] ?? 'postNoun'.tr())
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
||||
.textColor(Colors.white),
|
||||
Text('postDetail')
|
||||
.tr()
|
||||
.textColor(Colors.white.withAlpha((255 * 0.9).round())),
|
||||
if (_data?.body['title'] != null)
|
||||
Text(_data?.body['title'] ?? 'postNoun'.tr())
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
||||
.textColor(Colors.white),
|
||||
if (_data?.body['title'] != null)
|
||||
Text('postDetail'.tr())
|
||||
.textColor(Colors.white.withAlpha((255 * 0.9).round()))
|
||||
else
|
||||
Text('postDetail'.tr())
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
||||
.textColor(Colors.white),
|
||||
],
|
||||
).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
|
||||
),
|
||||
@@ -98,17 +108,57 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(data: _data!),
|
||||
child: PostItem(
|
||||
data: _data!,
|
||||
showComments: false,
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Text('postCommentsDetailed')
|
||||
.plural(_data!.metric.replyCount)
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
||||
.padding(horizontal: 16, top: 12, bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.comment, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCommentsDetailed')
|
||||
.plural(_data!.metric.replyCount)
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 12),
|
||||
),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 240,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: _data!.id,
|
||||
onPost: () {
|
||||
_childListKey.currentState!.refresh();
|
||||
setState(() {
|
||||
_data = _data!.copyWith(
|
||||
metric: _data!.metric.copyWith(
|
||||
replyCount: _data!.metric.replyCount + 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_data != null)
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
parentPostId: _data!.id,
|
||||
),
|
||||
if (_data != null) PostCommentSliverList(parentPostId: _data!.id),
|
||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -328,7 +328,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
)
|
||||
),
|
||||
]
|
||||
.expandIndexed(
|
||||
(idx, ele) => [
|
||||
@@ -390,7 +390,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
onPressed: (_writeController.isBusy ||
|
||||
_writeController.publisher == null)
|
||||
? null
|
||||
: () => _writeController.post(context),
|
||||
: () {
|
||||
_writeController.post(context).then((_) {
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
});
|
||||
},
|
||||
icon: const Icon(Symbols.send),
|
||||
label: Text('postPublish').tr(),
|
||||
),
|
||||
|
||||
@@ -20,14 +20,13 @@ class SnPost with _$SnPost {
|
||||
required String? aliasPrefix,
|
||||
required List<dynamic> tags,
|
||||
required List<dynamic> categories,
|
||||
required dynamic reactions,
|
||||
required dynamic replies,
|
||||
required dynamic replyId,
|
||||
required dynamic repostId,
|
||||
required dynamic replyTo,
|
||||
required dynamic repostTo,
|
||||
required dynamic visibleUsersList,
|
||||
required dynamic invisibleUsersList,
|
||||
required List<SnPost>? replies,
|
||||
required int? replyId,
|
||||
required int? repostId,
|
||||
required SnPost? replyTo,
|
||||
required SnPost? repostTo,
|
||||
required List<int>? visibleUsersList,
|
||||
required List<int>? invisibleUsersList,
|
||||
required int visibility,
|
||||
required DateTime? editedAt,
|
||||
required DateTime? pinnedAt,
|
||||
@@ -37,8 +36,6 @@ class SnPost with _$SnPost {
|
||||
required DateTime? publishedUntil,
|
||||
required int totalUpvote,
|
||||
required int totalDownvote,
|
||||
required int? realmId,
|
||||
required dynamic realm,
|
||||
required int publisherId,
|
||||
required SnPublisher publisher,
|
||||
required SnMetric metric,
|
||||
@@ -81,6 +78,7 @@ class SnMetric with _$SnMetric {
|
||||
const factory SnMetric({
|
||||
required int replyCount,
|
||||
required int reactionCount,
|
||||
@Default({}) Map<String, int> reactionList,
|
||||
}) = _SnMetric;
|
||||
|
||||
factory SnMetric.fromJson(Map<String, Object?> json) =>
|
||||
|
||||
@@ -31,14 +31,13 @@ mixin _$SnPost {
|
||||
String? get aliasPrefix => throw _privateConstructorUsedError;
|
||||
List<dynamic> get tags => throw _privateConstructorUsedError;
|
||||
List<dynamic> get categories => throw _privateConstructorUsedError;
|
||||
dynamic get reactions => throw _privateConstructorUsedError;
|
||||
dynamic get replies => throw _privateConstructorUsedError;
|
||||
dynamic get replyId => throw _privateConstructorUsedError;
|
||||
dynamic get repostId => throw _privateConstructorUsedError;
|
||||
dynamic get replyTo => throw _privateConstructorUsedError;
|
||||
dynamic get repostTo => throw _privateConstructorUsedError;
|
||||
dynamic get visibleUsersList => throw _privateConstructorUsedError;
|
||||
dynamic get invisibleUsersList => throw _privateConstructorUsedError;
|
||||
List<SnPost>? get replies => throw _privateConstructorUsedError;
|
||||
int? get replyId => throw _privateConstructorUsedError;
|
||||
int? get repostId => throw _privateConstructorUsedError;
|
||||
SnPost? get replyTo => throw _privateConstructorUsedError;
|
||||
SnPost? get repostTo => throw _privateConstructorUsedError;
|
||||
List<int>? get visibleUsersList => throw _privateConstructorUsedError;
|
||||
List<int>? get invisibleUsersList => throw _privateConstructorUsedError;
|
||||
int get visibility => throw _privateConstructorUsedError;
|
||||
DateTime? get editedAt => throw _privateConstructorUsedError;
|
||||
DateTime? get pinnedAt => throw _privateConstructorUsedError;
|
||||
@@ -48,8 +47,6 @@ mixin _$SnPost {
|
||||
DateTime? get publishedUntil => throw _privateConstructorUsedError;
|
||||
int get totalUpvote => throw _privateConstructorUsedError;
|
||||
int get totalDownvote => throw _privateConstructorUsedError;
|
||||
int? get realmId => throw _privateConstructorUsedError;
|
||||
dynamic get realm => throw _privateConstructorUsedError;
|
||||
int get publisherId => throw _privateConstructorUsedError;
|
||||
SnPublisher get publisher => throw _privateConstructorUsedError;
|
||||
SnMetric get metric => throw _privateConstructorUsedError;
|
||||
@@ -81,14 +78,13 @@ abstract class $SnPostCopyWith<$Res> {
|
||||
String? aliasPrefix,
|
||||
List<dynamic> tags,
|
||||
List<dynamic> categories,
|
||||
dynamic reactions,
|
||||
dynamic replies,
|
||||
dynamic replyId,
|
||||
dynamic repostId,
|
||||
dynamic replyTo,
|
||||
dynamic repostTo,
|
||||
dynamic visibleUsersList,
|
||||
dynamic invisibleUsersList,
|
||||
List<SnPost>? replies,
|
||||
int? replyId,
|
||||
int? repostId,
|
||||
SnPost? replyTo,
|
||||
SnPost? repostTo,
|
||||
List<int>? visibleUsersList,
|
||||
List<int>? invisibleUsersList,
|
||||
int visibility,
|
||||
DateTime? editedAt,
|
||||
DateTime? pinnedAt,
|
||||
@@ -98,13 +94,13 @@ abstract class $SnPostCopyWith<$Res> {
|
||||
DateTime? publishedUntil,
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int? realmId,
|
||||
dynamic realm,
|
||||
int publisherId,
|
||||
SnPublisher publisher,
|
||||
SnMetric metric,
|
||||
SnPostPreload? preload});
|
||||
|
||||
$SnPostCopyWith<$Res>? get replyTo;
|
||||
$SnPostCopyWith<$Res>? get repostTo;
|
||||
$SnPublisherCopyWith<$Res> get publisher;
|
||||
$SnMetricCopyWith<$Res> get metric;
|
||||
$SnPostPreloadCopyWith<$Res>? get preload;
|
||||
@@ -136,7 +132,6 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
Object? aliasPrefix = freezed,
|
||||
Object? tags = null,
|
||||
Object? categories = null,
|
||||
Object? reactions = freezed,
|
||||
Object? replies = freezed,
|
||||
Object? replyId = freezed,
|
||||
Object? repostId = freezed,
|
||||
@@ -153,8 +148,6 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
Object? publishedUntil = freezed,
|
||||
Object? totalUpvote = null,
|
||||
Object? totalDownvote = null,
|
||||
Object? realmId = freezed,
|
||||
Object? realm = freezed,
|
||||
Object? publisherId = null,
|
||||
Object? publisher = null,
|
||||
Object? metric = null,
|
||||
@@ -205,38 +198,34 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
? _value.categories
|
||||
: categories // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,
|
||||
reactions: freezed == reactions
|
||||
? _value.reactions
|
||||
: reactions // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
replies: freezed == replies
|
||||
? _value.replies
|
||||
: replies // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as List<SnPost>?,
|
||||
replyId: freezed == replyId
|
||||
? _value.replyId
|
||||
: replyId // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as int?,
|
||||
repostId: freezed == repostId
|
||||
? _value.repostId
|
||||
: repostId // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as int?,
|
||||
replyTo: freezed == replyTo
|
||||
? _value.replyTo
|
||||
: replyTo // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as SnPost?,
|
||||
repostTo: freezed == repostTo
|
||||
? _value.repostTo
|
||||
: repostTo // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as SnPost?,
|
||||
visibleUsersList: freezed == visibleUsersList
|
||||
? _value.visibleUsersList
|
||||
: visibleUsersList // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as List<int>?,
|
||||
invisibleUsersList: freezed == invisibleUsersList
|
||||
? _value.invisibleUsersList
|
||||
: invisibleUsersList // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as List<int>?,
|
||||
visibility: null == visibility
|
||||
? _value.visibility
|
||||
: visibility // ignore: cast_nullable_to_non_nullable
|
||||
@@ -273,14 +262,6 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
? _value.totalDownvote
|
||||
: totalDownvote // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
realmId: freezed == realmId
|
||||
? _value.realmId
|
||||
: realmId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
realm: freezed == realm
|
||||
? _value.realm
|
||||
: realm // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
publisherId: null == publisherId
|
||||
? _value.publisherId
|
||||
: publisherId // ignore: cast_nullable_to_non_nullable
|
||||
@@ -300,6 +281,34 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPostCopyWith<$Res>? get replyTo {
|
||||
if (_value.replyTo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPostCopyWith<$Res>(_value.replyTo!, (value) {
|
||||
return _then(_value.copyWith(replyTo: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPostCopyWith<$Res>? get repostTo {
|
||||
if (_value.repostTo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPostCopyWith<$Res>(_value.repostTo!, (value) {
|
||||
return _then(_value.copyWith(repostTo: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@@ -354,14 +363,13 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
String? aliasPrefix,
|
||||
List<dynamic> tags,
|
||||
List<dynamic> categories,
|
||||
dynamic reactions,
|
||||
dynamic replies,
|
||||
dynamic replyId,
|
||||
dynamic repostId,
|
||||
dynamic replyTo,
|
||||
dynamic repostTo,
|
||||
dynamic visibleUsersList,
|
||||
dynamic invisibleUsersList,
|
||||
List<SnPost>? replies,
|
||||
int? replyId,
|
||||
int? repostId,
|
||||
SnPost? replyTo,
|
||||
SnPost? repostTo,
|
||||
List<int>? visibleUsersList,
|
||||
List<int>? invisibleUsersList,
|
||||
int visibility,
|
||||
DateTime? editedAt,
|
||||
DateTime? pinnedAt,
|
||||
@@ -371,13 +379,15 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
DateTime? publishedUntil,
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int? realmId,
|
||||
dynamic realm,
|
||||
int publisherId,
|
||||
SnPublisher publisher,
|
||||
SnMetric metric,
|
||||
SnPostPreload? preload});
|
||||
|
||||
@override
|
||||
$SnPostCopyWith<$Res>? get replyTo;
|
||||
@override
|
||||
$SnPostCopyWith<$Res>? get repostTo;
|
||||
@override
|
||||
$SnPublisherCopyWith<$Res> get publisher;
|
||||
@override
|
||||
@@ -410,7 +420,6 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
Object? aliasPrefix = freezed,
|
||||
Object? tags = null,
|
||||
Object? categories = null,
|
||||
Object? reactions = freezed,
|
||||
Object? replies = freezed,
|
||||
Object? replyId = freezed,
|
||||
Object? repostId = freezed,
|
||||
@@ -427,8 +436,6 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
Object? publishedUntil = freezed,
|
||||
Object? totalUpvote = null,
|
||||
Object? totalDownvote = null,
|
||||
Object? realmId = freezed,
|
||||
Object? realm = freezed,
|
||||
Object? publisherId = null,
|
||||
Object? publisher = null,
|
||||
Object? metric = null,
|
||||
@@ -479,38 +486,34 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
? _value._categories
|
||||
: categories // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,
|
||||
reactions: freezed == reactions
|
||||
? _value.reactions
|
||||
: reactions // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
replies: freezed == replies
|
||||
? _value.replies
|
||||
? _value._replies
|
||||
: replies // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as List<SnPost>?,
|
||||
replyId: freezed == replyId
|
||||
? _value.replyId
|
||||
: replyId // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as int?,
|
||||
repostId: freezed == repostId
|
||||
? _value.repostId
|
||||
: repostId // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as int?,
|
||||
replyTo: freezed == replyTo
|
||||
? _value.replyTo
|
||||
: replyTo // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as SnPost?,
|
||||
repostTo: freezed == repostTo
|
||||
? _value.repostTo
|
||||
: repostTo // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as SnPost?,
|
||||
visibleUsersList: freezed == visibleUsersList
|
||||
? _value.visibleUsersList
|
||||
? _value._visibleUsersList
|
||||
: visibleUsersList // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as List<int>?,
|
||||
invisibleUsersList: freezed == invisibleUsersList
|
||||
? _value.invisibleUsersList
|
||||
? _value._invisibleUsersList
|
||||
: invisibleUsersList // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as List<int>?,
|
||||
visibility: null == visibility
|
||||
? _value.visibility
|
||||
: visibility // ignore: cast_nullable_to_non_nullable
|
||||
@@ -547,14 +550,6 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
? _value.totalDownvote
|
||||
: totalDownvote // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
realmId: freezed == realmId
|
||||
? _value.realmId
|
||||
: realmId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
realm: freezed == realm
|
||||
? _value.realm
|
||||
: realm // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
publisherId: null == publisherId
|
||||
? _value.publisherId
|
||||
: publisherId // ignore: cast_nullable_to_non_nullable
|
||||
@@ -590,14 +585,13 @@ class _$SnPostImpl extends _SnPost {
|
||||
required this.aliasPrefix,
|
||||
required final List<dynamic> tags,
|
||||
required final List<dynamic> categories,
|
||||
required this.reactions,
|
||||
required this.replies,
|
||||
required final List<SnPost>? replies,
|
||||
required this.replyId,
|
||||
required this.repostId,
|
||||
required this.replyTo,
|
||||
required this.repostTo,
|
||||
required this.visibleUsersList,
|
||||
required this.invisibleUsersList,
|
||||
required final List<int>? visibleUsersList,
|
||||
required final List<int>? invisibleUsersList,
|
||||
required this.visibility,
|
||||
required this.editedAt,
|
||||
required this.pinnedAt,
|
||||
@@ -607,8 +601,6 @@ class _$SnPostImpl extends _SnPost {
|
||||
required this.publishedUntil,
|
||||
required this.totalUpvote,
|
||||
required this.totalDownvote,
|
||||
required this.realmId,
|
||||
required this.realm,
|
||||
required this.publisherId,
|
||||
required this.publisher,
|
||||
required this.metric,
|
||||
@@ -616,6 +608,9 @@ class _$SnPostImpl extends _SnPost {
|
||||
: _body = body,
|
||||
_tags = tags,
|
||||
_categories = categories,
|
||||
_replies = replies,
|
||||
_visibleUsersList = visibleUsersList,
|
||||
_invisibleUsersList = invisibleUsersList,
|
||||
super._();
|
||||
|
||||
factory _$SnPostImpl.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -661,22 +656,46 @@ class _$SnPostImpl extends _SnPost {
|
||||
return EqualUnmodifiableListView(_categories);
|
||||
}
|
||||
|
||||
final List<SnPost>? _replies;
|
||||
@override
|
||||
final dynamic reactions;
|
||||
List<SnPost>? get replies {
|
||||
final value = _replies;
|
||||
if (value == null) return null;
|
||||
if (_replies is EqualUnmodifiableListView) return _replies;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final dynamic replies;
|
||||
final int? replyId;
|
||||
@override
|
||||
final dynamic replyId;
|
||||
final int? repostId;
|
||||
@override
|
||||
final dynamic repostId;
|
||||
final SnPost? replyTo;
|
||||
@override
|
||||
final dynamic replyTo;
|
||||
final SnPost? repostTo;
|
||||
final List<int>? _visibleUsersList;
|
||||
@override
|
||||
final dynamic repostTo;
|
||||
List<int>? get visibleUsersList {
|
||||
final value = _visibleUsersList;
|
||||
if (value == null) return null;
|
||||
if (_visibleUsersList is EqualUnmodifiableListView)
|
||||
return _visibleUsersList;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
final List<int>? _invisibleUsersList;
|
||||
@override
|
||||
final dynamic visibleUsersList;
|
||||
@override
|
||||
final dynamic invisibleUsersList;
|
||||
List<int>? get invisibleUsersList {
|
||||
final value = _invisibleUsersList;
|
||||
if (value == null) return null;
|
||||
if (_invisibleUsersList is EqualUnmodifiableListView)
|
||||
return _invisibleUsersList;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final int visibility;
|
||||
@override
|
||||
@@ -696,10 +715,6 @@ class _$SnPostImpl extends _SnPost {
|
||||
@override
|
||||
final int totalDownvote;
|
||||
@override
|
||||
final int? realmId;
|
||||
@override
|
||||
final dynamic realm;
|
||||
@override
|
||||
final int publisherId;
|
||||
@override
|
||||
final SnPublisher publisher;
|
||||
@@ -710,7 +725,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, reactions: $reactions, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, realm: $realm, publisherId: $publisherId, publisher: $publisher, metric: $metric, preload: $preload)';
|
||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, publisherId: $publisherId, publisher: $publisher, metric: $metric, preload: $preload)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -735,16 +750,17 @@ class _$SnPostImpl extends _SnPost {
|
||||
const DeepCollectionEquality().equals(other._tags, _tags) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._categories, _categories) &&
|
||||
const DeepCollectionEquality().equals(other.reactions, reactions) &&
|
||||
const DeepCollectionEquality().equals(other.replies, replies) &&
|
||||
const DeepCollectionEquality().equals(other.replyId, replyId) &&
|
||||
const DeepCollectionEquality().equals(other.repostId, repostId) &&
|
||||
const DeepCollectionEquality().equals(other.replyTo, replyTo) &&
|
||||
const DeepCollectionEquality().equals(other.repostTo, repostTo) &&
|
||||
const DeepCollectionEquality().equals(other._replies, _replies) &&
|
||||
(identical(other.replyId, replyId) || other.replyId == replyId) &&
|
||||
(identical(other.repostId, repostId) ||
|
||||
other.repostId == repostId) &&
|
||||
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
|
||||
(identical(other.repostTo, repostTo) ||
|
||||
other.repostTo == repostTo) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.visibleUsersList, visibleUsersList) &&
|
||||
.equals(other._visibleUsersList, _visibleUsersList) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.invisibleUsersList, invisibleUsersList) &&
|
||||
.equals(other._invisibleUsersList, _invisibleUsersList) &&
|
||||
(identical(other.visibility, visibility) ||
|
||||
other.visibility == visibility) &&
|
||||
(identical(other.editedAt, editedAt) ||
|
||||
@@ -762,8 +778,6 @@ class _$SnPostImpl extends _SnPost {
|
||||
other.totalUpvote == totalUpvote) &&
|
||||
(identical(other.totalDownvote, totalDownvote) ||
|
||||
other.totalDownvote == totalDownvote) &&
|
||||
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
||||
const DeepCollectionEquality().equals(other.realm, realm) &&
|
||||
(identical(other.publisherId, publisherId) ||
|
||||
other.publisherId == publisherId) &&
|
||||
(identical(other.publisher, publisher) ||
|
||||
@@ -787,14 +801,13 @@ class _$SnPostImpl extends _SnPost {
|
||||
aliasPrefix,
|
||||
const DeepCollectionEquality().hash(_tags),
|
||||
const DeepCollectionEquality().hash(_categories),
|
||||
const DeepCollectionEquality().hash(reactions),
|
||||
const DeepCollectionEquality().hash(replies),
|
||||
const DeepCollectionEquality().hash(replyId),
|
||||
const DeepCollectionEquality().hash(repostId),
|
||||
const DeepCollectionEquality().hash(replyTo),
|
||||
const DeepCollectionEquality().hash(repostTo),
|
||||
const DeepCollectionEquality().hash(visibleUsersList),
|
||||
const DeepCollectionEquality().hash(invisibleUsersList),
|
||||
const DeepCollectionEquality().hash(_replies),
|
||||
replyId,
|
||||
repostId,
|
||||
replyTo,
|
||||
repostTo,
|
||||
const DeepCollectionEquality().hash(_visibleUsersList),
|
||||
const DeepCollectionEquality().hash(_invisibleUsersList),
|
||||
visibility,
|
||||
editedAt,
|
||||
pinnedAt,
|
||||
@@ -804,8 +817,6 @@ class _$SnPostImpl extends _SnPost {
|
||||
publishedUntil,
|
||||
totalUpvote,
|
||||
totalDownvote,
|
||||
realmId,
|
||||
const DeepCollectionEquality().hash(realm),
|
||||
publisherId,
|
||||
publisher,
|
||||
metric,
|
||||
@@ -841,14 +852,13 @@ abstract class _SnPost extends SnPost {
|
||||
required final String? aliasPrefix,
|
||||
required final List<dynamic> tags,
|
||||
required final List<dynamic> categories,
|
||||
required final dynamic reactions,
|
||||
required final dynamic replies,
|
||||
required final dynamic replyId,
|
||||
required final dynamic repostId,
|
||||
required final dynamic replyTo,
|
||||
required final dynamic repostTo,
|
||||
required final dynamic visibleUsersList,
|
||||
required final dynamic invisibleUsersList,
|
||||
required final List<SnPost>? replies,
|
||||
required final int? replyId,
|
||||
required final int? repostId,
|
||||
required final SnPost? replyTo,
|
||||
required final SnPost? repostTo,
|
||||
required final List<int>? visibleUsersList,
|
||||
required final List<int>? invisibleUsersList,
|
||||
required final int visibility,
|
||||
required final DateTime? editedAt,
|
||||
required final DateTime? pinnedAt,
|
||||
@@ -858,8 +868,6 @@ abstract class _SnPost extends SnPost {
|
||||
required final DateTime? publishedUntil,
|
||||
required final int totalUpvote,
|
||||
required final int totalDownvote,
|
||||
required final int? realmId,
|
||||
required final dynamic realm,
|
||||
required final int publisherId,
|
||||
required final SnPublisher publisher,
|
||||
required final SnMetric metric,
|
||||
@@ -891,21 +899,19 @@ abstract class _SnPost extends SnPost {
|
||||
@override
|
||||
List<dynamic> get categories;
|
||||
@override
|
||||
dynamic get reactions;
|
||||
List<SnPost>? get replies;
|
||||
@override
|
||||
dynamic get replies;
|
||||
int? get replyId;
|
||||
@override
|
||||
dynamic get replyId;
|
||||
int? get repostId;
|
||||
@override
|
||||
dynamic get repostId;
|
||||
SnPost? get replyTo;
|
||||
@override
|
||||
dynamic get replyTo;
|
||||
SnPost? get repostTo;
|
||||
@override
|
||||
dynamic get repostTo;
|
||||
List<int>? get visibleUsersList;
|
||||
@override
|
||||
dynamic get visibleUsersList;
|
||||
@override
|
||||
dynamic get invisibleUsersList;
|
||||
List<int>? get invisibleUsersList;
|
||||
@override
|
||||
int get visibility;
|
||||
@override
|
||||
@@ -925,10 +931,6 @@ abstract class _SnPost extends SnPost {
|
||||
@override
|
||||
int get totalDownvote;
|
||||
@override
|
||||
int? get realmId;
|
||||
@override
|
||||
dynamic get realm;
|
||||
@override
|
||||
int get publisherId;
|
||||
@override
|
||||
SnPublisher get publisher;
|
||||
@@ -1356,6 +1358,7 @@ SnMetric _$SnMetricFromJson(Map<String, dynamic> json) {
|
||||
mixin _$SnMetric {
|
||||
int get replyCount => throw _privateConstructorUsedError;
|
||||
int get reactionCount => throw _privateConstructorUsedError;
|
||||
Map<String, int> get reactionList => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnMetric to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -1372,7 +1375,7 @@ abstract class $SnMetricCopyWith<$Res> {
|
||||
factory $SnMetricCopyWith(SnMetric value, $Res Function(SnMetric) then) =
|
||||
_$SnMetricCopyWithImpl<$Res, SnMetric>;
|
||||
@useResult
|
||||
$Res call({int replyCount, int reactionCount});
|
||||
$Res call({int replyCount, int reactionCount, Map<String, int> reactionList});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -1392,6 +1395,7 @@ class _$SnMetricCopyWithImpl<$Res, $Val extends SnMetric>
|
||||
$Res call({
|
||||
Object? replyCount = null,
|
||||
Object? reactionCount = null,
|
||||
Object? reactionList = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
replyCount: null == replyCount
|
||||
@@ -1402,6 +1406,10 @@ class _$SnMetricCopyWithImpl<$Res, $Val extends SnMetric>
|
||||
? _value.reactionCount
|
||||
: reactionCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
reactionList: null == reactionList
|
||||
? _value.reactionList
|
||||
: reactionList // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
@@ -1414,7 +1422,7 @@ abstract class _$$SnMetricImplCopyWith<$Res>
|
||||
__$$SnMetricImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({int replyCount, int reactionCount});
|
||||
$Res call({int replyCount, int reactionCount, Map<String, int> reactionList});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -1432,6 +1440,7 @@ class __$$SnMetricImplCopyWithImpl<$Res>
|
||||
$Res call({
|
||||
Object? replyCount = null,
|
||||
Object? reactionCount = null,
|
||||
Object? reactionList = null,
|
||||
}) {
|
||||
return _then(_$SnMetricImpl(
|
||||
replyCount: null == replyCount
|
||||
@@ -1442,6 +1451,10 @@ class __$$SnMetricImplCopyWithImpl<$Res>
|
||||
? _value.reactionCount
|
||||
: reactionCount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
reactionList: null == reactionList
|
||||
? _value._reactionList
|
||||
: reactionList // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1449,7 +1462,11 @@ class __$$SnMetricImplCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnMetricImpl implements _SnMetric {
|
||||
const _$SnMetricImpl({required this.replyCount, required this.reactionCount});
|
||||
const _$SnMetricImpl(
|
||||
{required this.replyCount,
|
||||
required this.reactionCount,
|
||||
final Map<String, int> reactionList = const {}})
|
||||
: _reactionList = reactionList;
|
||||
|
||||
factory _$SnMetricImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnMetricImplFromJson(json);
|
||||
@@ -1458,10 +1475,18 @@ class _$SnMetricImpl implements _SnMetric {
|
||||
final int replyCount;
|
||||
@override
|
||||
final int reactionCount;
|
||||
final Map<String, int> _reactionList;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<String, int> get reactionList {
|
||||
if (_reactionList is EqualUnmodifiableMapView) return _reactionList;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_reactionList);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnMetric(replyCount: $replyCount, reactionCount: $reactionCount)';
|
||||
return 'SnMetric(replyCount: $replyCount, reactionCount: $reactionCount, reactionList: $reactionList)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1472,12 +1497,15 @@ class _$SnMetricImpl implements _SnMetric {
|
||||
(identical(other.replyCount, replyCount) ||
|
||||
other.replyCount == replyCount) &&
|
||||
(identical(other.reactionCount, reactionCount) ||
|
||||
other.reactionCount == reactionCount));
|
||||
other.reactionCount == reactionCount) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._reactionList, _reactionList));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, replyCount, reactionCount);
|
||||
int get hashCode => Object.hash(runtimeType, replyCount, reactionCount,
|
||||
const DeepCollectionEquality().hash(_reactionList));
|
||||
|
||||
/// Create a copy of SnMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -1498,7 +1526,8 @@ class _$SnMetricImpl implements _SnMetric {
|
||||
abstract class _SnMetric implements SnMetric {
|
||||
const factory _SnMetric(
|
||||
{required final int replyCount,
|
||||
required final int reactionCount}) = _$SnMetricImpl;
|
||||
required final int reactionCount,
|
||||
final Map<String, int> reactionList}) = _$SnMetricImpl;
|
||||
|
||||
factory _SnMetric.fromJson(Map<String, dynamic> json) =
|
||||
_$SnMetricImpl.fromJson;
|
||||
@@ -1507,6 +1536,8 @@ abstract class _SnMetric implements SnMetric {
|
||||
int get replyCount;
|
||||
@override
|
||||
int get reactionCount;
|
||||
@override
|
||||
Map<String, int> get reactionList;
|
||||
|
||||
/// Create a copy of SnMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
||||
@@ -20,14 +20,23 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
||||
aliasPrefix: json['alias_prefix'] as String?,
|
||||
tags: json['tags'] as List<dynamic>,
|
||||
categories: json['categories'] as List<dynamic>,
|
||||
reactions: json['reactions'],
|
||||
replies: json['replies'],
|
||||
replyId: json['reply_id'],
|
||||
repostId: json['repost_id'],
|
||||
replyTo: json['reply_to'],
|
||||
repostTo: json['repost_to'],
|
||||
visibleUsersList: json['visible_users_list'],
|
||||
invisibleUsersList: json['invisible_users_list'],
|
||||
replies: (json['replies'] as List<dynamic>?)
|
||||
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
replyId: (json['reply_id'] as num?)?.toInt(),
|
||||
repostId: (json['repost_id'] as num?)?.toInt(),
|
||||
replyTo: json['reply_to'] == null
|
||||
? null
|
||||
: SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
|
||||
repostTo: json['repost_to'] == null
|
||||
? null
|
||||
: SnPost.fromJson(json['repost_to'] as Map<String, dynamic>),
|
||||
visibleUsersList: (json['visible_users_list'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
invisibleUsersList: (json['invisible_users_list'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
visibility: (json['visibility'] as num).toInt(),
|
||||
editedAt: json['edited_at'] == null
|
||||
? null
|
||||
@@ -47,8 +56,6 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
||||
: DateTime.parse(json['published_until'] as String),
|
||||
totalUpvote: (json['total_upvote'] as num).toInt(),
|
||||
totalDownvote: (json['total_downvote'] as num).toInt(),
|
||||
realmId: (json['realm_id'] as num?)?.toInt(),
|
||||
realm: json['realm'],
|
||||
publisherId: (json['publisher_id'] as num).toInt(),
|
||||
publisher:
|
||||
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||
@@ -71,12 +78,11 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
||||
'alias_prefix': instance.aliasPrefix,
|
||||
'tags': instance.tags,
|
||||
'categories': instance.categories,
|
||||
'reactions': instance.reactions,
|
||||
'replies': instance.replies,
|
||||
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
||||
'reply_id': instance.replyId,
|
||||
'repost_id': instance.repostId,
|
||||
'reply_to': instance.replyTo,
|
||||
'repost_to': instance.repostTo,
|
||||
'reply_to': instance.replyTo?.toJson(),
|
||||
'repost_to': instance.repostTo?.toJson(),
|
||||
'visible_users_list': instance.visibleUsersList,
|
||||
'invisible_users_list': instance.invisibleUsersList,
|
||||
'visibility': instance.visibility,
|
||||
@@ -88,8 +94,6 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
||||
'published_until': instance.publishedUntil?.toIso8601String(),
|
||||
'total_upvote': instance.totalUpvote,
|
||||
'total_downvote': instance.totalDownvote,
|
||||
'realm_id': instance.realmId,
|
||||
'realm': instance.realm,
|
||||
'publisher_id': instance.publisherId,
|
||||
'publisher': instance.publisher.toJson(),
|
||||
'metric': instance.metric.toJson(),
|
||||
@@ -131,12 +135,17 @@ _$SnMetricImpl _$$SnMetricImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnMetricImpl(
|
||||
replyCount: (json['reply_count'] as num).toInt(),
|
||||
reactionCount: (json['reaction_count'] as num).toInt(),
|
||||
reactionList: (json['reaction_list'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||
) ??
|
||||
const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnMetricImplToJson(_$SnMetricImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'reply_count': instance.replyCount,
|
||||
'reaction_count': instance.reactionCount,
|
||||
'reaction_list': instance.reactionList,
|
||||
};
|
||||
|
||||
_$SnPublisherImpl _$$SnPublisherImplFromJson(Map<String, dynamic> json) =>
|
||||
|
||||
20
lib/types/reaction.dart
Normal file
20
lib/types/reaction.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
class ReactInfo {
|
||||
final String icon;
|
||||
final int attitude;
|
||||
|
||||
const ReactInfo({required this.icon, required this.attitude});
|
||||
}
|
||||
|
||||
const Map<String, ReactInfo> kTemplateReactions = {
|
||||
'thumb_up': ReactInfo(icon: '👍', attitude: 1),
|
||||
'thumb_down': ReactInfo(icon: '👎', attitude: 2),
|
||||
'just_okay': ReactInfo(icon: '😅', attitude: 0),
|
||||
'cry': ReactInfo(icon: '😭', attitude: 0),
|
||||
'confuse': ReactInfo(icon: '🧐', attitude: 0),
|
||||
'clap': ReactInfo(icon: '👏', attitude: 1),
|
||||
'laugh': ReactInfo(icon: '😂', attitude: 1),
|
||||
'angry': ReactInfo(icon: '😡', attitude: 2),
|
||||
'party': ReactInfo(icon: '🎉', attitude: 1),
|
||||
'joy': ReactInfo(icon: '🤣', attitude: 1),
|
||||
'pray': ReactInfo(icon: '🙏', attitude: 1),
|
||||
};
|
||||
@@ -28,6 +28,9 @@ class AttachmentDetailPopup extends StatelessWidget {
|
||||
tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}',
|
||||
child: PhotoView(
|
||||
key: Key('attachment-detail-${data.rid}-$heroTag'),
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
),
|
||||
imageProvider: UniversalImage.provider(
|
||||
sn.getAttachmentUrl(data.rid),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_detail.dart';
|
||||
@@ -23,15 +29,11 @@ class AttachmentItem extends StatelessWidget {
|
||||
case 'image':
|
||||
return Hero(
|
||||
tag: 'attachment-${data.rid}-$heroTag',
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return UniversalImage(
|
||||
sn.getAttachmentUrl(data.rid),
|
||||
key: Key('attachment-${data.rid}-$heroTag'),
|
||||
fit: BoxFit.cover,
|
||||
cacheHeight: constraints.maxHeight,
|
||||
cacheWidth: constraints.maxWidth,
|
||||
);
|
||||
}),
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data.rid),
|
||||
key: Key('attachment-${data.rid}-$heroTag'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
default:
|
||||
return const Placeholder();
|
||||
@@ -43,6 +45,12 @@ class AttachmentItem extends StatelessWidget {
|
||||
final uuid = Uuid();
|
||||
final heroTag = uuid.v4();
|
||||
|
||||
if (data.isMature) {
|
||||
return _AttachmentItemSensitiveBlur(
|
||||
child: _buildContent(context, heroTag),
|
||||
);
|
||||
}
|
||||
|
||||
if (isExpandable) {
|
||||
return GestureDetector(
|
||||
child: _buildContent(context, heroTag),
|
||||
@@ -58,3 +66,87 @@ class AttachmentItem extends StatelessWidget {
|
||||
return _buildContent(context, heroTag);
|
||||
}
|
||||
}
|
||||
|
||||
class _AttachmentItemSensitiveBlur extends StatefulWidget {
|
||||
final Widget child;
|
||||
const _AttachmentItemSensitiveBlur({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<_AttachmentItemSensitiveBlur> createState() =>
|
||||
_AttachmentItemSensitiveBlurState();
|
||||
}
|
||||
|
||||
class _AttachmentItemSensitiveBlurState
|
||||
extends State<_AttachmentItemSensitiveBlur> {
|
||||
bool _doesShow = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
widget.child,
|
||||
ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.visibility_off,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
const Gap(8),
|
||||
Text('sensitiveContent')
|
||||
.tr()
|
||||
.fontSize(20)
|
||||
.textColor(Colors.white)
|
||||
.bold(),
|
||||
Text('sensitiveContentDescription')
|
||||
.tr()
|
||||
.fontSize(14)
|
||||
.textColor(Colors.white.withOpacity(0.8)),
|
||||
const Gap(16),
|
||||
InkWell(
|
||||
child: Text('sensitiveContentReveal')
|
||||
.tr()
|
||||
.textColor(Colors.white),
|
||||
onTap: () {
|
||||
setState(() => _doesShow = !_doesShow);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.opacity(_doesShow ? 0 : 1, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
||||
if (_doesShow)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: InkWell(
|
||||
child: Icon(
|
||||
Symbols.visibility_off,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
blurRadius: 3,
|
||||
offset: Offset(0, 1.5),
|
||||
),
|
||||
],
|
||||
).padding(all: 12),
|
||||
onTap: () {
|
||||
setState(() => _doesShow = !_doesShow);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,15 @@ import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
class AttachmentList extends StatelessWidget {
|
||||
final List<SnAttachment> data;
|
||||
final bool? bordered;
|
||||
final double? maxListHeight;
|
||||
final double? maxHeight;
|
||||
const AttachmentList({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.bordered,
|
||||
this.maxListHeight,
|
||||
this.maxHeight,
|
||||
});
|
||||
|
||||
static const double kMaxListItemWidth = 520;
|
||||
static const double kMaxItemWidth = 520;
|
||||
static const BorderRadius kDefaultRadius =
|
||||
BorderRadius.all(Radius.circular(8));
|
||||
|
||||
@@ -33,9 +33,10 @@ class AttachmentList extends StatelessWidget {
|
||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight ?? double.infinity,
|
||||
maxWidth: math.min(
|
||||
MediaQuery.of(context).size.width - 20,
|
||||
kMaxListItemWidth,
|
||||
kMaxItemWidth,
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
@@ -64,7 +65,7 @@ class AttachmentList extends StatelessWidget {
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxHeight: maxListHeight ?? 320),
|
||||
constraints: BoxConstraints(maxHeight: maxHeight ?? 320),
|
||||
child: ScrollConfiguration(
|
||||
behavior: _AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
@@ -73,9 +74,10 @@ class AttachmentList extends StatelessWidget {
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight ?? double.infinity,
|
||||
maxWidth: math.min(
|
||||
MediaQuery.of(context).size.width - 20,
|
||||
kMaxListItemWidth,
|
||||
kMaxItemWidth,
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:surface/widgets/navigation/app_destinations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/navigation.dart';
|
||||
|
||||
class AppBottomNavigationBar extends StatefulWidget {
|
||||
const AppBottomNavigationBar({super.key});
|
||||
@@ -10,23 +12,46 @@ class AppBottomNavigationBar extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
|
||||
int _currentIndex = 0;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context
|
||||
.read<NavigationProvider>()
|
||||
.autoDetectIndex(GoRouter.maybeOf(context));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BottomNavigationBar(
|
||||
currentIndex: _currentIndex,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
showUnselectedLabels: false,
|
||||
items: appDestinations.map((ele) {
|
||||
return BottomNavigationBarItem(
|
||||
icon: ele.icon,
|
||||
label: ele.label,
|
||||
final nav = context.watch<NavigationProvider>();
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: nav,
|
||||
builder: (context, _) {
|
||||
if (!nav.isIndexInRange(0, nav.pinnedDestinationCount)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final destinations = [
|
||||
...nav.destinations.where((ele) => ele.isPinned),
|
||||
];
|
||||
|
||||
return BottomNavigationBar(
|
||||
currentIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount),
|
||||
type: BottomNavigationBarType.fixed,
|
||||
showUnselectedLabels: false,
|
||||
items: destinations.map((ele) {
|
||||
return BottomNavigationBarItem(
|
||||
icon: ele.icon,
|
||||
label: ele.label.tr(),
|
||||
);
|
||||
}).toList(),
|
||||
onTap: (idx) {
|
||||
nav.setIndex(idx);
|
||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
onTap: (idx) {
|
||||
setState(() => _currentIndex = idx);
|
||||
GoRouter.of(context).goNamed(appDestinations[idx].screen);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class AppNavDestination {
|
||||
final String label;
|
||||
final String screen;
|
||||
final Widget icon;
|
||||
|
||||
AppNavDestination({
|
||||
required this.label,
|
||||
required this.screen,
|
||||
required this.icon,
|
||||
});
|
||||
}
|
||||
|
||||
List<AppNavDestination> appDestinations = [
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.home, weight: 400, opticalSize: 20),
|
||||
screen: 'home',
|
||||
label: tr('screenHome'),
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.explore, weight: 400, opticalSize: 20),
|
||||
screen: 'explore',
|
||||
label: tr('screenExplore'),
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
|
||||
screen: 'account',
|
||||
label: tr('screenAccount'),
|
||||
),
|
||||
];
|
||||
85
lib/widgets/navigation/app_drawer_navigation.dart
Normal file
85
lib/widgets/navigation/app_drawer_navigation.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/navigation.dart';
|
||||
|
||||
class AppNavigationDrawer extends StatefulWidget {
|
||||
const AppNavigationDrawer({super.key});
|
||||
|
||||
@override
|
||||
State<AppNavigationDrawer> createState() => _AppNavigationDrawerState();
|
||||
}
|
||||
|
||||
class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context
|
||||
.read<NavigationProvider>()
|
||||
.autoDetectIndex(GoRouter.maybeOf(context));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final nav = context.watch<NavigationProvider>();
|
||||
|
||||
final backgroundColor = ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? Theme.of(context).colorScheme.surface
|
||||
: null;
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: nav,
|
||||
builder: (context, _) {
|
||||
final destinations = [
|
||||
...nav.destinations.where((ele) => ele.isPinned),
|
||||
...nav.destinations.where((ele) => !ele.isPinned),
|
||||
];
|
||||
|
||||
return NavigationDrawer(
|
||||
backgroundColor: backgroundColor,
|
||||
selectedIndex: nav.currentIndex,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Solar Network').bold(),
|
||||
Text('Solar Network 2.0α').fontSize(12).textColor(
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.5)),
|
||||
],
|
||||
).padding(
|
||||
horizontal: 32,
|
||||
top: math.max(MediaQuery.of(context).padding.top, 16),
|
||||
bottom: 16,
|
||||
),
|
||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
||||
return NavigationDrawerDestination(
|
||||
icon: ele.icon,
|
||||
label: Text(ele.label).tr(),
|
||||
);
|
||||
}),
|
||||
const Divider(),
|
||||
...destinations.where((ele) => !ele.isPinned).map((ele) {
|
||||
return NavigationDrawerDestination(
|
||||
icon: ele.icon,
|
||||
label: Text(ele.label).tr(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onDestinationSelected: (idx) {
|
||||
nav.setIndex(idx);
|
||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||
Scaffold.of(context).closeDrawer();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
|
||||
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
|
||||
|
||||
class AppScaffold extends StatelessWidget {
|
||||
final PreferredSizeWidget? appBar;
|
||||
@@ -14,6 +15,7 @@ class AppScaffold extends StatelessWidget {
|
||||
final Widget? body;
|
||||
final bool autoImplyAppBar;
|
||||
final bool showBottomNavigation;
|
||||
final bool showDrawer;
|
||||
const AppScaffold({
|
||||
super.key,
|
||||
this.appBar,
|
||||
@@ -23,17 +25,21 @@ class AppScaffold extends StatelessWidget {
|
||||
this.body,
|
||||
this.autoImplyAppBar = false,
|
||||
this.showBottomNavigation = false,
|
||||
this.showDrawer = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isShowDrawer = showDrawer
|
||||
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
||||
: false;
|
||||
final isShowBottomNavigation = (showBottomNavigation)
|
||||
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
||||
: false;
|
||||
|
||||
final state = GoRouter.maybeOf(context);
|
||||
|
||||
return AppBackground(
|
||||
final innerWidget = AppBackground(
|
||||
child: Scaffold(
|
||||
appBar: appBar ??
|
||||
(autoImplyAppBar
|
||||
@@ -50,9 +56,22 @@ class AppScaffold extends StatelessWidget {
|
||||
body: body,
|
||||
floatingActionButtonLocation: floatingActionButtonLocation,
|
||||
floatingActionButton: floatingActionButton,
|
||||
drawer: isShowDrawer ? AppNavigationDrawer() : null,
|
||||
bottomNavigationBar:
|
||||
isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
||||
),
|
||||
);
|
||||
|
||||
if (showDrawer) {
|
||||
return Row(
|
||||
children: [
|
||||
AppNavigationDrawer(),
|
||||
VerticalDivider(width: 1, color: Theme.of(context).dividerColor),
|
||||
Expanded(child: innerWidget),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return innerWidget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class PostCommentSliverList extends StatefulWidget {
|
||||
@@ -16,10 +17,10 @@ class PostCommentSliverList extends StatefulWidget {
|
||||
const PostCommentSliverList({super.key, required this.parentPostId});
|
||||
|
||||
@override
|
||||
State<PostCommentSliverList> createState() => _PostCommentSliverListState();
|
||||
State<PostCommentSliverList> createState() => PostCommentSliverListState();
|
||||
}
|
||||
|
||||
class _PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
bool _isBusy = true;
|
||||
|
||||
final List<SnPost> _posts = List.empty(growable: true);
|
||||
@@ -67,6 +68,11 @@ class _PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
if (mounted) setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
_posts.clear();
|
||||
_fetchPosts();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -97,7 +103,7 @@ class _PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
}
|
||||
}
|
||||
|
||||
class PostCommentListPopup extends StatelessWidget {
|
||||
class PostCommentListPopup extends StatefulWidget {
|
||||
final int postId;
|
||||
final int commentCount;
|
||||
const PostCommentListPopup({
|
||||
@@ -106,8 +112,17 @@ class PostCommentListPopup extends StatelessWidget {
|
||||
this.commentCount = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PostCommentListPopup> createState() => _PostCommentListPopupState();
|
||||
}
|
||||
|
||||
class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
||||
final GlobalKey<PostCommentSliverListState> _childListKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -117,14 +132,36 @@ class PostCommentListPopup extends StatelessWidget {
|
||||
const Icon(Symbols.comment, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCommentsDetailed')
|
||||
.plural(commentCount)
|
||||
.plural(widget.commentCount)
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
PostCommentSliverList(parentPostId: postId),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 240,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: widget.postId,
|
||||
onPost: () {
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
parentPostId: widget.postId,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
@@ -12,30 +13,52 @@ import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_reaction.dart';
|
||||
|
||||
class PostItem extends StatelessWidget {
|
||||
final SnPost data;
|
||||
final bool showReactions;
|
||||
final bool showComments;
|
||||
final Function(SnPost data)? onChanged;
|
||||
const PostItem({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.showReactions = true,
|
||||
this.showComments = true,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
void _onChanged(SnPost data) {
|
||||
if (onChanged != null) onChanged!(data);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isListAttachments =
|
||||
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ||
|
||||
(data.preload?.attachments?.length ?? 0) > 1;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostContentHeader(data: data).padding(horizontal: 12, vertical: 8),
|
||||
_PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6),
|
||||
if (data.repostTo != null)
|
||||
_PostQuoteContent(child: data.repostTo!).padding(
|
||||
horizontal: 12,
|
||||
),
|
||||
if (data.preload?.attachments?.isNotEmpty ?? true)
|
||||
AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
bordered: true,
|
||||
),
|
||||
_PostBottomAction(data: data, showComments: showComments)
|
||||
.padding(left: 12, right: 18),
|
||||
maxHeight: 520,
|
||||
).padding(horizontal: isListAttachments ? 12 : 0),
|
||||
_PostBottomAction(
|
||||
data: data,
|
||||
showComments: showComments,
|
||||
showReactions: showReactions,
|
||||
onChanged: _onChanged,
|
||||
).padding(left: 12, right: 18),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -44,7 +67,14 @@ class PostItem extends StatelessWidget {
|
||||
class _PostBottomAction extends StatelessWidget {
|
||||
final SnPost data;
|
||||
final bool showComments;
|
||||
const _PostBottomAction({required this.data, required this.showComments});
|
||||
final bool showReactions;
|
||||
final Function(SnPost data) onChanged;
|
||||
const _PostBottomAction({
|
||||
required this.data,
|
||||
required this.showComments,
|
||||
required this.showReactions,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -56,16 +86,40 @@ class _PostBottomAction extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.add_reaction, size: 20, color: iconColor),
|
||||
const Gap(8),
|
||||
Text('postReact').tr(),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 8),
|
||||
onTap: () {},
|
||||
),
|
||||
if (showReactions)
|
||||
InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.add_reaction, size: 20, color: iconColor),
|
||||
const Gap(8),
|
||||
if (data.totalDownvote > 0 || data.totalUpvote > 0)
|
||||
Text('postReactionPoints').plural(
|
||||
data.totalUpvote - data.totalDownvote,
|
||||
)
|
||||
else
|
||||
Text('postReact').tr(),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 8),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => PostReactionPopup(
|
||||
data: data,
|
||||
onChanged: (value, isPositive, delta) {
|
||||
onChanged(data.copyWith(
|
||||
totalUpvote: isPositive
|
||||
? data.totalUpvote + delta
|
||||
: data.totalUpvote,
|
||||
totalDownvote: !isPositive
|
||||
? data.totalDownvote + delta
|
||||
: data.totalDownvote,
|
||||
metric: data.metric.copyWith(reactionList: value),
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (showComments)
|
||||
InkWell(
|
||||
child: Row(
|
||||
@@ -104,7 +158,13 @@ class _PostBottomAction extends StatelessWidget {
|
||||
|
||||
class _PostContentHeader extends StatelessWidget {
|
||||
final SnPost data;
|
||||
const _PostContentHeader({required this.data});
|
||||
final bool isCompact;
|
||||
final bool showActions;
|
||||
const _PostContentHeader({
|
||||
required this.data,
|
||||
this.isCompact = false,
|
||||
this.showActions = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -113,13 +173,16 @@ class _PostContentHeader extends StatelessWidget {
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
AccountImage(content: data.publisher.avatar),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
AccountImage(
|
||||
content: data.publisher.avatar,
|
||||
radius: isCompact ? 12 : 20,
|
||||
),
|
||||
Gap(isCompact ? 8 : 12),
|
||||
if (isCompact)
|
||||
Row(
|
||||
children: [
|
||||
Text(data.publisher.nick).bold(),
|
||||
const Gap(4),
|
||||
Row(
|
||||
children: [
|
||||
Text('@${data.publisher.name}').fontSize(13),
|
||||
@@ -130,86 +193,104 @@ class _PostContentHeader extends StatelessWidget {
|
||||
],
|
||||
).opacity(0.8),
|
||||
],
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(data.publisher.nick).bold(),
|
||||
Row(
|
||||
children: [
|
||||
Text('@${data.publisher.name}').fontSize(13),
|
||||
const Gap(4),
|
||||
Text(RelativeTime(context).format(
|
||||
data.publishedAt ?? data.createdAt,
|
||||
)).fontSize(13),
|
||||
],
|
||||
).opacity(0.8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Symbols.more_horiz),
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
if (isAuthor)
|
||||
if (showActions)
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Symbols.more_horiz),
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
if (isAuthor)
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const Gap(16),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
queryParameters: {'editing': data.id.toString()},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isAuthor)
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete),
|
||||
const Gap(16),
|
||||
Text('delete').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isAuthor) const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const Icon(Symbols.reply),
|
||||
const Gap(16),
|
||||
Text('edit').tr(),
|
||||
Text('reply').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
queryParameters: {'editing': data.id.toString()},
|
||||
queryParameters: {'replying': data.id.toString()},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isAuthor)
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete),
|
||||
const Icon(Symbols.forward),
|
||||
const Gap(16),
|
||||
Text('delete').tr(),
|
||||
Text('repost').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
queryParameters: {'reposting': data.id.toString()},
|
||||
);
|
||||
},
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.flag),
|
||||
const Gap(16),
|
||||
Text('report').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isAuthor) const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.reply),
|
||||
const Gap(16),
|
||||
Text('reply').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
queryParameters: {'replying': data.id.toString()},
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.forward),
|
||||
const Gap(16),
|
||||
Text('repost').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
queryParameters: {'reposting': data.id.toString()},
|
||||
);
|
||||
},
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.flag),
|
||||
const Gap(16),
|
||||
Text('report').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -225,3 +306,29 @@ class _PostContentBody extends StatelessWidget {
|
||||
return MarkdownTextContent(content: data['content']);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostQuoteContent extends StatelessWidget {
|
||||
final SnPost child;
|
||||
const _PostQuoteContent({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
_PostContentHeader(data: child, isCompact: true, showActions: false)
|
||||
.padding(bottom: 4),
|
||||
_PostContentBody(data: child.body),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_detail.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class PostMediaPendingList extends StatelessWidget {
|
||||
final PostWriteController controller;
|
||||
@@ -36,6 +39,8 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
if (result == null) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
controller.setIsBusy(true);
|
||||
|
||||
final rawBytes =
|
||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||
.buffer
|
||||
@@ -44,6 +49,26 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
idx,
|
||||
PostWriteMedia.fromBytes(rawBytes, media.name, media.type),
|
||||
);
|
||||
|
||||
controller.setIsBusy(false);
|
||||
}
|
||||
|
||||
void _deleteAttachment(BuildContext context, int idx) async {
|
||||
final media = controller.attachments[idx];
|
||||
if (media.attachment == null) return;
|
||||
|
||||
controller.setIsBusy(true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/uc/attachments/${media.attachment!.id}');
|
||||
controller.removeAttachmentAt(idx);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
controller.setIsBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -84,6 +109,14 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
icon: Symbols.crop,
|
||||
onSelected: () => _cropImage(context, idx),
|
||||
),
|
||||
if (media.attachment != null)
|
||||
MenuItem(
|
||||
label: 'delete'.tr(),
|
||||
icon: Symbols.delete,
|
||||
onSelected: controller.isBusy
|
||||
? null
|
||||
: () => _deleteAttachment(context, idx),
|
||||
),
|
||||
if (media.attachment == null)
|
||||
MenuItem(
|
||||
label: 'delete'.tr(),
|
||||
|
||||
@@ -1,10 +1,236 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
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/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
|
||||
class PostMiniEditor extends StatelessWidget {
|
||||
const PostMiniEditor({super.key});
|
||||
class PostMiniEditor extends StatefulWidget {
|
||||
final int? postReplyId;
|
||||
final Function? onPost;
|
||||
const PostMiniEditor({super.key, this.postReplyId, this.onPost});
|
||||
|
||||
@override
|
||||
State<PostMiniEditor> createState() => _PostMiniEditorState();
|
||||
}
|
||||
|
||||
class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
final PostWriteController _writeController = PostWriteController();
|
||||
|
||||
bool _isFetching = false;
|
||||
bool get _isLoading => _isFetching || _writeController.isLoading;
|
||||
|
||||
List<SnPublisher>? _publishers;
|
||||
|
||||
Future<void> _fetchPublishers() async {
|
||||
setState(() => _isFetching = true);
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/publishers');
|
||||
_publishers = List<SnPublisher>.from(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||
);
|
||||
_writeController.setPublisher(_publishers?.firstOrNull);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isFetching = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchPublishers();
|
||||
_writeController.fetchRelatedPost(
|
||||
context,
|
||||
replying: widget.postReplyId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_writeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
return ListenableBuilder(
|
||||
listenable: _writeController,
|
||||
builder: (context, _) {
|
||||
return Column(
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnPublisher>(
|
||||
isExpanded: true,
|
||||
hint: Text(
|
||||
'fieldPostPublisher',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
).tr(),
|
||||
items: <DropdownMenuItem<SnPublisher>>[
|
||||
...(_publishers?.map(
|
||||
(item) => DropdownMenuItem<SnPublisher>(
|
||||
enabled: _writeController.editingPost == null,
|
||||
value: item,
|
||||
child: Row(
|
||||
children: [
|
||||
AccountImage(content: item.avatar, radius: 16),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.nick).textStyle(
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!),
|
||||
Text('@${item.name}')
|
||||
.textStyle(Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!)
|
||||
.fontSize(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
) ??
|
||||
[]),
|
||||
DropdownMenuItem<SnPublisher>(
|
||||
value: null,
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
child: const Icon(Symbols.add),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('publishersNew').tr().textStyle(
|
||||
Theme.of(context).textTheme.bodyMedium!),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
value: _writeController.publisher,
|
||||
onChanged: (SnPublisher? value) {
|
||||
if (value == null) {
|
||||
GoRouter.of(context)
|
||||
.pushNamed('accountPublisherNew')
|
||||
.then((value) {
|
||||
if (value == true) {
|
||||
_publishers = null;
|
||||
_fetchPublishers();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_writeController.setPublisher(value);
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
height: 48,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _writeController.contentController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'fieldPostContent'.tr(),
|
||||
hintStyle: TextStyle(fontSize: 14),
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
LoadingIndicator(isActive: _isLoading),
|
||||
if (_writeController.isBusy && _writeController.progress != null)
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: _writeController.progress),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) =>
|
||||
LinearProgressIndicator(value: value, minHeight: 2),
|
||||
)
|
||||
else if (_writeController.isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Symbols.launch,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {
|
||||
if (widget.postReplyId != null)
|
||||
'replying': widget.postReplyId.toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: (_writeController.isBusy ||
|
||||
_writeController.publisher == null)
|
||||
? null
|
||||
: () {
|
||||
_writeController.post(context).then((_) {
|
||||
if (!context.mounted) return;
|
||||
if (widget.onPost != null) widget.onPost!();
|
||||
context.showSnackbar('postPosted'.tr());
|
||||
_writeController.reset();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Symbols.send),
|
||||
label: Text('postPublish').tr(),
|
||||
),
|
||||
],
|
||||
).padding(left: 12, right: 16, bottom: 4),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
128
lib/widgets/post/post_reaction.dart
Normal file
128
lib/widgets/post/post_reaction.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/reaction.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class PostReactionPopup extends StatefulWidget {
|
||||
final SnPost data;
|
||||
final Function(Map<String, int> value, bool isPositive, int delta)? onChanged;
|
||||
const PostReactionPopup({super.key, required this.data, this.onChanged});
|
||||
|
||||
@override
|
||||
State<PostReactionPopup> createState() => _PostReactionPopupState();
|
||||
}
|
||||
|
||||
class _PostReactionPopupState extends State<PostReactionPopup> {
|
||||
bool _isSubmitting = false;
|
||||
late Map<String, int> _reactions;
|
||||
|
||||
Future<void> _reactPost(String symbol, int attitude) async {
|
||||
if (_isSubmitting) return;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
try {
|
||||
setState(() => _isSubmitting = true);
|
||||
final resp = await sn.client.post(
|
||||
'/cgi/co/posts/${widget.data.id}/react',
|
||||
data: {
|
||||
'symbol': symbol,
|
||||
'attitude': attitude,
|
||||
},
|
||||
);
|
||||
if (resp.statusCode == 201) {
|
||||
_reactions[symbol] = (_reactions[symbol] ?? 0) + 1;
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showSnackbar('postReactCompleted'.tr());
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(
|
||||
_reactions,
|
||||
kTemplateReactions[symbol]!.attitude == 1,
|
||||
1,
|
||||
);
|
||||
}
|
||||
} else if (resp.statusCode == 204) {
|
||||
_reactions[symbol] = (_reactions[symbol] ?? 0) - 1;
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showSnackbar('postReactUncompleted'.tr());
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(
|
||||
_reactions,
|
||||
kTemplateReactions[symbol]!.attitude == 1,
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_reactions = Map.from(widget.data.metric.reactionList);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.mood, size: 24),
|
||||
const Gap(16),
|
||||
Text('postReactions')
|
||||
.tr()
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisCount: 4,
|
||||
children: kTemplateReactions.entries.map((e) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (widget.onChanged == null) return;
|
||||
_reactPost(e.key, e.value.attitude).then((_) {
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(e.value.icon).fontSize(40),
|
||||
Text(
|
||||
e.key,
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
const Gap(6),
|
||||
Text(
|
||||
'x${_reactions[e.key]?.toString() ?? '0'}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -30,67 +30,21 @@ class UniversalImage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
final double? resizeHeight =
|
||||
cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
|
||||
final double? resizeWidth =
|
||||
cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
|
||||
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
memCacheHeight: cacheHeight != null
|
||||
? (cacheHeight! * devicePixelRatio).round()
|
||||
: null,
|
||||
memCacheWidth: cacheWidth != null
|
||||
? (cacheWidth! * devicePixelRatio).round()
|
||||
: null,
|
||||
progressIndicatorBuilder: noProgressIndicator
|
||||
? null
|
||||
: (context, url, downloadProgress) => Center(
|
||||
child: TweenAnimationBuilder(
|
||||
tween: Tween(
|
||||
begin: 0,
|
||||
end: downloadProgress.progress ?? 0,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => CircularProgressIndicator(
|
||||
value: downloadProgress.progress != null
|
||||
? value.toDouble()
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget: noErrorWidget
|
||||
? null
|
||||
: (context, url, error) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimateWidgetExtensions(Icon(Symbols.close, size: 24))
|
||||
.animate(onPlay: (e) => e.repeat(reverse: true))
|
||||
.fade(duration: 500.ms),
|
||||
Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
).center(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return Image.network(
|
||||
url,
|
||||
return Image(
|
||||
image: ResizeImage(
|
||||
UniversalImage.provider(url),
|
||||
width: resizeWidth?.round(),
|
||||
height: resizeHeight?.round(),
|
||||
policy: ResizeImagePolicy.fit,
|
||||
),
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
cacheHeight: cacheHeight != null
|
||||
? (cacheHeight! * devicePixelRatio).round()
|
||||
: null,
|
||||
cacheWidth:
|
||||
cacheWidth != null ? (cacheWidth! * devicePixelRatio).round() : null,
|
||||
loadingBuilder: noProgressIndicator
|
||||
? null
|
||||
: (BuildContext context, Widget child,
|
||||
@@ -146,3 +100,37 @@ class UniversalImage extends StatelessWidget {
|
||||
return NetworkImage(url);
|
||||
}
|
||||
}
|
||||
|
||||
class AutoResizeUniversalImage extends StatelessWidget {
|
||||
final String url;
|
||||
final double? width, height;
|
||||
final BoxFit? fit;
|
||||
final bool noProgressIndicator;
|
||||
final bool noErrorWidget;
|
||||
|
||||
const AutoResizeUniversalImage(
|
||||
this.url, {
|
||||
super.key,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit,
|
||||
this.noProgressIndicator = false,
|
||||
this.noErrorWidget = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return UniversalImage(
|
||||
url,
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
noProgressIndicator: noProgressIndicator,
|
||||
noErrorWidget: noErrorWidget,
|
||||
cacheHeight: constraints.maxHeight,
|
||||
cacheWidth: constraints.maxWidth,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
48
pubspec.lock
48
pubspec.lock
@@ -5,23 +5,23 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "76.0.0"
|
||||
version: "72.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
version: "0.3.2"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.11.0"
|
||||
version: "6.7.0"
|
||||
animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -202,10 +202,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.18.0"
|
||||
connectivity_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -410,10 +410,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877
|
||||
sha256: "16dc141db5a2ccc6520ebb6a2eb5945b1b09e95085c021d9f914f8ded7f1465c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.3"
|
||||
version: "8.1.4"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -790,18 +790,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "10.0.5"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.5"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -838,10 +838,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3-main.0"
|
||||
version: "0.1.2-main.4"
|
||||
markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1150,7 +1150,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
version: "0.0.99"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1227,10 +1227,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1251,10 +1251,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.2.0"
|
||||
styled_widget:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1291,10 +1291,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.2"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1411,10 +1411,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.0"
|
||||
version: "14.2.5"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -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: 1.0.0+1
|
||||
version: 2.0.0+2
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@@ -166,4 +166,4 @@ flutter_native_splash:
|
||||
color_dark: "#000000"
|
||||
branding: "assets/icon/branding-light.png"
|
||||
branding_dark: "assets/icon/branding-dark.png"
|
||||
branding_bottom_padding: 24
|
||||
branding_bottom_padding: 24
|
||||
|
||||
9
roadsign.toml
Normal file
9
roadsign.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
id = "solian-next"
|
||||
|
||||
[[locations]]
|
||||
id = "solian-next"
|
||||
host = ["sn-next.solsynth.dev"]
|
||||
path = ["/"]
|
||||
[[locations.destinations]]
|
||||
id = "solian-next-web"
|
||||
uri = "files:///workdir/solian-next?fallback=index.html&index=index.html"
|
||||
Reference in New Issue
Block a user