Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
3ac6822ab6 | |||
7a5fd2e468 | |||
e1ddd22e4e | |||
22b2ae32e9 | |||
9d5c452eae | |||
0fdb1e4ead | |||
724bd6592e | |||
2d347e0d41 |
@ -22,9 +22,9 @@
|
||||
"explore": "Explore",
|
||||
"posts": "Posts",
|
||||
"unlink": "Unlink",
|
||||
"feedSearch": "Search Feed",
|
||||
"feedSearchWithTag": "Searching with tag #@key",
|
||||
"feedSearchWithCategory": "Searching in category @category",
|
||||
"postSearch": "Search Post",
|
||||
"postSearchWithTag": "Searching with tag #@key",
|
||||
"postSearchWithCategory": "Searching in category @category",
|
||||
"feedUnreadCount": "@count posts you may missed",
|
||||
"messages": "Messages",
|
||||
"messagesUnreadCount": "@count messages unread",
|
||||
@ -453,5 +453,9 @@
|
||||
"accountDeletionConfirm": "Confirm request account deletion",
|
||||
"accountDeletionConfirmDesc": "Are you sure to delete account @account? You will receive a confirmation email with a link to confirm the deletion of the account within 24 hours. Note that this action is irreversible, and all data associated with the account will be deleted, and you should be careful about it.",
|
||||
"accountDeletionRequested": "Account deletion requested, check your inbox to confirm the request.",
|
||||
"slideToConfirm": "Slide to confirm"
|
||||
"slideToConfirm": "Slide to confirm",
|
||||
"serviceStatus": "Status of Service",
|
||||
"firstBootTime": "First boot at @time",
|
||||
"rateTheApp": "Rate the app",
|
||||
"rateTheAppDesc": "Rate Solar Network on the App Store to let us serve you better!"
|
||||
}
|
||||
|
@ -32,9 +32,9 @@
|
||||
"dashboard": "仪表盘",
|
||||
"today": "今日",
|
||||
"yesterday": "昨日",
|
||||
"feedSearch": "搜索资讯",
|
||||
"feedSearchWithTag": "检索带有 #@key 标签的资讯",
|
||||
"feedSearchWithCategory": "检索位于分类 @category 的资讯",
|
||||
"postSearch": "搜索帖子",
|
||||
"postSearchWithTag": "检索带有 #@key 标签的资讯",
|
||||
"postSearchWithCategory": "检索位于分类 @category 的资讯",
|
||||
"feedUnreadCount": "@count 条你可能错过的帖子",
|
||||
"messages": "消息",
|
||||
"messagesUnreadCount": "@count 条未读的消息",
|
||||
@ -449,5 +449,9 @@
|
||||
"accountDeletionConfirm": "确认账号删除请求",
|
||||
"accountDeletionConfirmDesc": "你确定要删除账号 @account 吗?你将会在其绑定的主要邮件地址收到一封包含着确认删除账号连接的邮件,在二十四小时内使用该连接即可完成删除账号。注意,本操作不可撤销,并且账号创建或关联的所有数据都将被删除,请三思而后行。",
|
||||
"accountDeletionRequested": "已请求删除账号,检查你的收件箱来确认请求。",
|
||||
"slideToConfirm": "滑动来确认"
|
||||
"slideToConfirm": "滑动来确认",
|
||||
"serviceStatus": "服务状态",
|
||||
"firstBootTime": "首次启动于 @time",
|
||||
"rateTheApp": "给应用评分",
|
||||
"rateTheAppDesc": "在 App Store 上给 Solar Network 评分,让我们更好地为您服务吧!"
|
||||
}
|
||||
|
@ -227,6 +227,8 @@ PODS:
|
||||
- TOCropViewController (~> 2.7.4)
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- in_app_review (0.2.0):
|
||||
- Flutter
|
||||
- livekit_client (2.2.6):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
@ -318,6 +320,7 @@ DEPENDENCIES:
|
||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
@ -406,6 +409,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/image_cropper/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
in_app_review:
|
||||
:path: ".symlinks/plugins/in_app_review/ios"
|
||||
livekit_client:
|
||||
:path: ".symlinks/plugins/livekit_client/ios"
|
||||
media_kit_libs_ios_video:
|
||||
@ -482,6 +487,7 @@ SPEC CHECKSUMS:
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
livekit_client: 20e01637431bc108dad451c8a11c1d206e1dd2cd
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
|
@ -1,87 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solink</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>FirebaseMessagingAutoInitEnabled</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Solian</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>solian</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Allow you take photo/video for your message or post</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Allow you record audio for your message or post</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Allow you add photo to your message or post</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>FlutterDeepLinkingEnabled</key>
|
||||
<true/>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solink</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>FirebaseMessagingAutoInitEnabled</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Solian</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>solian</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Allow you take photo/video for your message or post</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Allow you record audio for your message or post</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Allow you add photo to your message or post</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>FlutterDeepLinkingEnabled</key>
|
||||
<true/>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>zh_CN</string>
|
||||
<string>en</string>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -42,6 +44,27 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
|
||||
final Completer _bootCompleter = Completer();
|
||||
|
||||
void _requestRating() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.containsKey('first_boot_time')) {
|
||||
final rawTime = prefs.getString('first_boot_time');
|
||||
final time = DateTime.tryParse(rawTime ?? '');
|
||||
if (time != null &&
|
||||
time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
|
||||
final inAppReview = InAppReview.instance;
|
||||
if (prefs.getBool('rating_requested') == true) return;
|
||||
if (await inAppReview.isAvailable()) {
|
||||
await inAppReview.requestReview();
|
||||
prefs.setBool('rating_requested', true);
|
||||
} else {
|
||||
log('Unable request app review, unavailable');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
prefs.setString('first_boot_time', DateTime.now().toIso8601String());
|
||||
}
|
||||
}
|
||||
|
||||
void _updateNow(String localVersionString, String remoteVersionString) {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
@ -226,6 +249,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
super.initState();
|
||||
_runPeriods();
|
||||
_checkForUpdate();
|
||||
_bootCompleter.future.then((_) {
|
||||
_requestRating();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -27,6 +27,8 @@ abstract class PlatformInfo {
|
||||
|
||||
static bool get canCacheImage => isAndroid || isIOS || isMacOS;
|
||||
|
||||
static bool get canRateTheApp => isIOS || isMacOS;
|
||||
|
||||
static bool get canRecord => (isMobile || isMacOS);
|
||||
|
||||
static bool get canPushNotification => isAndroid || isIOS || isMacOS;
|
||||
@ -38,4 +40,4 @@ abstract class PlatformInfo {
|
||||
} catch (_) {}
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import 'package:solian/screens/realms.dart';
|
||||
import 'package:solian/screens/realms/realm_detail.dart';
|
||||
import 'package:solian/screens/realms/realm_organize.dart';
|
||||
import 'package:solian/screens/realms/realm_view.dart';
|
||||
import 'package:solian/screens/feed.dart';
|
||||
import 'package:solian/screens/explore.dart';
|
||||
import 'package:solian/screens/posts/post_editor.dart';
|
||||
import 'package:solian/screens/settings.dart';
|
||||
import 'package:solian/shells/root_shell.dart';
|
||||
@ -78,13 +78,18 @@ abstract class AppRouter {
|
||||
builder: (context, state, child) => child,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/feed',
|
||||
name: 'feed',
|
||||
builder: (context, state) => const FeedScreen(),
|
||||
path: '/explore',
|
||||
name: 'explore',
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/feed/search',
|
||||
name: 'feedSearch',
|
||||
path: '/drafts',
|
||||
name: 'draftBox',
|
||||
builder: (context, state) => const DraftBoxScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/posts/search',
|
||||
name: 'postSearch',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: FeedSearchScreen(
|
||||
@ -93,11 +98,6 @@ abstract class AppRouter {
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/drafts',
|
||||
name: 'draftBox',
|
||||
builder: (context, state) => const DraftBoxScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/posts/view/:id',
|
||||
name: 'postDetail',
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
@ -52,8 +54,9 @@ class AboutScreen extends StatelessWidget {
|
||||
CenteredContainer(
|
||||
maxWidth: 280,
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
@ -91,6 +94,13 @@ class AboutScreen extends StatelessWidget {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('serviceStatus'.tr),
|
||||
onPressed: () {
|
||||
launchUrlString('https://status.solsynth.dev');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -102,6 +112,34 @@ class AboutScreen extends StatelessWidget {
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: SharedPreferences.getInstance(),
|
||||
builder: (context, snapshot) {
|
||||
const textStyle = TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 12,
|
||||
);
|
||||
if (!snapshot.hasData ||
|
||||
!snapshot.data!.containsKey('first_boot_time')) {
|
||||
return Text(
|
||||
'firstBootTime'.trParams({'time': 'unknown'.tr}),
|
||||
style: textStyle,
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
'firstBootTime'.trParams({
|
||||
'time': DateFormat('yyyy-MM-dd').format(
|
||||
DateTime.tryParse(
|
||||
snapshot.data!.getString('first_boot_time')!,
|
||||
)?.toLocal() ??
|
||||
DateTime.now(),
|
||||
),
|
||||
}),
|
||||
style: textStyle,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -16,14 +16,14 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||
|
||||
class FeedScreen extends StatefulWidget {
|
||||
const FeedScreen({super.key});
|
||||
class ExploreScreen extends StatefulWidget {
|
||||
const ExploreScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FeedScreen> createState() => _FeedScreenState();
|
||||
State<ExploreScreen> createState() => _ExploreScreenState();
|
||||
}
|
||||
|
||||
class _FeedScreenState extends State<FeedScreen>
|
||||
class _ExploreScreenState extends State<ExploreScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final PostListController _postController;
|
||||
late final TabController _tabController;
|
||||
@ -82,7 +82,7 @@ class _FeedScreenState extends State<FeedScreen>
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
title: AppBarTitle('feed'.tr),
|
||||
title: AppBarTitle('explore'.tr),
|
||||
centerTitle: false,
|
||||
floating: true,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
@ -63,13 +63,13 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.label),
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
title: Text('feedSearchWithTag'.trParams({'key': widget.tag!})),
|
||||
title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
|
||||
),
|
||||
if (widget.category != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.category),
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
title: Text('feedSearchWithCategory'
|
||||
title: Text('postSearchWithCategory'
|
||||
.trParams({'key': widget.category!})),
|
||||
),
|
||||
Expanded(
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
@ -205,6 +206,21 @@ class _SettingScreenState extends State<SettingScreen> {
|
||||
});
|
||||
},
|
||||
),
|
||||
if (PlatformInfo.canRateTheApp)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.star),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('rateTheApp'.tr),
|
||||
subtitle: Text('rateTheAppDesc'.tr),
|
||||
onTap: () {
|
||||
final inAppReview = InAppReview.instance;
|
||||
|
||||
inAppReview.openStoreListing(
|
||||
appStoreId: '6499032345',
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
|
@ -34,8 +34,17 @@ class AutoCacheImage extends StatelessWidget {
|
||||
progressIndicatorBuilder: noProgressIndicator
|
||||
? null
|
||||
: (context, url, downloadProgress) => Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: downloadProgress.progress,
|
||||
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
|
||||
@ -74,11 +83,20 @@ class AutoCacheImage extends StatelessWidget {
|
||||
ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
child: TweenAnimationBuilder(
|
||||
tween: Tween(
|
||||
begin: 0,
|
||||
end: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: 0,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? value.toDouble()
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -49,6 +49,7 @@ class ChatEventMessage extends StatelessWidget {
|
||||
return MarkdownTextContent(
|
||||
parentId: 'm${item.id}',
|
||||
isSelectable: true,
|
||||
isAutoWarp: true,
|
||||
content: body.text,
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:markdown/markdown.dart' as markdown;
|
||||
import 'package:markdown/markdown.dart';
|
||||
@ -15,6 +16,7 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
final String parentId;
|
||||
final bool isSelectable;
|
||||
final bool isLargeText;
|
||||
final bool isAutoWarp;
|
||||
|
||||
const MarkdownTextContent({
|
||||
super.key,
|
||||
@ -22,139 +24,175 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
required this.parentId,
|
||||
this.isSelectable = false,
|
||||
this.isLargeText = false,
|
||||
this.isAutoWarp = false,
|
||||
});
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
final emojiRegex = RegExp(r':([-\w]+):');
|
||||
final emojiMatch = emojiRegex.allMatches(content);
|
||||
final isOnlyEmoji = content.replaceAll(emojiRegex, '').trim().isEmpty;
|
||||
final stickerRegex = RegExp(r':([-\w]+):');
|
||||
|
||||
return Markdown(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
data: content,
|
||||
padding: EdgeInsets.zero,
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||
Theme.of(context),
|
||||
).copyWith(
|
||||
textScaleFactor: isLargeText ? 1.1 : 1,
|
||||
blockquote: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
blockquoteDecoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
horizontalRuleDecoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 1.0,
|
||||
color: Theme.of(context).dividerColor,
|
||||
// Split the content into paragraphs
|
||||
final paragraphs = content.split(RegExp(r'\n\s*\n'));
|
||||
|
||||
// Iterate over each paragraph to process stickers individually
|
||||
List<Widget> contentWidgets = [];
|
||||
for (var idx = 0; idx < paragraphs.length; idx++) {
|
||||
// Getting paragraph
|
||||
var paragraph = paragraphs[idx];
|
||||
|
||||
// Auto adding new-lines
|
||||
if (isAutoWarp) {
|
||||
paragraph = paragraph.replaceAll('\n', '\\\n');
|
||||
}
|
||||
|
||||
// Matching stickers
|
||||
final stickerMatch = stickerRegex.allMatches(paragraph);
|
||||
final isOnlySticker =
|
||||
paragraph.replaceAll(stickerRegex, '').trim().isEmpty;
|
||||
|
||||
contentWidgets.add(
|
||||
Markdown(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
data: paragraph,
|
||||
padding: EdgeInsets.zero,
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||
Theme.of(context),
|
||||
).copyWith(
|
||||
textScaleFactor: isLargeText ? 1.1 : 1,
|
||||
blockquote: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
blockquoteDecoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
horizontalRuleDecoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 1.0,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
extensionSet: markdown.ExtensionSet(
|
||||
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||
<markdown.InlineSyntax>[
|
||||
_UserNameCardInlineSyntax(),
|
||||
_CustomEmoteInlineSyntax(),
|
||||
markdown.EmojiSyntax(),
|
||||
markdown.AutolinkSyntax(),
|
||||
markdown.AutolinkExtensionSyntax(),
|
||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
||||
],
|
||||
),
|
||||
onTapLink: (text, href, title) async {
|
||||
if (href == null) return;
|
||||
if (href.startsWith('solink://')) {
|
||||
final segments = href.replaceFirst('solink://', '').split('/');
|
||||
switch (segments[0]) {
|
||||
case 'users':
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
name: segments[1],
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await launchUrlString(
|
||||
href,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
imageBuilder: (uri, title, alt) {
|
||||
var url = uri.toString();
|
||||
double? width, height;
|
||||
BoxFit? fit;
|
||||
if (url.startsWith('solink://')) {
|
||||
final segments = url.replaceFirst('solink://', '').split('/');
|
||||
switch (segments[0]) {
|
||||
case 'stickers':
|
||||
double radius = 8;
|
||||
final StickerProvider sticker = Get.find();
|
||||
if (emojiMatch.length <= 1 && isOnlyEmoji) {
|
||||
width = 128;
|
||||
height = 128;
|
||||
} else if (emojiMatch.length <= 3 && isOnlyEmoji) {
|
||||
width = 32;
|
||||
height = 32;
|
||||
} else {
|
||||
radius = 4;
|
||||
width = 16;
|
||||
height = 16;
|
||||
extensionSet: markdown.ExtensionSet(
|
||||
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||
<markdown.InlineSyntax>[
|
||||
_UserNameCardInlineSyntax(),
|
||||
_CustomEmoteInlineSyntax(),
|
||||
markdown.EmojiSyntax(),
|
||||
markdown.AutolinkSyntax(),
|
||||
markdown.AutolinkExtensionSyntax(),
|
||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
||||
],
|
||||
),
|
||||
onTapLink: (text, href, title) async {
|
||||
if (href == null) return;
|
||||
if (href.startsWith('solink://')) {
|
||||
final segments = href.replaceFirst('solink://', '').split('/');
|
||||
switch (segments[0]) {
|
||||
case 'users':
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
name: segments[1],
|
||||
),
|
||||
);
|
||||
}
|
||||
fit = BoxFit.contain;
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(radius)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: FutureBuilder(
|
||||
future: sticker.getStickerByAlias(segments[1]),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return AutoCacheImage(
|
||||
snapshot.data!.imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
noErrorWidget: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
).paddingSymmetric(vertical: 4);
|
||||
case 'attachments':
|
||||
const radius = BorderRadius.all(Radius.circular(8));
|
||||
return LimitedBox(
|
||||
maxHeight: MediaQuery.of(context).size.width,
|
||||
child: ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: AttachmentSelfContainedEntry(
|
||||
isDense: true,
|
||||
parentId: parentId,
|
||||
rid: segments[1],
|
||||
),
|
||||
),
|
||||
).paddingSymmetric(vertical: 4);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return AutoCacheImage(
|
||||
url,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
);
|
||||
},
|
||||
await launchUrlString(
|
||||
href,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
imageBuilder: (uri, title, alt) {
|
||||
var url = uri.toString();
|
||||
double? width, height;
|
||||
BoxFit? fit;
|
||||
if (url.startsWith('solink://')) {
|
||||
final segments = url.replaceFirst('solink://', '').split('/');
|
||||
switch (segments[0]) {
|
||||
case 'stickers':
|
||||
double radius = 8;
|
||||
final StickerProvider sticker = Get.find();
|
||||
|
||||
// Adjust sticker size based on the sticker count in this paragraph
|
||||
if (stickerMatch.length <= 1 && isOnlySticker) {
|
||||
width = 128;
|
||||
height = 128;
|
||||
} else if (stickerMatch.length <= 3 && isOnlySticker) {
|
||||
width = 32;
|
||||
height = 32;
|
||||
} else {
|
||||
radius = 4;
|
||||
width = 16;
|
||||
height = 16;
|
||||
}
|
||||
fit = BoxFit.contain;
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(radius)),
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: FutureBuilder(
|
||||
future: sticker.getStickerByAlias(segments[1]),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator());
|
||||
}
|
||||
return AutoCacheImage(
|
||||
snapshot.data!.imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
noErrorWidget: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
).paddingSymmetric(vertical: 4);
|
||||
case 'attachments':
|
||||
const radius = BorderRadius.all(Radius.circular(8));
|
||||
return LimitedBox(
|
||||
maxHeight: MediaQuery.of(context).size.width,
|
||||
child: ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: AttachmentSelfContainedEntry(
|
||||
isDense: true,
|
||||
parentId: parentId,
|
||||
rid: segments[1],
|
||||
),
|
||||
),
|
||||
).paddingSymmetric(vertical: 4);
|
||||
}
|
||||
}
|
||||
return AutoCacheImage(
|
||||
url,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (idx < paragraphs.length - 1) {
|
||||
contentWidgets.add(const Gap(4));
|
||||
}
|
||||
}
|
||||
|
||||
// Return the list of widgets for the paragraphs
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: contentWidgets,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -9,9 +9,9 @@ abstract class AppNavigation {
|
||||
page: 'dashboard',
|
||||
),
|
||||
AppNavigationDestination(
|
||||
icon: Icons.newspaper,
|
||||
label: 'feed'.tr,
|
||||
page: 'feed',
|
||||
icon: Icons.explore,
|
||||
label: 'explore'.tr,
|
||||
page: 'explore',
|
||||
),
|
||||
AppNavigationDestination(
|
||||
icon: Icons.workspaces,
|
||||
|
@ -18,6 +18,7 @@ import 'package:solian/widgets/link_expansion.dart';
|
||||
import 'package:solian/widgets/markdown_text_content.dart';
|
||||
import 'package:solian/widgets/posts/post_tags.dart';
|
||||
import 'package:solian/widgets/posts/post_quick_action.dart';
|
||||
import 'package:solian/widgets/relative_date.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:timeago/timeago.dart' show format;
|
||||
|
||||
@ -69,360 +70,6 @@ class _PostItemState extends State<PostItem> {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget _buildDate() {
|
||||
if (widget.isFullDate) {
|
||||
return Text(DateFormat('y/M/d HH:mm')
|
||||
.format(item.publishedAt?.toLocal() ?? DateTime.now()));
|
||||
} else {
|
||||
return Text(
|
||||
format(
|
||||
item.publishedAt?.toLocal() ?? DateTime.now(),
|
||||
locale: 'en_short',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildThumbnail() {
|
||||
if (widget.item.body['thumbnail'] == null) return const SizedBox.shrink();
|
||||
final border = BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 0.3,
|
||||
);
|
||||
return Container(
|
||||
decoration: BoxDecoration(border: Border(top: border, bottom: border)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: AttachmentSelfContainedEntry(
|
||||
rid: widget.item.body['thumbnail'],
|
||||
parentId: 'p${item.id}-thumbnail',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.isCompact)
|
||||
AccountAvatar(
|
||||
content: item.author.avatar,
|
||||
radius: 10,
|
||||
).paddingOnly(left: 2, top: 1),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
item.author.nick,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
_buildDate().paddingOnly(left: 4),
|
||||
],
|
||||
),
|
||||
if (item.body['title'] != null)
|
||||
Text(
|
||||
item.body['title'],
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(fontSize: 15),
|
||||
),
|
||||
if (item.body['description'] != null)
|
||||
Text(
|
||||
item.body['description'],
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: widget.isCompact ? 6 : 12),
|
||||
),
|
||||
if (widget.item.type == 'article')
|
||||
Badge(
|
||||
label: Text('article'.tr),
|
||||
).paddingOnly(top: 3),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderDivider() {
|
||||
if (item.body['description'] != null || item.body['title'] != null) {
|
||||
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
|
||||
vertical: 8,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildFooter() {
|
||||
List<String> labels = List.empty(growable: true);
|
||||
if (widget.item.editedAt != null) {
|
||||
labels.add('postEdited'.trParams({
|
||||
'date': DateFormat('yy/M/d HH:mm').format(item.editedAt!.toLocal()),
|
||||
}));
|
||||
}
|
||||
if (widget.item.realm != null) {
|
||||
labels.add('postInRealm'.trParams({
|
||||
'realm': widget.item.realm!.alias,
|
||||
}));
|
||||
}
|
||||
|
||||
List<Widget> widgets = List.empty(growable: true);
|
||||
|
||||
if (widget.item.tags?.isNotEmpty ?? false) {
|
||||
widgets.add(PostTagsList(tags: widget.item.tags!));
|
||||
}
|
||||
if (labels.isNotEmpty) {
|
||||
widgets.add(Text(
|
||||
labels.join(' · '),
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
));
|
||||
}
|
||||
if (widget.item.pinnedAt != null) {
|
||||
widgets.add(Text(
|
||||
'postPinned'.tr,
|
||||
style: TextStyle(fontSize: 12, color: _unFocusColor),
|
||||
));
|
||||
}
|
||||
|
||||
if (widgets.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widgets,
|
||||
).paddingOnly(top: 4);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildReply(BuildContext context) {
|
||||
return OpenContainer(
|
||||
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
|
||||
closedBuilder: (_, openContainer) => Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 16,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'postRepliedNotify'.trParams(
|
||||
{'username': '@${widget.item.replyTo!.author.name}'},
|
||||
),
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
).paddingOnly(left: 6),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 12),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: PostItem(
|
||||
item: widget.item.replyTo!,
|
||||
isCompact: true,
|
||||
attachmentParent: widget.item.id.toString(),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
openBuilder: (_, __) => TitleShell(
|
||||
title: 'postDetail'.tr,
|
||||
child: PostDetailScreen(
|
||||
id: widget.item.replyTo!.id.toString(),
|
||||
post: widget.item.replyTo!,
|
||||
),
|
||||
),
|
||||
closedElevation: 0,
|
||||
openElevation: 0,
|
||||
closedColor:
|
||||
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
||||
openColor: Theme.of(context).colorScheme.surface,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRepost(BuildContext context) {
|
||||
return OpenContainer(
|
||||
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
|
||||
closedBuilder: (_, openContainer) => Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.retweet,
|
||||
size: 16,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'postRepostedNotify'.trParams(
|
||||
{'username': '@${widget.item.repostTo!.author.name}'},
|
||||
),
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
).paddingOnly(left: 6),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 12),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: PostItem(
|
||||
item: widget.item.repostTo!,
|
||||
isCompact: true,
|
||||
attachmentParent: widget.item.id.toString(),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
openBuilder: (_, __) => TitleShell(
|
||||
title: 'postDetail'.tr,
|
||||
child: PostDetailScreen(
|
||||
id: widget.item.repostTo!.id.toString(),
|
||||
post: widget.item.repostTo!,
|
||||
),
|
||||
),
|
||||
closedElevation: 0,
|
||||
openElevation: 0,
|
||||
closedColor:
|
||||
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
||||
openColor: Theme.of(context).colorScheme.surface,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachments() {
|
||||
final List<String> attachments = item.body['attachments'] is List
|
||||
? List.from(item.body['attachments']?.whereType<String>())
|
||||
: List.empty();
|
||||
|
||||
if (attachments.length > 3) {
|
||||
return AttachmentList(
|
||||
parentId: widget.item.id.toString(),
|
||||
attachmentsId: attachments,
|
||||
autoload: false,
|
||||
isGrid: true,
|
||||
).paddingOnly(left: 36, top: 4, bottom: 4);
|
||||
} else if (attachments.length > 1 || AppTheme.isLargeScreen(context)) {
|
||||
return AttachmentList(
|
||||
parentId: widget.item.id.toString(),
|
||||
attachmentsId: attachments,
|
||||
autoload: false,
|
||||
isColumn: true,
|
||||
).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
|
||||
} else {
|
||||
return AttachmentList(
|
||||
flatMaxHeight: MediaQuery.of(context).size.width,
|
||||
parentId: widget.item.id.toString(),
|
||||
attachmentsId: attachments,
|
||||
autoload: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFeaturedReply() {
|
||||
if ((widget.item.metric?.replyCount ?? 0) == 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final List<String> attachments = item.body['attachments'] is List
|
||||
? List.from(item.body['attachments']?.whereType<String>())
|
||||
: List.empty();
|
||||
final unFocusColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
return FutureBuilder(
|
||||
future: Get.find<PostProvider>().listPostFeaturedReply(
|
||||
widget.item.id.toString(),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: snapshot.data!
|
||||
.map(
|
||||
(x) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountAvatar(content: x.author.avatar, radius: 10),
|
||||
const Gap(6),
|
||||
Text(
|
||||
x.author.nick,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Gap(6),
|
||||
Text(
|
||||
format(
|
||||
x.publishedAt?.toLocal() ?? DateTime.now(),
|
||||
locale: 'en_short',
|
||||
),
|
||||
).paddingOnly(top: 0.5),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MarkdownTextContent(
|
||||
content: x.body['content'],
|
||||
parentId: 'p${item.id}-featured-reply${x.id}',
|
||||
),
|
||||
if (x.body['attachments'] is List &&
|
||||
x.body['attachments'].length > 0)
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.file_copy,
|
||||
size: 15,
|
||||
color: unFocusColor,
|
||||
).paddingOnly(right: 5),
|
||||
Text(
|
||||
'attachmentHint'.trParams(
|
||||
{
|
||||
'count': x.body['attachments'].length
|
||||
.toString()
|
||||
},
|
||||
),
|
||||
style: TextStyle(color: unFocusColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 12, vertical: 8),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: 300.ms,
|
||||
curve: Curves.easeIn,
|
||||
)
|
||||
.paddingOnly(
|
||||
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
||||
? 10
|
||||
: 6,
|
||||
left:
|
||||
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
||||
? 24
|
||||
: 60,
|
||||
right: 16,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double _contentHeight = 0;
|
||||
|
||||
@override
|
||||
@ -436,9 +83,15 @@ class _PostItemState extends State<PostItem> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildThumbnail().paddingOnly(bottom: 8),
|
||||
_buildHeader().paddingSymmetric(horizontal: 12),
|
||||
_buildHeaderDivider().paddingSymmetric(horizontal: 12),
|
||||
_PostThumbnail(
|
||||
rid: item.body['thumbnail'],
|
||||
parentId: widget.item.id.toString(),
|
||||
).paddingOnly(bottom: 8),
|
||||
_PostHeaderWidget(
|
||||
isCompact: widget.isCompact,
|
||||
item: item,
|
||||
).paddingSymmetric(horizontal: 12),
|
||||
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
|
||||
Stack(
|
||||
children: [
|
||||
SizedContainer(
|
||||
@ -448,10 +101,14 @@ class _PostItemState extends State<PostItem> {
|
||||
onChange: (size) {
|
||||
setState(() => _contentHeight = size.height);
|
||||
},
|
||||
child: MarkdownTextContent(
|
||||
parentId: 'p${item.id}',
|
||||
content: item.body['content'],
|
||||
isSelectable: widget.isContentSelectable,
|
||||
child: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: MarkdownTextContent(
|
||||
parentId: 'p${item.id}',
|
||||
content: item.body['content'],
|
||||
isAutoWarp: item.type == 'story',
|
||||
isSelectable: widget.isContentSelectable,
|
||||
),
|
||||
).paddingOnly(
|
||||
left: 16,
|
||||
right: 12,
|
||||
@ -489,7 +146,7 @@ class _PostItemState extends State<PostItem> {
|
||||
right: 8,
|
||||
top: 4,
|
||||
),
|
||||
_buildFooter().paddingOnly(left: 16),
|
||||
_PostFooterWidget(item: item).paddingOnly(left: 16),
|
||||
if (attachments.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
@ -515,7 +172,10 @@ class _PostItemState extends State<PostItem> {
|
||||
closedBuilder: (_, openContainer) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildThumbnail().paddingOnly(bottom: 4),
|
||||
_PostThumbnail(
|
||||
rid: item.body['thumbnail'],
|
||||
parentId: widget.item.id.toString(),
|
||||
).paddingOnly(bottom: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -537,8 +197,11 @@ class _PostItemState extends State<PostItem> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildHeaderDivider(),
|
||||
_PostHeaderWidget(
|
||||
isCompact: widget.isCompact,
|
||||
item: item,
|
||||
),
|
||||
_PostHeaderDividerWidget(item: item),
|
||||
Stack(
|
||||
children: [
|
||||
SizedContainer(
|
||||
@ -549,13 +212,17 @@ class _PostItemState extends State<PostItem> {
|
||||
onChange: (size) {
|
||||
setState(() => _contentHeight = size.height);
|
||||
},
|
||||
child: MarkdownTextContent(
|
||||
parentId: 'p${item.id}-embed',
|
||||
content: item.body['content'],
|
||||
isSelectable: widget.isContentSelectable,
|
||||
isLargeText: item.type == 'article' &&
|
||||
widget.isFullContent,
|
||||
).paddingOnly(left: 12, right: 8),
|
||||
child: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: MarkdownTextContent(
|
||||
parentId: 'p${item.id}-embed',
|
||||
content: item.body['content'],
|
||||
isAutoWarp: item.type == 'story',
|
||||
isSelectable: widget.isContentSelectable,
|
||||
isLargeText: item.type == 'article' &&
|
||||
widget.isFullContent,
|
||||
).paddingOnly(left: 12, right: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_contentHeight >= 320 && !widget.isFullContent)
|
||||
@ -569,10 +236,14 @@ class _PostItemState extends State<PostItem> {
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surface
|
||||
(widget.backgroundColor ??
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surface),
|
||||
(widget.backgroundColor ??
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surface)
|
||||
.withOpacity(0),
|
||||
],
|
||||
),
|
||||
@ -586,15 +257,33 @@ class _PostItemState extends State<PostItem> {
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: _buildReply(context),
|
||||
child: _PostEmbedWidget(
|
||||
isClickable: widget.isClickable,
|
||||
isOverrideEmbedClickable:
|
||||
widget.isOverrideEmbedClickable,
|
||||
item: widget.item.replyTo!,
|
||||
username: widget.item.replyTo!.author.name,
|
||||
hintText: 'postRepliedNotify',
|
||||
icon: FontAwesomeIcons.reply,
|
||||
id: widget.item.replyTo!.id.toString(),
|
||||
),
|
||||
),
|
||||
if (widget.item.repostTo != null && widget.isShowEmbed)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: _buildRepost(context),
|
||||
child: _PostEmbedWidget(
|
||||
isClickable: widget.isClickable,
|
||||
isOverrideEmbedClickable:
|
||||
widget.isOverrideEmbedClickable,
|
||||
item: widget.item.repostTo!,
|
||||
username: widget.item.repostTo!.author.name,
|
||||
hintText: 'postRepostedNotify',
|
||||
icon: FontAwesomeIcons.retweet,
|
||||
id: widget.item.repostTo!.id.toString(),
|
||||
),
|
||||
),
|
||||
_buildFooter().paddingOnly(left: 12),
|
||||
_PostFooterWidget(item: item).paddingOnly(left: 12),
|
||||
LinkExpansion(content: item.body['content'])
|
||||
.paddingOnly(top: 4),
|
||||
],
|
||||
@ -610,8 +299,8 @@ class _PostItemState extends State<PostItem> {
|
||||
right: 16,
|
||||
left: 16,
|
||||
),
|
||||
_buildAttachments(),
|
||||
if (widget.showFeaturedReply) _buildFeaturedReply(),
|
||||
_PostAttachmentWidget(item: item),
|
||||
if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item),
|
||||
if (widget.isShowReply || widget.isReactable)
|
||||
PostQuickAction(
|
||||
isShowReply: widget.isShowReply,
|
||||
@ -654,6 +343,400 @@ class _PostItemState extends State<PostItem> {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
final Post item;
|
||||
|
||||
const _PostFeaturedReplyWidget({required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLargeScreen = AppTheme.isLargeScreen(context);
|
||||
final unFocusColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
if ((item.metric?.replyCount ?? 0) == 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<String> attachments = item.body['attachments'] is List
|
||||
? List.from(item.body['attachments']?.whereType<String>())
|
||||
: List.empty();
|
||||
|
||||
return FutureBuilder(
|
||||
future:
|
||||
Get.find<PostProvider>().listPostFeaturedReply(item.id.toString()),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: snapshot.data!
|
||||
.map(
|
||||
(reply) => ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: OpenContainer(
|
||||
closedBuilder: (_, openContainer) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountAvatar(
|
||||
content: reply.author.avatar,
|
||||
radius: 10,
|
||||
),
|
||||
const Gap(6),
|
||||
Text(
|
||||
reply.author.nick,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Gap(6),
|
||||
Text(
|
||||
format(
|
||||
reply.publishedAt?.toLocal() ?? DateTime.now(),
|
||||
locale: 'en_short',
|
||||
),
|
||||
).paddingOnly(top: 0.5),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MarkdownTextContent(
|
||||
isAutoWarp: reply.type == 'story',
|
||||
content: reply.body['content'],
|
||||
parentId:
|
||||
'p${item.id}-featured-reply${reply.id}',
|
||||
),
|
||||
if (reply.body['attachments'] is List &&
|
||||
reply.body['attachments'].isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.file_copy,
|
||||
size: 15,
|
||||
color: unFocusColor,
|
||||
).paddingOnly(right: 5),
|
||||
Text(
|
||||
'attachmentHint'.trParams(
|
||||
{
|
||||
'count': reply
|
||||
.body['attachments'].length
|
||||
.toString(),
|
||||
},
|
||||
),
|
||||
style: TextStyle(color: unFocusColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 12, vertical: 8),
|
||||
openBuilder: (_, __) => TitleShell(
|
||||
title: 'postDetail'.tr,
|
||||
child: PostDetailScreen(
|
||||
id: reply.id.toString(),
|
||||
post: reply,
|
||||
),
|
||||
),
|
||||
closedElevation: 0,
|
||||
openElevation: 0,
|
||||
closedColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
openColor: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: 300.ms,
|
||||
curve: Curves.easeIn,
|
||||
)
|
||||
.paddingOnly(
|
||||
top: (attachments.length == 1 && !isLargeScreen) ? 10 : 6,
|
||||
left: (attachments.length == 1 && !isLargeScreen) ? 24 : 60,
|
||||
right: 16,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostAttachmentWidget extends StatelessWidget {
|
||||
final Post item;
|
||||
|
||||
const _PostAttachmentWidget({required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLargeScreen = AppTheme.isLargeScreen(context);
|
||||
|
||||
final List<String> attachments = item.body['attachments'] is List
|
||||
? List.from(item.body['attachments']?.whereType<String>())
|
||||
: List.empty();
|
||||
|
||||
if (attachments.length > 3) {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
attachmentsId: attachments,
|
||||
autoload: false,
|
||||
isGrid: true,
|
||||
).paddingOnly(left: 36, top: 4, bottom: 4);
|
||||
} else if (attachments.length > 1 || isLargeScreen) {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
attachmentsId: attachments,
|
||||
autoload: false,
|
||||
isColumn: true,
|
||||
).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
|
||||
} else {
|
||||
return AttachmentList(
|
||||
flatMaxHeight: MediaQuery.of(context).size.width,
|
||||
parentId: item.id.toString(),
|
||||
attachmentsId: attachments,
|
||||
autoload: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PostEmbedWidget extends StatelessWidget {
|
||||
final bool isClickable;
|
||||
final bool isOverrideEmbedClickable;
|
||||
final Post item;
|
||||
final String username;
|
||||
final String hintText;
|
||||
final IconData icon;
|
||||
final String id;
|
||||
|
||||
const _PostEmbedWidget({
|
||||
required this.isClickable,
|
||||
required this.isOverrideEmbedClickable,
|
||||
required this.item,
|
||||
required this.username,
|
||||
required this.hintText,
|
||||
required this.icon,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final unFocusColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
return OpenContainer(
|
||||
tappable: isClickable || isOverrideEmbedClickable,
|
||||
closedBuilder: (_, openContainer) => Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FaIcon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: unFocusColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
hintText.trParams(
|
||||
{'username': '@$username'},
|
||||
),
|
||||
style: TextStyle(color: unFocusColor),
|
||||
).paddingOnly(left: 6),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 12),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: PostItem(
|
||||
item: item,
|
||||
isCompact: true,
|
||||
attachmentParent: id,
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
openBuilder: (_, __) => TitleShell(
|
||||
title: 'postDetail'.tr,
|
||||
child: PostDetailScreen(
|
||||
id: id,
|
||||
post: item,
|
||||
),
|
||||
),
|
||||
closedElevation: 0,
|
||||
openElevation: 0,
|
||||
closedColor: Theme.of(context).colorScheme.surface,
|
||||
openColor: Theme.of(context).colorScheme.surface,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostHeaderDividerWidget extends StatelessWidget {
|
||||
final Post item;
|
||||
|
||||
const _PostHeaderDividerWidget({
|
||||
required this.item,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (item.body['description'] != null || item.body['title'] != null) {
|
||||
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
|
||||
vertical: 8,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
class _PostFooterWidget extends StatelessWidget {
|
||||
final Post item;
|
||||
|
||||
const _PostFooterWidget({required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final unFocusColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
List<String> labels = List.empty(growable: true);
|
||||
if (item.editedAt != null) {
|
||||
labels.add('postEdited'.trParams({
|
||||
'date': DateFormat('yy/M/d HH:mm').format(item.editedAt!.toLocal()),
|
||||
}));
|
||||
}
|
||||
if (item.realm != null) {
|
||||
labels.add('postInRealm'.trParams({
|
||||
'realm': item.realm!.alias,
|
||||
}));
|
||||
}
|
||||
|
||||
List<Widget> widgets = List.empty(growable: true);
|
||||
|
||||
if (item.tags?.isNotEmpty ?? false) {
|
||||
widgets.add(PostTagsList(tags: item.tags!));
|
||||
}
|
||||
if (labels.isNotEmpty) {
|
||||
widgets.add(Text(
|
||||
labels.join(' · '),
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: unFocusColor,
|
||||
),
|
||||
));
|
||||
}
|
||||
if (item.pinnedAt != null) {
|
||||
widgets.add(Text(
|
||||
'postPinned'.tr,
|
||||
style: TextStyle(fontSize: 12, color: unFocusColor),
|
||||
));
|
||||
}
|
||||
|
||||
if (widgets.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widgets,
|
||||
).paddingOnly(top: 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PostHeaderWidget extends StatelessWidget {
|
||||
final bool isCompact;
|
||||
final Post item;
|
||||
|
||||
const _PostHeaderWidget({
|
||||
required this.isCompact,
|
||||
required this.item,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isCompact)
|
||||
AccountAvatar(
|
||||
content: item.author.avatar,
|
||||
radius: 10,
|
||||
).paddingOnly(left: 2, top: 1),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
item.author.nick,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now())
|
||||
.paddingOnly(left: 4),
|
||||
],
|
||||
),
|
||||
if (item.body['title'] != null)
|
||||
Text(
|
||||
item.body['title'],
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(fontSize: 15),
|
||||
),
|
||||
if (item.body['description'] != null)
|
||||
Text(
|
||||
item.body['description'],
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: isCompact ? 6 : 12),
|
||||
),
|
||||
if (item.type == 'article')
|
||||
Badge(
|
||||
label: Text('article'.tr),
|
||||
).paddingOnly(top: 3),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostThumbnail extends StatelessWidget {
|
||||
final String parentId;
|
||||
final String? rid;
|
||||
|
||||
const _PostThumbnail({required this.parentId, required this.rid});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (rid?.isEmpty ?? true) return const SizedBox.shrink();
|
||||
final border = BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 0.3,
|
||||
);
|
||||
return Container(
|
||||
decoration: BoxDecoration(border: Border(top: border, bottom: border)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: AttachmentSelfContainedEntry(
|
||||
rid: rid!,
|
||||
parentId: 'p$parentId-thumbnail',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef _OnWidgetSizeChange = void Function(Size size);
|
||||
|
||||
class _MeasureSizeRenderObject extends RenderProxyBox {
|
||||
|
@ -27,7 +27,7 @@ class PostTagsList extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('feedSearch', queryParameters: {
|
||||
AppRouter.instance.pushNamed('postSearch', queryParameters: {
|
||||
'tag': x.alias,
|
||||
});
|
||||
},
|
||||
|
23
lib/widgets/relative_date.dart
Normal file
23
lib/widgets/relative_date.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:timeago/timeago.dart';
|
||||
|
||||
class RelativeDate extends StatelessWidget {
|
||||
final DateTime date;
|
||||
final bool isFull;
|
||||
|
||||
const RelativeDate(this.date, {super.key, this.isFull = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isFull) {
|
||||
return Text(DateFormat('y/M/d HH:mm').format(date));
|
||||
}
|
||||
return Text(
|
||||
format(
|
||||
date,
|
||||
locale: 'en_short',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_webrtc
|
||||
import gal
|
||||
import in_app_review
|
||||
import livekit_client
|
||||
import macos_window_utils
|
||||
import media_kit_libs_macos_video
|
||||
@ -46,6 +47,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
||||
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
|
@ -158,6 +158,8 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (8.0.2):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- in_app_review (0.2.0):
|
||||
- FlutterMacOS
|
||||
- livekit_client (2.2.6):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
@ -234,6 +236,7 @@ DEPENDENCIES:
|
||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
||||
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
|
||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||
@ -299,6 +302,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral
|
||||
gal:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
||||
in_app_review:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
|
||||
livekit_client:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
|
||||
macos_window_utils:
|
||||
@ -336,7 +341,7 @@ SPEC CHECKSUMS:
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
|
||||
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
|
||||
firebase_analytics: a2d0d907566e4a48e27745317f05b4b7db85edd9
|
||||
firebase_core: c55630cdb8a01cf49eae741dd4bc8c93bdd546b8
|
||||
@ -359,6 +364,7 @@ SPEC CHECKSUMS:
|
||||
GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
in_app_review: a850789fad746e89bce03d4aeee8078b45a53fd0
|
||||
livekit_client: 98d09566e3a936b3402be8091ec3845556d36800
|
||||
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
@ -377,7 +383,7 @@ SPEC CHECKSUMS:
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b
|
||||
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3
|
||||
|
||||
|
@ -47,6 +47,11 @@
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>zh_CN</string>
|
||||
<string>en</string>
|
||||
</array>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INStartCallIntent</string>
|
||||
|
24
pubspec.lock
24
pubspec.lock
@ -474,10 +474,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385
|
||||
sha256: cb284e267f8e2a45a904b5c094d2ba51d0aabfc20b1538ab786d9ef7dc2bf75c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
version: "0.9.4+1"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1125,6 +1125,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
in_app_review:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: in_app_review
|
||||
sha256: "99869244d09adc76af16bf8fd731dd13cef58ecafd5917847589c49f378cbb30"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.9"
|
||||
in_app_review_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_review_platform_interface
|
||||
sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
infinite_scroll_pagination:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -2078,10 +2094,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
|
||||
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.2.1"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2,7 +2,7 @@ name: solian
|
||||
description: "The Solar Network App"
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.2.3+1
|
||||
version: 1.2.4+1
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.4 <4.0.0"
|
||||
@ -83,6 +83,7 @@ dependencies:
|
||||
flutter_app_update: ^3.1.0
|
||||
version: ^3.0.2
|
||||
action_slider: ^0.7.0
|
||||
in_app_review: ^2.0.9
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user