Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
107379d9fe | |||
0d807b8708 | |||
ac1b3fe15c | |||
5853de32a2 | |||
eac1be365e | |||
3fb1d7a6d4 | |||
0480b5244f | |||
56fb92c6b9 | |||
b3267f0026 | |||
88587c10da | |||
9012566dbf | |||
6e00a99803 | |||
aa17a5d52a | |||
ebeffbe1aa | |||
d22eac5c10 | |||
e5381dd5e0 | |||
1c26944a05 |
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"annvisery"
|
||||
]
|
||||
}
|
@ -54,6 +54,7 @@
|
||||
"about": "About",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"insert": "Insert",
|
||||
"settings": "Settings",
|
||||
"settingsNotificationBgService": "Background notification service",
|
||||
"settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.",
|
||||
@ -366,6 +367,7 @@
|
||||
"bsPreparingData": "Preparing User Data",
|
||||
"bsRegisteringPushNotify": "Enabling Push Notifications",
|
||||
"bsDismissibleErrorHint": "Click anywhere to ignore this error",
|
||||
"bsContinuable": "Click anywhere to continue",
|
||||
"postShareContent": "@content\n\n@username on the Solar Network\nCheck it out: @link",
|
||||
"postShareSubject": "@title by @username on Solar Network",
|
||||
"themeColor": "Global Theme Color",
|
||||
@ -486,5 +488,11 @@
|
||||
"shareImage": "Share as image",
|
||||
"shareImageFooter": "Only on the Solar Network",
|
||||
"fileSavedAt": "File saved at @path",
|
||||
"showIp": "Show IP Address"
|
||||
"showIp": "Show IP Address",
|
||||
"shotOn": "Shot on @device",
|
||||
"unread": "Unread",
|
||||
"searchTook": "Took @time",
|
||||
"searchResult": "@count Matches",
|
||||
"happyBirthday": "Happy birthday @name!",
|
||||
"happyBirthdayDesc": "Today is your @count birthday"
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
"about": "关于",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"insert": "插入",
|
||||
"settings": "设置",
|
||||
"settingsNotificationBgService": "常驻通知服务",
|
||||
"settingsNotificationBgServiceDesc": "在设备常驻一个通知服务,使得部分不支持推送通知的设备可以在后台收到通知;启用该功能的情况下不会向服务器注册推送通知,并且你会始终在他人眼中成为在线(隐身除外);可能需要在设置中关闭电量与流量优化。",
|
||||
@ -362,6 +363,7 @@
|
||||
"bsPreparingData": "正在准备用户资料",
|
||||
"bsRegisteringPushNotify": "正在启用推送通知",
|
||||
"bsDismissibleErrorHint": "点击任意地方忽略此错误",
|
||||
"bsContinuable": "点击任意处继续",
|
||||
"postShareContent": "@content\n\n@username 在 Solar Network\n原帖地址:@link",
|
||||
"postShareSubject": "@username 在 Solar Network 发表的 @title",
|
||||
"themeColor": "全局主题色",
|
||||
@ -482,5 +484,11 @@
|
||||
"shareImage": "分享图片",
|
||||
"shareImageFooter": "上 Solar Network 看更多有趣帖子",
|
||||
"fileSavedAt": "文件保存于 @path",
|
||||
"showIp": "显示 IP 地址"
|
||||
"showIp": "显示 IP 地址",
|
||||
"shotOn": "由 @device 拍摄",
|
||||
"unread": "未读",
|
||||
"searchTook": "耗时 @time",
|
||||
"searchResult": "匹配到 @count 条结果",
|
||||
"happyBirthday": "生日快乐,@name!",
|
||||
"happyBirthdayDesc": "今天是你的第 @count 个生日"
|
||||
}
|
||||
|
@ -462,8 +462,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
@ -508,7 +508,7 @@ SPEC CHECKSUMS:
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
@ -519,7 +519,7 @@ SPEC CHECKSUMS:
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:confetti/confetti.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -10,9 +11,11 @@ import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
@ -22,6 +25,11 @@ import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:flutter_app_update/flutter_app_update.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
enum BootstrapperSpecialState {
|
||||
userBirthday,
|
||||
appAnniversary,
|
||||
}
|
||||
|
||||
class BootstrapperShell extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
@ -42,6 +50,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
|
||||
int _periodCursor = 0;
|
||||
|
||||
// Special state is some special event triggered after bootstrapping
|
||||
BootstrapperSpecialState? _specialState;
|
||||
|
||||
final Completer _bootCompleter = Completer();
|
||||
|
||||
void _requestRating() async {
|
||||
@ -198,11 +209,26 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
final AuthProvider auth = Get.find();
|
||||
try {
|
||||
await Future.wait([
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<NotificationProvider>().fetchNotification(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RelationshipProvider>().refreshRelativeList(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RealmProvider>().refreshAvailableRealms(),
|
||||
]);
|
||||
|
||||
if (auth.isAuthorized.isTrue && auth.userProfile.value != null) {
|
||||
final account = Account.fromJson(auth.userProfile.value!);
|
||||
if (account.profile?.birthday != null) {
|
||||
final birthDate = account.profile!.birthday!.toLocal();
|
||||
final isBirthday = birthDate.day == DateTime.now().day;
|
||||
if (isBirthday) {
|
||||
setState(
|
||||
() => _specialState = BootstrapperSpecialState.userBirthday,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
@ -214,7 +240,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isTrue) {
|
||||
try {
|
||||
Get.find<WebSocketProvider>().registerPushNotifications();
|
||||
Get.find<NotificationProvider>().registerPushNotifications();
|
||||
} catch (err) {
|
||||
context.showSnackbar(
|
||||
'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}),
|
||||
@ -352,8 +378,142 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if (_specialState != null) {
|
||||
return GestureDetector(
|
||||
child: RootContainer(
|
||||
child: switch (_specialState) {
|
||||
BootstrapperSpecialState.appAnniversary => const Placeholder(),
|
||||
_ => _BirthdaySpecialScreen(),
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _specialState = null);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class _BirthdaySpecialScreen extends StatefulWidget {
|
||||
const _BirthdaySpecialScreen();
|
||||
|
||||
@override
|
||||
State<_BirthdaySpecialScreen> createState() => _BirthdaySpecialScreenState();
|
||||
}
|
||||
|
||||
class _BirthdaySpecialScreenState extends State<_BirthdaySpecialScreen> {
|
||||
late final ConfettiController _confettiController =
|
||||
ConfettiController(duration: const Duration(seconds: 10));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_confettiController.play();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
String _toOrdinal(int num) {
|
||||
if (num >= 11 && num <= 13) {
|
||||
return '${num}th';
|
||||
}
|
||||
|
||||
switch (num % 10) {
|
||||
case 1:
|
||||
return '${num}st';
|
||||
case 2:
|
||||
return '${num}nd';
|
||||
case 3:
|
||||
return '${num}rd';
|
||||
default:
|
||||
return '${num}th';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
final account = Account.fromJson(auth.userProfile.value!);
|
||||
|
||||
final birthDate = account.profile!.birthday!.toLocal();
|
||||
final birthdayCount = DateTime.now().difference(birthDate).inDays ~/ 365;
|
||||
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _confettiController,
|
||||
blastDirectionality: BlastDirectionality.explosive,
|
||||
shouldLoop: true,
|
||||
colors: const [
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.pink,
|
||||
Colors.orange,
|
||||
Colors.purple
|
||||
],
|
||||
maxBlastForce: 30,
|
||||
minBlastForce: 15,
|
||||
emissionFrequency: 0.05,
|
||||
numberOfParticles: 20,
|
||||
gravity: 0.2,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
child: CenteredContainer(
|
||||
maxWidth: 320,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'🎂',
|
||||
style: TextStyle(fontSize: 60),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'happyBirthday'.trParams({
|
||||
'name': account.profile?.firstName != null
|
||||
? [
|
||||
account.profile?.firstName,
|
||||
account.profile?.lastName
|
||||
].join(' ')
|
||||
: '@${account.name}',
|
||||
}),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text(
|
||||
'happyBirthdayDesc'.trParams({
|
||||
'count': _toOrdinal(birthdayCount),
|
||||
}),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'bsContinuable'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
).paddingOnly(bottom: 5),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -125,6 +125,21 @@ class PostEditorController extends GetxController {
|
||||
onRemove: (String value) {
|
||||
attachments.remove(value);
|
||||
},
|
||||
onInsert: (String str) {
|
||||
final text = contentController.text;
|
||||
final selection = contentController.selection;
|
||||
final newText = text.replaceRange(
|
||||
selection.start,
|
||||
selection.end,
|
||||
str,
|
||||
);
|
||||
contentController.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: selection.baseOffset + str.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import 'package:solian/providers/database/services/messages.dart';
|
||||
import 'package:solian/providers/last_read.dart';
|
||||
import 'package:solian/providers/link_expander.dart';
|
||||
import 'package:solian/providers/navigation.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/providers/subscription.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
@ -138,11 +139,12 @@ class SolianApp extends StatelessWidget {
|
||||
Get.put(NavigationStateProvider());
|
||||
|
||||
Get.lazyPut(() => AuthProvider());
|
||||
Get.lazyPut(() => WebSocketProvider());
|
||||
Get.lazyPut(() => RelationshipProvider());
|
||||
Get.lazyPut(() => PostProvider());
|
||||
Get.lazyPut(() => StickerProvider());
|
||||
Get.lazyPut(() => AttachmentProvider());
|
||||
Get.lazyPut(() => WebSocketProvider());
|
||||
Get.lazyPut(() => NotificationProvider());
|
||||
Get.lazyPut(() => StatusProvider());
|
||||
Get.lazyPut(() => ChannelProvider());
|
||||
Get.lazyPut(() => RealmProvider());
|
||||
@ -154,6 +156,6 @@ class SolianApp extends StatelessWidget {
|
||||
Get.lazyPut(() => LastReadProvider());
|
||||
Get.lazyPut(() => SubscriptionProvider());
|
||||
|
||||
Get.find<WebSocketProvider>().requestPermissions();
|
||||
Get.find<NotificationProvider>().requestPermissions();
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'notification.g.dart';
|
||||
|
||||
const Map<String, IconData> NotificationTopicIcons = {
|
||||
'passport.security.alert': Icons.gpp_maybe,
|
||||
'interactive.subscription': Icons.subscriptions,
|
||||
'interactive.feedback': Icons.add_reaction,
|
||||
'messaging.callStart': Icons.call_received,
|
||||
};
|
||||
|
||||
@JsonSerializable()
|
||||
class Notification {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
DateTime? readAt;
|
||||
String topic;
|
||||
String title;
|
||||
String? subtitle;
|
||||
String body;
|
||||
String? avatar;
|
||||
String? picture;
|
||||
Map<String, dynamic>? metadata;
|
||||
int? senderId;
|
||||
int accountId;
|
||||
|
||||
@ -21,11 +32,14 @@ class Notification {
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.readAt,
|
||||
required this.topic,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.body,
|
||||
required this.avatar,
|
||||
required this.picture,
|
||||
required this.metadata,
|
||||
required this.senderId,
|
||||
required this.accountId,
|
||||
});
|
||||
|
@ -13,11 +13,16 @@ Notification _$NotificationFromJson(Map<String, dynamic> json) => Notification(
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
readAt: json['read_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['read_at'] as String),
|
||||
topic: json['topic'] as String,
|
||||
title: json['title'] as String,
|
||||
subtitle: json['subtitle'] as String?,
|
||||
body: json['body'] as String,
|
||||
avatar: json['avatar'] as String?,
|
||||
picture: json['picture'] as String?,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
senderId: (json['sender_id'] as num?)?.toInt(),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
@ -28,11 +33,14 @@ Map<String, dynamic> _$NotificationToJson(Notification instance) =>
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'read_at': instance.readAt?.toIso8601String(),
|
||||
'topic': instance.topic,
|
||||
'title': instance.title,
|
||||
'subtitle': instance.subtitle,
|
||||
'body': instance.body,
|
||||
'avatar': instance.avatar,
|
||||
'picture': instance.picture,
|
||||
'metadata': instance.metadata,
|
||||
'sender_id': instance.senderId,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/auth.dart';
|
||||
import 'package:solian/providers/database/database.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
@ -174,7 +175,7 @@ class AuthProvider extends GetConnect {
|
||||
);
|
||||
|
||||
Get.find<WebSocketProvider>().connect();
|
||||
Get.find<WebSocketProvider>().notifyPrefetch();
|
||||
Get.find<NotificationProvider>().fetchNotification();
|
||||
|
||||
return credentials!;
|
||||
}
|
||||
@ -184,8 +185,8 @@ class AuthProvider extends GetConnect {
|
||||
userProfile.value = null;
|
||||
|
||||
Get.find<WebSocketProvider>().disconnect();
|
||||
Get.find<WebSocketProvider>().notifications.clear();
|
||||
Get.find<WebSocketProvider>().notificationUnread.value = 0;
|
||||
Get.find<NotificationProvider>().notifications.clear();
|
||||
Get.find<NotificationProvider>().notificationUnread.value = 0;
|
||||
|
||||
AppDatabase.removeDatabase();
|
||||
autoStopBackgroundNotificationService();
|
||||
|
@ -44,9 +44,12 @@ class PostProvider extends GetxController {
|
||||
final queries = [
|
||||
'take=${10}',
|
||||
'offset=$page',
|
||||
'truncate=false',
|
||||
];
|
||||
final client = await auth.configureClient('interactive');
|
||||
final resp = await client.get('/posts/drafts?${queries.join('&')}');
|
||||
final resp = await client.get(
|
||||
'/posts/drafts?${queries.join('&')}',
|
||||
);
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
@ -299,53 +299,71 @@ typedef $$LocalMessageEventTableTableUpdateCompanionBuilder
|
||||
});
|
||||
|
||||
class $$LocalMessageEventTableTableFilterComposer
|
||||
extends FilterComposer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableFilterComposer(super.$state);
|
||||
ColumnFilters<int> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnFilters<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<int> get channelId => $state.composableBuilder(
|
||||
column: $state.table.channelId,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
ColumnFilters<int> get channelId => $composableBuilder(
|
||||
column: $table.channelId, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnWithTypeConverterFilters<Event?, Event, String> get data =>
|
||||
$state.composableBuilder(
|
||||
column: $state.table.data,
|
||||
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
|
||||
column,
|
||||
joinBuilders: joinBuilders));
|
||||
$composableBuilder(
|
||||
column: $table.data,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||
|
||||
ColumnFilters<DateTime> get createdAt => $state.composableBuilder(
|
||||
column: $state.table.createdAt,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => ColumnFilters(column));
|
||||
}
|
||||
|
||||
class $$LocalMessageEventTableTableOrderingComposer
|
||||
extends OrderingComposer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableOrderingComposer(super.$state);
|
||||
ColumnOrderings<int> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnOrderings<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<int> get channelId => $state.composableBuilder(
|
||||
column: $state.table.channelId,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
ColumnOrderings<int> get channelId => $composableBuilder(
|
||||
column: $table.channelId, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get data => $state.composableBuilder(
|
||||
column: $state.table.data,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
ColumnOrderings<String> get data => $composableBuilder(
|
||||
column: $table.data, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $state.composableBuilder(
|
||||
column: $state.table.createdAt,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
|
||||
}
|
||||
|
||||
class $$LocalMessageEventTableTableAnnotationComposer
|
||||
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
GeneratedColumn<int> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get channelId =>
|
||||
$composableBuilder(column: $table.channelId, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<Event?, String> get data =>
|
||||
$composableBuilder(column: $table.data, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
||||
@ -354,6 +372,7 @@ class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
||||
LocalMessageEventTableData,
|
||||
$$LocalMessageEventTableTableFilterComposer,
|
||||
$$LocalMessageEventTableTableOrderingComposer,
|
||||
$$LocalMessageEventTableTableAnnotationComposer,
|
||||
$$LocalMessageEventTableTableCreateCompanionBuilder,
|
||||
$$LocalMessageEventTableTableUpdateCompanionBuilder,
|
||||
(
|
||||
@ -368,10 +387,15 @@ class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
filteringComposer: $$LocalMessageEventTableTableFilterComposer(
|
||||
ComposerState(db, table)),
|
||||
orderingComposer: $$LocalMessageEventTableTableOrderingComposer(
|
||||
ComposerState(db, table)),
|
||||
createFilteringComposer: () =>
|
||||
$$LocalMessageEventTableTableFilterComposer(
|
||||
$db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
$$LocalMessageEventTableTableOrderingComposer(
|
||||
$db: db, $table: table),
|
||||
createComputedFieldComposer: () =>
|
||||
$$LocalMessageEventTableTableAnnotationComposer(
|
||||
$db: db, $table: table),
|
||||
updateCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<int> channelId = const Value.absent(),
|
||||
@ -410,6 +434,7 @@ typedef $$LocalMessageEventTableTableProcessedTableManager
|
||||
LocalMessageEventTableData,
|
||||
$$LocalMessageEventTableTableFilterComposer,
|
||||
$$LocalMessageEventTableTableOrderingComposer,
|
||||
$$LocalMessageEventTableTableAnnotationComposer,
|
||||
$$LocalMessageEventTableTableCreateCompanionBuilder,
|
||||
$$LocalMessageEventTableTableUpdateCompanionBuilder,
|
||||
(
|
||||
|
175
lib/providers/notifications.dart
Normal file
175
lib/providers/notifications.dart
Normal file
@ -0,0 +1,175 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/notification.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
|
||||
class NotificationProvider extends GetxController {
|
||||
RxBool isBusy = false.obs;
|
||||
|
||||
RxInt notificationUnread = 0.obs;
|
||||
RxList<Notification> notifications =
|
||||
List<Notification>.empty(growable: true).obs;
|
||||
|
||||
Future<void> fetchNotification() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.get('/notifications?skip=0&take=100');
|
||||
if (resp.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
|
||||
if (data != null) {
|
||||
notifications.addAll(data);
|
||||
notificationUnread.value = data.where((x) => x.readAt == null).length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markAllRead() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
isBusy.value = true;
|
||||
|
||||
final NotificationProvider nty = Get.find();
|
||||
|
||||
List<int> markList = List.empty(growable: true);
|
||||
for (final element in nty.notifications) {
|
||||
if (element.id <= 0) continue;
|
||||
if (element.readAt != null) continue;
|
||||
markList.add(element.id);
|
||||
}
|
||||
|
||||
if (markList.isNotEmpty) {
|
||||
final client = await auth.configureClient('auth');
|
||||
await client.put('/notifications/read', {'messages': markList});
|
||||
}
|
||||
|
||||
nty.notifications.value = nty.notifications.map((x) {
|
||||
x.readAt = DateTime.now();
|
||||
return x;
|
||||
}).toList();
|
||||
nty.notifications.refresh();
|
||||
|
||||
isBusy.value = false;
|
||||
}
|
||||
|
||||
Future<void> markOneRead(Notification element, int index) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final NotificationProvider nty = Get.find();
|
||||
|
||||
if (element.id <= 0) {
|
||||
nty.notifications.removeAt(index);
|
||||
return;
|
||||
} else if (element.readAt != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
isBusy.value = true;
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
await client.put('/notifications/read/${element.id}', {});
|
||||
|
||||
nty.notifications[0].readAt = DateTime.now();
|
||||
nty.notifications.refresh();
|
||||
|
||||
isBusy.value = false;
|
||||
}
|
||||
|
||||
void requestPermissions() {
|
||||
try {
|
||||
FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
announcement: true,
|
||||
carPlay: true,
|
||||
badge: true,
|
||||
sound: true);
|
||||
} catch (_) {
|
||||
// When firebase isn't initialized (background service)
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestNotificationsPermission();
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
MacOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerPushNotifications() async {
|
||||
if (PlatformInfo.isWeb) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.getBool('service_background_notification') == true) {
|
||||
log('Background notification service has been enabled, skip register push notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
late final String? token;
|
||||
late final String provider;
|
||||
var deviceUuid = await _getDeviceUuid();
|
||||
|
||||
if (deviceUuid == null || deviceUuid.isEmpty) {
|
||||
log("Unable to active push notifications, couldn't get device uuid");
|
||||
return;
|
||||
} else {
|
||||
log('Device UUID is $deviceUuid');
|
||||
}
|
||||
|
||||
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
||||
provider = 'apple';
|
||||
token = await FirebaseMessaging.instance.getAPNSToken();
|
||||
} else {
|
||||
provider = 'firebase';
|
||||
token = await FirebaseMessaging.instance.getToken();
|
||||
}
|
||||
log('Device Push Token is $token');
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.post('/notifications/subscribe', {
|
||||
'provider': provider,
|
||||
'device_token': token,
|
||||
'device_id': deviceUuid,
|
||||
});
|
||||
if (resp.statusCode != 200 && resp.statusCode != 400) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _getDeviceUuid() async {
|
||||
if (PlatformInfo.isWeb) return null;
|
||||
return await FlutterUdid.consistentUdid;
|
||||
}
|
||||
}
|
@ -3,17 +3,11 @@ import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/notification.dart';
|
||||
import 'package:solian/models/packet.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
@ -21,56 +15,10 @@ class WebSocketProvider extends GetxController {
|
||||
RxBool isConnected = false.obs;
|
||||
RxBool isConnecting = false.obs;
|
||||
|
||||
RxInt notificationUnread = 0.obs;
|
||||
RxList<Notification> notifications =
|
||||
List<Notification>.empty(growable: true).obs;
|
||||
|
||||
WebSocketChannel? websocket;
|
||||
|
||||
StreamController<NetworkPackage> stream = StreamController.broadcast();
|
||||
|
||||
@override
|
||||
onInit() {
|
||||
notifyPrefetch();
|
||||
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void requestPermissions() {
|
||||
try {
|
||||
FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
announcement: true,
|
||||
carPlay: true,
|
||||
badge: true,
|
||||
sound: true);
|
||||
} catch (_) {
|
||||
// When firebase isn't initialized (background service)
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestNotificationsPermission();
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
MacOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect({noRetry = false}) async {
|
||||
if (isConnected.value) {
|
||||
return;
|
||||
@ -119,8 +67,9 @@ class WebSocketProvider extends GetxController {
|
||||
log('Websocket incoming message: ${packet.method} ${packet.message}');
|
||||
stream.sink.add(packet);
|
||||
if (packet.method == 'notifications.new') {
|
||||
notifications.add(Notification.fromJson(packet.payload!));
|
||||
notificationUnread.value++;
|
||||
final NotificationProvider nty = Get.find();
|
||||
nty.notifications.add(Notification.fromJson(packet.payload!));
|
||||
nty.notificationUnread.value++;
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
@ -133,70 +82,4 @@ class WebSocketProvider extends GetxController {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> notifyPrefetch() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.get('/notifications?skip=0&take=100');
|
||||
if (resp.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
|
||||
if (data != null) {
|
||||
notifications.addAll(data);
|
||||
notificationUnread.value = data.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerPushNotifications() async {
|
||||
if (PlatformInfo.isWeb) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.getBool('service_background_notification') == true) {
|
||||
log('Background notification service has been enabled, skip register push notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
late final String? token;
|
||||
late final String provider;
|
||||
var deviceUuid = await _getDeviceUuid();
|
||||
|
||||
if (deviceUuid == null || deviceUuid.isEmpty) {
|
||||
log("Unable to active push notifications, couldn't get device uuid");
|
||||
return;
|
||||
} else {
|
||||
log('Device UUID is $deviceUuid');
|
||||
}
|
||||
|
||||
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
||||
provider = 'apple';
|
||||
token = await FirebaseMessaging.instance.getAPNSToken();
|
||||
} else {
|
||||
provider = 'firebase';
|
||||
token = await FirebaseMessaging.instance.getToken();
|
||||
}
|
||||
log('Device Push Token is $token');
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.post('/notifications/subscribe', {
|
||||
'provider': provider,
|
||||
'device_token': token,
|
||||
'device_id': deviceUuid,
|
||||
});
|
||||
if (resp.statusCode != 200 && resp.statusCode != 400) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _getDeviceUuid() async {
|
||||
if (PlatformInfo.isWeb) return null;
|
||||
return await FlutterUdid.consistentUdid;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/models/notification.dart' as notify;
|
||||
import 'package:solian/models/notification.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/markdown_text_content.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
import 'package:solian/widgets/relative_date.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class NotificationScreen extends StatefulWidget {
|
||||
@ -14,57 +19,9 @@ class NotificationScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NotificationScreenState extends State<NotificationScreen> {
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _markAllRead() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final WebSocketProvider provider = Get.find();
|
||||
|
||||
List<int> markList = List.empty(growable: true);
|
||||
for (final element in provider.notifications) {
|
||||
if (element.id <= 0) continue;
|
||||
markList.add(element.id);
|
||||
}
|
||||
|
||||
if (markList.isNotEmpty) {
|
||||
final client = await auth.configureClient('auth');
|
||||
await client.put('/notifications/read', {'messages': markList});
|
||||
}
|
||||
|
||||
provider.notifications.clear();
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> _markOneRead(notify.Notification element, int index) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final WebSocketProvider provider = Get.find();
|
||||
|
||||
if (element.id <= 0) {
|
||||
provider.notifications.removeAt(index);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
await client.put('/notifications/read/${element.id}', {});
|
||||
|
||||
provider.notifications.removeAt(index);
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final WebSocketProvider ws = Get.find();
|
||||
final NotificationProvider nty = Get.find();
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.85,
|
||||
@ -77,71 +34,174 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
if (_isBusy)
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => nty.fetchNotification(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: const LinearProgressIndicator().animate().scaleX(),
|
||||
),
|
||||
if (ws.notifications.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.check),
|
||||
title: Text('notifyEmpty'.tr),
|
||||
subtitle: Text('notifyEmptyCaption'.tr),
|
||||
),
|
||||
child: LoadingIndicator(
|
||||
isActive: nty.isBusy.value,
|
||||
),
|
||||
),
|
||||
if (ws.notifications.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.checklist),
|
||||
title: Text('notifyAllRead'.tr),
|
||||
onTap: _isBusy ? null : () => _markAllRead(),
|
||||
if (nty.notifications
|
||||
.where((x) => x.readAt == null)
|
||||
.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.check),
|
||||
title: Text('notifyEmpty'.tr),
|
||||
subtitle: Text('notifyEmptyCaption'.tr),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (nty.notifications
|
||||
.where((x) => x.readAt == null)
|
||||
.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.checklist),
|
||||
title: Text('notifyAllRead'.tr),
|
||||
onTap: nty.isBusy.value
|
||||
? null
|
||||
: () => nty.markAllRead(),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList.separated(
|
||||
itemCount: nty.notifications.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var element = nty.notifications[index];
|
||||
return ClipRect(
|
||||
child: Dismissible(
|
||||
direction: element.readAt == null
|
||||
? DismissDirection.horizontal
|
||||
: DismissDirection.none,
|
||||
key: Key(const Uuid().v4()),
|
||||
background: Container(
|
||||
color: Colors.lightBlue,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20),
|
||||
alignment: Alignment.centerLeft,
|
||||
child:
|
||||
const Icon(Icons.check, color: Colors.white),
|
||||
),
|
||||
secondaryBackground: Container(
|
||||
color: Colors.lightBlue,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20),
|
||||
alignment: Alignment.centerRight,
|
||||
child:
|
||||
const Icon(Icons.check, color: Colors.white),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 28,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(NotificationTopicIcons[element.topic]),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (element.readAt == null)
|
||||
Badge(
|
||||
label: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.new_releases_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
size: 12,
|
||||
),
|
||||
const Gap(4),
|
||||
Text('unread'.tr),
|
||||
],
|
||||
),
|
||||
).paddingOnly(bottom: 4),
|
||||
Text(
|
||||
element.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
),
|
||||
if (element.subtitle != null)
|
||||
Text(
|
||||
element.subtitle!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall,
|
||||
),
|
||||
if (element.subtitle != null)
|
||||
const Gap(4),
|
||||
MarkdownTextContent(
|
||||
content: element.body,
|
||||
isAutoWarp: true,
|
||||
isSelectable: true,
|
||||
parentId:
|
||||
'notification-${element.id}',
|
||||
),
|
||||
if ([
|
||||
'interactive.feedback',
|
||||
'interactive.subscription'
|
||||
].contains(element.topic) &&
|
||||
element.metadata?['related_post'] !=
|
||||
null)
|
||||
_PostRelatedNotificationWidget(
|
||||
metadata: element.metadata!,
|
||||
),
|
||||
const Gap(8),
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
child: Row(
|
||||
children: [
|
||||
RelativeDate(
|
||||
element.createdAt,
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'·',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
const Gap(4),
|
||||
RelativeDate(
|
||||
element.createdAt,
|
||||
style: TextStyle(fontSize: 12),
|
||||
isFull: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onDismissed: (_) => nty.markOneRead(element, index),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) =>
|
||||
const Divider(thickness: 0.3, height: 0.3),
|
||||
),
|
||||
SliverList.separated(
|
||||
itemCount: ws.notifications.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var element = ws.notifications[index];
|
||||
return Dismissible(
|
||||
key: Key(const Uuid().v4()),
|
||||
background: Container(
|
||||
color: Colors.lightBlue,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: const Icon(Icons.check, color: Colors.white),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 8,
|
||||
),
|
||||
title: Text(element.title),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (element.subtitle != null)
|
||||
Text(element.subtitle!),
|
||||
Text(element.body),
|
||||
],
|
||||
),
|
||||
),
|
||||
onDismissed: (_) => _markOneRead(element, index),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) =>
|
||||
const Divider(thickness: 0.3, height: 0.3),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
@ -156,7 +216,7 @@ class NotificationButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final WebSocketProvider provider = Get.find();
|
||||
final NotificationProvider nty = Get.find();
|
||||
|
||||
final button = IconButton(
|
||||
icon: const Icon(Icons.notifications),
|
||||
@ -166,16 +226,16 @@ class NotificationButton extends StatelessWidget {
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const NotificationScreen(),
|
||||
).then((_) => provider.notificationUnread.value = 0);
|
||||
).then((_) => nty.notificationUnread.value = 0);
|
||||
},
|
||||
);
|
||||
|
||||
return Obx(() {
|
||||
if (provider.notificationUnread.value > 0) {
|
||||
if (nty.notificationUnread.value > 0) {
|
||||
return Badge(
|
||||
isLabelVisible: true,
|
||||
offset: const Offset(-8, 2),
|
||||
label: Text(provider.notificationUnread.value.toString()),
|
||||
label: Text(nty.notificationUnread.value.toString()),
|
||||
child: button,
|
||||
);
|
||||
} else {
|
||||
@ -184,3 +244,31 @@ class NotificationButton extends StatelessWidget {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PostRelatedNotificationWidget extends StatelessWidget {
|
||||
final Map<String, dynamic> metadata;
|
||||
|
||||
const _PostRelatedNotificationWidget({super.key, required this.metadata});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: PostItem(
|
||||
item: Post.fromJson(metadata['related_post']),
|
||||
isCompact: true,
|
||||
).paddingAll(8),
|
||||
),
|
||||
onTap: () {
|
||||
final data = Post.fromJson(metadata['related_post']);
|
||||
Navigator.pop(context);
|
||||
AppRouter.instance.pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'id': data.id.toString()},
|
||||
extra: data,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -348,7 +348,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
detail: _userinfo,
|
||||
profile: _userinfo!.profile,
|
||||
extraWidgets: [
|
||||
if (_dailySignRecords.isNotEmpty)
|
||||
if (_dailySignRecords.length > 1)
|
||||
Card(
|
||||
child: SizedBox(
|
||||
height: 180,
|
||||
|
@ -8,8 +8,8 @@ import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/auth.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
@ -178,7 +178,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
|
||||
Get.find<RealmProvider>().refreshAvailableRealms();
|
||||
Get.find<RelationshipProvider>().refreshRelativeList();
|
||||
Get.find<WebSocketProvider>().registerPushNotifications();
|
||||
Get.find<NotificationProvider>().registerPushNotifications();
|
||||
autoConfigureBackgroundNotificationService();
|
||||
autoStartBackgroundNotificationService();
|
||||
|
||||
|
@ -198,7 +198,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
Widget build(BuildContext context) {
|
||||
final ChatCallProvider ctrl = Get.find();
|
||||
|
||||
return RootContainer(
|
||||
return ResponsiveRootContainer(
|
||||
child: Scaffold(
|
||||
appBar: widget.hideAppBar
|
||||
? null
|
||||
|
@ -47,16 +47,19 @@ class ChatListShell extends StatelessWidget {
|
||||
direction: Axis.horizontal,
|
||||
divider: ResizableDivider(
|
||||
thickness: 0.3,
|
||||
color: Theme.of(context).dividerColor,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.3),
|
||||
),
|
||||
children: [
|
||||
const ResizableChild(
|
||||
minSize: 280,
|
||||
maxSize: 520,
|
||||
size: ResizableSize.pixels(320),
|
||||
size: ResizableSize.pixels(360),
|
||||
child: ChatList(),
|
||||
),
|
||||
ResizableChild(child: child ?? const EmptyPagePlaceholder()),
|
||||
ResizableChild(
|
||||
minSize: 280,
|
||||
child: child ?? const EmptyPagePlaceholder(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -20,7 +20,7 @@ import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/providers/daily_sign.dart';
|
||||
import 'package:solian/providers/database/services/messages.dart';
|
||||
import 'package:solian/providers/last_read.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
@ -38,7 +38,7 @@ class DashboardScreen extends StatefulWidget {
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
late final AuthProvider _auth = Get.find();
|
||||
late final LastReadProvider _lastRead = Get.find();
|
||||
late final WebSocketProvider _ws = Get.find();
|
||||
late final NotificationProvider _nty = Get.find();
|
||||
late final PostProvider _posts = Get.find();
|
||||
late final DailySignProvider _dailySign = Get.find();
|
||||
|
||||
@ -46,7 +46,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
List<Notification> get _pendingNotifications =>
|
||||
List<Notification>.from(_ws.notifications)
|
||||
List<Notification>.from(_nty.notifications.where((x) => x.readAt == null))
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
|
||||
List<Post>? _currentPosts;
|
||||
@ -254,7 +254,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
),
|
||||
Text(
|
||||
'notificationUnreadCount'.trParams({
|
||||
'count': _ws.notifications.length.toString(),
|
||||
'count': _pendingNotifications.length.toString(),
|
||||
}),
|
||||
),
|
||||
],
|
||||
@ -267,12 +267,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const NotificationScreen(),
|
||||
).then((_) => _ws.notificationUnread.value = 0);
|
||||
).then((_) => _nty.notificationUnread.value = 0);
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 18, right: 18, bottom: 8),
|
||||
if (_ws.notifications.isNotEmpty)
|
||||
if (_pendingNotifications.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 76,
|
||||
child: ListView.separated(
|
||||
|
@ -1,15 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/posts/post_action.dart';
|
||||
import 'package:solian/widgets/posts/post_owned_list.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class DraftBoxScreen extends StatefulWidget {
|
||||
const DraftBoxScreen({super.key});
|
||||
@ -19,38 +20,50 @@ class DraftBoxScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
||||
final PagingController<int, Post> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
bool _isBusy = true;
|
||||
int? _totalPosts;
|
||||
final List<Post> _posts = List.empty(growable: true);
|
||||
|
||||
_getPosts(int pageKey) async {
|
||||
final PostProvider provider = Get.find();
|
||||
_getPosts() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
Response resp;
|
||||
try {
|
||||
resp = await provider.listDraft(pageKey);
|
||||
} catch (e) {
|
||||
_pagingController.error = e;
|
||||
return;
|
||||
}
|
||||
final PostProvider posts = Get.find();
|
||||
final resp = await posts.listDraft(_posts.length);
|
||||
|
||||
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
||||
if (result.count == 0) {
|
||||
_pagingController.appendLastPage([]);
|
||||
return;
|
||||
}
|
||||
|
||||
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
|
||||
if (parsed != null && parsed.length >= 10) {
|
||||
_pagingController.appendPage(parsed, pageKey + parsed.length);
|
||||
} else if (parsed != null) {
|
||||
_pagingController.appendLastPage(parsed);
|
||||
}
|
||||
_totalPosts = result.count;
|
||||
_posts.addAll(parsed ?? List.empty());
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> _openActions(Post item) async {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => PostAction(
|
||||
item: item,
|
||||
noReact: true,
|
||||
),
|
||||
).then((value) {
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
_posts.clear();
|
||||
_getPosts();
|
||||
});
|
||||
} else if (value != null) {
|
||||
_posts.clear();
|
||||
_getPosts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pagingController.addPageRequestListener(_getPosts);
|
||||
_getPosts();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -68,47 +81,48 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
child: PagedListView<int, Post>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate(
|
||||
itemBuilder: (context, item, index) {
|
||||
return PostOwnedListEntry(
|
||||
item: item,
|
||||
isFullContent: true,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
onTap: () async {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => PostAction(
|
||||
item: item,
|
||||
noReact: true,
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_posts.clear();
|
||||
return _getPosts();
|
||||
},
|
||||
child: InfiniteList(
|
||||
itemCount: _posts.length,
|
||||
hasReachedMax: _totalPosts == _posts.length,
|
||||
isLoading: _isBusy,
|
||||
onFetchData: () => _getPosts(),
|
||||
itemBuilder: (context, index) {
|
||||
final item = _posts[index];
|
||||
return Card(
|
||||
child: GestureDetector(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PostItem(
|
||||
key: Key('p${item.id}'),
|
||||
item: item,
|
||||
isShowEmbed: false,
|
||||
isClickable: false,
|
||||
isShowReply: false,
|
||||
isReactable: false,
|
||||
onTapMore: () => _openActions(item),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
],
|
||||
),
|
||||
onTap: () => _openActions(item),
|
||||
),
|
||||
).then((value) {
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
_pagingController.refresh();
|
||||
});
|
||||
} else if (value != null) {
|
||||
_pagingController.refresh();
|
||||
}
|
||||
});
|
||||
).paddingOnly(left: 12, right: 12, bottom: 4);
|
||||
},
|
||||
).paddingOnly(left: 12, right: 12, bottom: 4);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ class PostSearchScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
int? _totalCount;
|
||||
Duration? _lastTook;
|
||||
|
||||
final TextEditingController _probeController = TextEditingController();
|
||||
final PagingController<int, Post> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
@ -43,18 +46,20 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
_pagingController.nextPageKey = 0;
|
||||
}
|
||||
|
||||
final PostProvider provider = Get.find();
|
||||
final PostProvider posts = Get.find();
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch()..start();
|
||||
|
||||
Response resp;
|
||||
try {
|
||||
if (_probeController.text.isEmpty) {
|
||||
resp = await provider.listPost(
|
||||
resp = await posts.listPost(
|
||||
pageKey,
|
||||
tag: widget.tag,
|
||||
category: widget.category,
|
||||
);
|
||||
} else {
|
||||
resp = await provider.searchPost(
|
||||
resp = await posts.searchPost(
|
||||
_probeController.text,
|
||||
pageKey,
|
||||
tag: widget.tag,
|
||||
@ -74,6 +79,11 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
_pagingController.appendLastPage(parsed);
|
||||
}
|
||||
|
||||
stopwatch.stop();
|
||||
|
||||
_totalCount = result.count;
|
||||
_lastTook = stopwatch.elapsed;
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@ -90,6 +100,9 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -136,6 +149,42 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
),
|
||||
),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (_totalCount != null || _lastTook != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.summarize_outlined,
|
||||
size: 16,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
const Gap(4),
|
||||
if (_totalCount != null)
|
||||
Text(
|
||||
'searchResult'.trParams({
|
||||
'count': _totalCount!.toString(),
|
||||
}),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
if (_lastTook != null)
|
||||
Text(
|
||||
'searchTook'.trParams({
|
||||
'time':
|
||||
'${(_lastTook!.inMilliseconds / 1000).toStringAsFixed(3)}s',
|
||||
}),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
|
@ -43,7 +43,10 @@ class RootShell extends StatelessWidget {
|
||||
|
||||
final showRailNavigation = AppTheme.isLargeScreen(context);
|
||||
|
||||
final destNames = AppNavigation.destinations.map((x) => x.page).toList();
|
||||
final destNames = [
|
||||
'postDetail',
|
||||
...AppNavigation.destinations.map((x) => x.page),
|
||||
];
|
||||
final showBottomNavigation =
|
||||
destNames.contains(routeName) && !showRailNavigation;
|
||||
|
||||
@ -52,13 +55,22 @@ class RootShell extends StatelessWidget {
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
bottomNavigationBar: showBottomNavigation
|
||||
? AppNavigationBottom(
|
||||
initialIndex: destNames.indexOf(routeName ?? 'page'),
|
||||
initialIndex: AppNavigation.destinations
|
||||
.map((x) => x.page)
|
||||
.toList()
|
||||
.indexOf(routeName ?? 'page'),
|
||||
)
|
||||
: null,
|
||||
body: AppTheme.isLargeScreen(context)
|
||||
? Row(
|
||||
children: [
|
||||
if (showRailNavigation) const AppNavigationRail(),
|
||||
if (showRailNavigation)
|
||||
AppNavigationRail(
|
||||
initialIndex: AppNavigation.destinations
|
||||
.map((x) => x.page)
|
||||
.toList()
|
||||
.indexOf(routeName ?? 'page'),
|
||||
),
|
||||
if (showRailNavigation)
|
||||
const VerticalDivider(
|
||||
width: 0.3,
|
||||
|
@ -33,12 +33,14 @@ class AttachmentEditorPopup extends StatefulWidget {
|
||||
final List<String>? initialAttachments;
|
||||
final void Function(String) onAdd;
|
||||
final void Function(String) onRemove;
|
||||
final void Function(String)? onInsert;
|
||||
|
||||
const AttachmentEditorPopup({
|
||||
super.key,
|
||||
required this.pool,
|
||||
required this.onAdd,
|
||||
required this.onRemove,
|
||||
this.onInsert,
|
||||
this.singleMode = false,
|
||||
this.imageOnly = false,
|
||||
this.autoUpload = false,
|
||||
@ -229,7 +231,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
.listMetadata(widget.initialAttachments ?? List.empty())
|
||||
.then((result) {
|
||||
setState(() {
|
||||
_attachments = List.from(result, growable: true);
|
||||
_attachments = List.from(
|
||||
result.where((x) => x != null),
|
||||
growable: true,
|
||||
);
|
||||
_isBusy = false;
|
||||
_isFirstTimeBusy = false;
|
||||
});
|
||||
@ -554,6 +559,22 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
setState(() => _attachments.removeAt(idx));
|
||||
},
|
||||
),
|
||||
if (widget.onInsert != null)
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text('insert'.tr),
|
||||
leading: const Icon(Icons.insert_link),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
widget.onInsert!(
|
||||
'',
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -8,6 +8,7 @@ import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
@ -103,9 +104,10 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final metaTextStyle = TextStyle(
|
||||
final metaTextStyle = GoogleFonts.roboto(
|
||||
fontSize: 12,
|
||||
color: _unFocusColor,
|
||||
height: 1,
|
||||
);
|
||||
|
||||
return DismissiblePage(
|
||||
@ -239,25 +241,58 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text(
|
||||
'#${widget.item.rid}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (widget.item.metadata?['width'] != null &&
|
||||
widget.item.metadata?['height'] != null)
|
||||
if (widget.item.metadata?['exif'] == null)
|
||||
Text(
|
||||
'${widget.item.metadata?['width']}x${widget.item.metadata?['height']}',
|
||||
'#${widget.item.rid}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (widget.item.metadata?['exif']?['Model'] != null)
|
||||
Text(
|
||||
'shotOn'.trParams({
|
||||
'device': widget.item.metadata?['exif']
|
||||
?['Model']
|
||||
}),
|
||||
style: metaTextStyle,
|
||||
).paddingOnly(right: 2),
|
||||
if (widget.item.metadata?['exif']?['ShutterSpeed'] !=
|
||||
null)
|
||||
Text(
|
||||
widget.item.metadata?['exif']?['ShutterSpeed'],
|
||||
style: metaTextStyle,
|
||||
).paddingOnly(right: 2),
|
||||
if (widget.item.metadata?['exif']?['ISO'] != null)
|
||||
Text(
|
||||
'ISO${widget.item.metadata?['exif']?['ISO']}',
|
||||
style: metaTextStyle,
|
||||
).paddingOnly(right: 2),
|
||||
if (widget.item.metadata?['exif']?['Aperture'] !=
|
||||
null)
|
||||
Text(
|
||||
'f/${widget.item.metadata?['exif']?['Aperture']}',
|
||||
style: metaTextStyle,
|
||||
).paddingOnly(right: 2),
|
||||
if (widget.item.metadata?['exif']?['Megapixels'] !=
|
||||
null &&
|
||||
widget.item.metadata?['exif']?['Model'] != null)
|
||||
Text(
|
||||
'${widget.item.metadata?['exif']?['Megapixels']}MP',
|
||||
style: metaTextStyle,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
widget.item.size.formatBytes(),
|
||||
style: metaTextStyle,
|
||||
),
|
||||
Text(
|
||||
'${widget.item.metadata?['width']}x${widget.item.metadata?['height']}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (widget.item.metadata?['ratio'] != null)
|
||||
Text(
|
||||
'${_getRatio().toPrecision(2)}',
|
||||
(widget.item.metadata?['ratio'] as num)
|
||||
.toStringAsFixed(2),
|
||||
style: metaTextStyle,
|
||||
),
|
||||
Text(
|
||||
widget.item.size.formatBytes(),
|
||||
style: metaTextStyle,
|
||||
),
|
||||
Text(
|
||||
widget.item.mimetype,
|
||||
style: metaTextStyle,
|
||||
|
@ -420,11 +420,13 @@ class AttachmentListEntry extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
if (item!.isMature && !showMature)
|
||||
BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -34,6 +35,24 @@ class ChatEventList extends StatelessWidget {
|
||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||
}
|
||||
|
||||
void _openActions(BuildContext context, Event item) {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => ChatEventAction(
|
||||
channel: channel,
|
||||
realm: channel.realm,
|
||||
item: item,
|
||||
onEdit: () {
|
||||
onEdit(item);
|
||||
},
|
||||
onReply: () {
|
||||
onReply(item);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
@ -65,50 +84,45 @@ class ChatEventList extends StatelessWidget {
|
||||
|
||||
final item = chatController.currentEvents[index].data;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Builder(builder: (context) {
|
||||
final widget = ChatEvent(
|
||||
key: Key('m${item!.uuid}'),
|
||||
item: item,
|
||||
isMerged: isMerged,
|
||||
chatController: chatController,
|
||||
).paddingOnly(
|
||||
top: !isMerged ? 8 : 0,
|
||||
bottom: !hasMerged ? 8 : 0,
|
||||
);
|
||||
return TapRegion(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Builder(builder: (context) {
|
||||
final widget = ChatEvent(
|
||||
key: Key('m${item!.uuid}'),
|
||||
item: item,
|
||||
isMerged: isMerged,
|
||||
chatController: chatController,
|
||||
).paddingOnly(
|
||||
top: !isMerged ? 8 : 0,
|
||||
bottom: !hasMerged ? 8 : 0,
|
||||
);
|
||||
|
||||
if (noAnimated) {
|
||||
return widget;
|
||||
} else {
|
||||
return widget
|
||||
.animate(
|
||||
key: Key('animated-m${item.uuid}'),
|
||||
)
|
||||
.slideY(
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
duration: 250.ms,
|
||||
begin: 0.5,
|
||||
end: 0,
|
||||
);
|
||||
if (noAnimated) {
|
||||
return widget;
|
||||
} else {
|
||||
return widget
|
||||
.animate(
|
||||
key: Key('animated-m${item.uuid}'),
|
||||
)
|
||||
.slideY(
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
duration: 250.ms,
|
||||
begin: 0.5,
|
||||
end: 0,
|
||||
);
|
||||
}
|
||||
}),
|
||||
onLongPress: () {
|
||||
_openActions(context, item!);
|
||||
},
|
||||
),
|
||||
onTapInside: (event) {
|
||||
if (event.buttons == kSecondaryMouseButton) {
|
||||
_openActions(context, item!);
|
||||
} else if (event.buttons == kMiddleMouseButton) {
|
||||
onReply(item!);
|
||||
}
|
||||
}),
|
||||
onLongPress: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => ChatEventAction(
|
||||
channel: channel,
|
||||
realm: channel.realm,
|
||||
item: item!,
|
||||
onEdit: () {
|
||||
onEdit(item);
|
||||
},
|
||||
onReply: () {
|
||||
onReply(item);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -2,12 +2,13 @@ import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:markdown/markdown.dart' as markdown;
|
||||
import 'package:path/path.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_item.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||
import 'package:solian/widgets/auto_cache_image.dart';
|
||||
import 'package:syntax_highlight/syntax_highlight.dart';
|
||||
@ -15,9 +16,10 @@ import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'account/account_profile_popup.dart';
|
||||
|
||||
class MarkdownTextContent extends StatelessWidget {
|
||||
class MarkdownTextContent extends StatefulWidget {
|
||||
final String content;
|
||||
final String parentId;
|
||||
final List<Attachment>? attachments;
|
||||
final bool isSelectable;
|
||||
final bool isLargeText;
|
||||
final bool isAutoWarp;
|
||||
@ -26,195 +28,221 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
super.key,
|
||||
required this.content,
|
||||
required this.parentId,
|
||||
this.attachments,
|
||||
this.isSelectable = false,
|
||||
this.isLargeText = false,
|
||||
this.isAutoWarp = false,
|
||||
});
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
@override
|
||||
State<MarkdownTextContent> createState() => _MarkdownTextContentState();
|
||||
}
|
||||
|
||||
class _MarkdownTextContentState extends State<MarkdownTextContent> {
|
||||
final List<int> _stickerSizes = [];
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
final stickerRegex = RegExp(r':([-\w]+):');
|
||||
|
||||
// Split the content into paragraphs
|
||||
final paragraphs = content.split(RegExp(r'\n\s*\n'));
|
||||
final paragraphs = widget.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];
|
||||
|
||||
// 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(
|
||||
textScaler: TextScaler.linear(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,
|
||||
),
|
||||
),
|
||||
),
|
||||
codeblockDecoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 0.3,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
)),
|
||||
builders: {
|
||||
'code': _MarkdownTextCodeElement(),
|
||||
},
|
||||
softLineBreak: true,
|
||||
extensionSet: markdown.ExtensionSet(
|
||||
<markdown.BlockSyntax>[
|
||||
markdown.CodeBlockSyntax(),
|
||||
...markdown.ExtensionSet.commonMark.blockSyntaxes,
|
||||
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||
],
|
||||
<markdown.InlineSyntax>[
|
||||
if (isAutoWarp) markdown.LineBreakSyntax(),
|
||||
_UserNameCardInlineSyntax(),
|
||||
_CustomEmoteInlineSyntax(),
|
||||
markdown.AutolinkSyntax(),
|
||||
markdown.AutolinkExtensionSyntax(),
|
||||
markdown.CodeSyntax(),
|
||||
...markdown.ExtensionSet.commonMark.inlineSyntaxes,
|
||||
...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();
|
||||
|
||||
// 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(isAutoWarp ? const Gap(4) : const Gap(8));
|
||||
if (stickerMatch.length > 3) {
|
||||
_stickerSizes.addAll(List.filled(stickerMatch.length, 16));
|
||||
} else if (stickerMatch.length > 1) {
|
||||
_stickerSizes.addAll(List.filled(stickerMatch.length, 32));
|
||||
} else {
|
||||
_stickerSizes.addAll(List.filled(stickerMatch.length, 128));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the list of widgets for the paragraphs
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: contentWidgets,
|
||||
Widget _buildContent(BuildContext context) {
|
||||
var stickerIdx = 0;
|
||||
|
||||
return Markdown(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
data: widget.content,
|
||||
padding: EdgeInsets.zero,
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||
Theme.of(context),
|
||||
).copyWith(
|
||||
textScaler: TextScaler.linear(widget.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
codeblockDecoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 0.3,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
)),
|
||||
builders: {
|
||||
'code': _MarkdownTextCodeElement(),
|
||||
},
|
||||
softLineBreak: true,
|
||||
extensionSet: markdown.ExtensionSet(
|
||||
<markdown.BlockSyntax>[
|
||||
markdown.CodeBlockSyntax(),
|
||||
...markdown.ExtensionSet.commonMark.blockSyntaxes,
|
||||
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||
],
|
||||
<markdown.InlineSyntax>[
|
||||
if (widget.isAutoWarp) markdown.LineBreakSyntax(),
|
||||
_UserNameCardInlineSyntax(),
|
||||
_CustomEmoteInlineSyntax(),
|
||||
markdown.AutolinkSyntax(),
|
||||
markdown.AutolinkExtensionSyntax(),
|
||||
markdown.CodeSyntax(),
|
||||
...markdown.ExtensionSet.commonMark.inlineSyntaxes,
|
||||
...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 = 4;
|
||||
final StickerProvider sticker = Get.find();
|
||||
|
||||
// Adjust sticker size based on the sticker count in this paragraph
|
||||
width =
|
||||
_stickerSizes.elementAtOrNull(stickerIdx)?.toDouble() ?? 16;
|
||||
height =
|
||||
_stickerSizes.elementAtOrNull(stickerIdx)?.toDouble() ?? 16;
|
||||
if (width > 16) {
|
||||
radius = 8;
|
||||
}
|
||||
stickerIdx++;
|
||||
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':
|
||||
final match = widget.attachments
|
||||
?.where((x) => x.rid == segments[1])
|
||||
.firstOrNull;
|
||||
const radius = BorderRadius.all(Radius.circular(8));
|
||||
if (match != null) {
|
||||
final isImage =
|
||||
match.mimetype.split('/').firstOrNull == 'image';
|
||||
double ratio = match.metadata?['ratio']?.toDouble() ??
|
||||
(isImage ? 1 : 16 / 9);
|
||||
return LimitedBox(
|
||||
maxWidth: 480,
|
||||
maxHeight: 640,
|
||||
child: AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: AttachmentItem(
|
||||
parentId: widget.parentId,
|
||||
item: match,
|
||||
),
|
||||
),
|
||||
),
|
||||
).paddingSymmetric(vertical: 4);
|
||||
} else {
|
||||
return LimitedBox(
|
||||
maxHeight: MediaQuery.of(context).size.width,
|
||||
child: ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: AttachmentSelfContainedEntry(
|
||||
isDense: true,
|
||||
parentId: widget.parentId,
|
||||
rid: segments[1],
|
||||
),
|
||||
),
|
||||
).paddingSymmetric(vertical: 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
return AutoCacheImage(
|
||||
url,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isSelectable) {
|
||||
if (widget.isSelectable) {
|
||||
return SelectionArea(child: _buildContent(context));
|
||||
}
|
||||
return _buildContent(context);
|
||||
|
@ -91,17 +91,21 @@ class _PostActionState extends State<PostAction> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final double width = hasMultipleAttachment ? 640 : 480;
|
||||
|
||||
final screenshot = ScreenshotController();
|
||||
final image = await screenshot.captureFromLongWidget(
|
||||
MediaQuery(
|
||||
data: MediaQuery.of(context),
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
size: Size(width, double.infinity),
|
||||
),
|
||||
child: PostShareImage(item: widget.item),
|
||||
),
|
||||
context: context,
|
||||
pixelRatio: 2,
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 480,
|
||||
maxWidth: hasMultipleAttachment ? 640 : 480,
|
||||
maxWidth: width,
|
||||
minHeight: 640,
|
||||
maxHeight: double.infinity,
|
||||
),
|
||||
|
@ -110,6 +110,7 @@ class _PostItemState extends State<PostItem> {
|
||||
child: MarkdownTextContent(
|
||||
parentId: 'p${item.id}',
|
||||
content: item.body['content'],
|
||||
attachments: item.preload?.attachments,
|
||||
isAutoWarp: item.type == 'story',
|
||||
isSelectable: widget.isContentSelectable,
|
||||
),
|
||||
@ -131,21 +132,12 @@ class _PostItemState extends State<PostItem> {
|
||||
right: 8,
|
||||
),
|
||||
if (attachments.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.file_copy,
|
||||
size: 15,
|
||||
color: _unFocusColor,
|
||||
).paddingOnly(right: 5),
|
||||
Text(
|
||||
'attachmentHint'.trParams(
|
||||
{'count': attachments.length.toString()},
|
||||
),
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
)
|
||||
],
|
||||
).paddingOnly(left: 14, top: 4),
|
||||
_PostAttachmentWidget(
|
||||
item: item,
|
||||
padding: widget.padding,
|
||||
isCompact: true,
|
||||
isNonScrollAttachment: widget.isNonScrollAttachment,
|
||||
).paddingOnly(top: 4),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -173,6 +165,7 @@ class _PostItemState extends State<PostItem> {
|
||||
child: MarkdownTextContent(
|
||||
parentId: 'p${item.id}-embed',
|
||||
content: item.body['content'],
|
||||
attachments: item.preload?.attachments,
|
||||
isAutoWarp: item.type == 'story',
|
||||
isSelectable: widget.isContentSelectable,
|
||||
),
|
||||
@ -220,6 +213,7 @@ class _PostItemState extends State<PostItem> {
|
||||
_PostAttachmentWidget(
|
||||
item: item,
|
||||
padding: widget.padding,
|
||||
isCompact: item.type == 'article',
|
||||
isNonScrollAttachment: widget.isNonScrollAttachment,
|
||||
),
|
||||
if (widget.showFeaturedReply)
|
||||
@ -388,11 +382,13 @@ class _PostAttachmentWidget extends StatelessWidget {
|
||||
final Post item;
|
||||
final EdgeInsets? padding;
|
||||
final bool isNonScrollAttachment;
|
||||
final bool isCompact;
|
||||
|
||||
const _PostAttachmentWidget({
|
||||
required this.item,
|
||||
required this.padding,
|
||||
required this.isNonScrollAttachment,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -403,8 +399,32 @@ class _PostAttachmentWidget extends StatelessWidget {
|
||||
? List.from(item.body['attachments']?.whereType<String>())
|
||||
: List.empty();
|
||||
|
||||
final unFocusColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
if (isCompact) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.file_copy,
|
||||
size: 13,
|
||||
color: unFocusColor,
|
||||
).paddingOnly(right: 5),
|
||||
Text(
|
||||
'attachmentHint'.trParams(
|
||||
{'count': attachments.length.toString()},
|
||||
),
|
||||
style: TextStyle(color: unFocusColor, fontSize: 13),
|
||||
)
|
||||
],
|
||||
).paddingOnly(
|
||||
left: (padding?.left ?? 0) + 17,
|
||||
right: (padding?.right ?? 0) + 17,
|
||||
);
|
||||
}
|
||||
|
||||
if (attachments.length == 1 && !isLargeScreen) {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
@ -422,7 +442,10 @@ class _PostAttachmentWidget extends StatelessWidget {
|
||||
attachments: item.preload?.attachments,
|
||||
autoload: false,
|
||||
isGrid: true,
|
||||
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||
).paddingOnly(
|
||||
left: (padding?.left ?? 0) + 14,
|
||||
right: (padding?.right ?? 0) + 14,
|
||||
);
|
||||
} else if (attachments.length == 1 || isNonScrollAttachment) {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
@ -430,7 +453,10 @@ class _PostAttachmentWidget extends StatelessWidget {
|
||||
attachments: item.preload?.attachments,
|
||||
autoload: false,
|
||||
isColumn: true,
|
||||
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||
).paddingOnly(
|
||||
left: (padding?.left ?? 0) + 14,
|
||||
right: (padding?.right ?? 0) + 14,
|
||||
);
|
||||
} else {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
@ -526,7 +552,7 @@ class _PostHeaderDividerWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (item.body['description'] != null || item.body['title'] != null) {
|
||||
return const Gap(8);
|
||||
return const SizedBox(height: 8);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@ -660,6 +686,11 @@ class _PostHeaderWidget extends StatelessWidget {
|
||||
IconButton(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
icon: const Icon(Icons.more_vert),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -2,
|
||||
),
|
||||
onPressed: () => onTapMore!(),
|
||||
),
|
||||
],
|
||||
|
@ -1,41 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
|
||||
class PostOwnedListEntry extends StatelessWidget {
|
||||
final Post item;
|
||||
final Function onTap;
|
||||
final bool isFullContent;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const PostOwnedListEntry({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.onTap,
|
||||
this.isFullContent = false,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: GestureDetector(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PostItem(
|
||||
key: Key('p${item.id}'),
|
||||
item: item,
|
||||
isShowEmbed: false,
|
||||
isClickable: false,
|
||||
isShowReply: false,
|
||||
isReactable: false,
|
||||
).paddingSymmetric(vertical: 8),
|
||||
],
|
||||
),
|
||||
onTap: () => onTap(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -4,20 +4,25 @@ import 'package:timeago/timeago.dart';
|
||||
|
||||
class RelativeDate extends StatelessWidget {
|
||||
final DateTime date;
|
||||
final TextStyle? style;
|
||||
final bool isFull;
|
||||
|
||||
const RelativeDate(this.date, {super.key, this.isFull = false});
|
||||
const RelativeDate(this.date, {super.key, this.style, this.isFull = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isFull) {
|
||||
return Text(DateFormat('y/M/d HH:mm').format(date));
|
||||
return Text(
|
||||
DateFormat('y/M/d HH:mm').format(date),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
format(
|
||||
date,
|
||||
locale: 'en_short',
|
||||
),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -351,9 +351,9 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
device_info_plus: f1aae8670672f75c4c8850ecbe0b2ddef62b0a22
|
||||
device_info_plus: 74e614483d05c89290d30a4c8feae15d555f7427
|
||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
||||
@ -386,7 +386,7 @@ SPEC CHECKSUMS:
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
package_info_plus: d2f71247aab4b6521434f887276093acc70d214c
|
||||
package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523
|
||||
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
@ -394,7 +394,7 @@ SPEC CHECKSUMS:
|
||||
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
||||
share_plus: a182a58e04e51647c0481aadabbc4de44b3a2bce
|
||||
share_plus: fd717ef89a2801d3491e737630112b80c310640e
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
|
96
pubspec.lock
96
pubspec.lock
@ -254,14 +254,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
confetti:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: confetti
|
||||
sha256: "79376a99648efbc3f23582f5784ced0fe239922bd1a0fb41f582051eba750751"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
connectivity_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "2056db5241f96cdc0126bd94459fc4cdc13876753768fc7a31c425e50a7177d0"
|
||||
sha256: "876849631b0c7dc20f8b471a2a03142841b482438e3b707955464f5ffca3e4c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.5"
|
||||
version: "6.1.0"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -274,10 +282,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
cross_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -290,10 +298,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.6"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -338,10 +346,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: db03b2d2a3fa466a4627709e1db58692c3f7f658e36a5942d342d86efedc4091
|
||||
sha256: c4af09051b4f0508f6c1dc0a5c085bf014d5c9a4a0678ce1799c2b4d716387a0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.0"
|
||||
version: "11.1.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -378,26 +386,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: d6ff1ec6a0f3fa097dda6b776cf601f1f3d88b53b287288e09c1306f394fb1b3
|
||||
sha256: df027d168a2985a2e9da900adeba2ab0136f0d84436592cf3cd5135f82c8579c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.20.3"
|
||||
version: "2.21.0"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "3ee987578ca2281b5ff91eadd757cd6dd36001458d6e33784f990d67ff38f756"
|
||||
sha256: "27bab15e7869b69259663590381180117873b9b273a1ea9ebb21bb73133d1233"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.20.3"
|
||||
version: "2.21.0"
|
||||
drift_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift_flutter
|
||||
sha256: c670c947fe17ad149678a43fdbbfdb69321f0c83d315043e34e8ad2729e11f49
|
||||
sha256: fec503e9d408f36bb345f9f6d24bc9d62b7b5f970db49760253d9e8d3acd48d5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "0.2.1"
|
||||
dropdown_button2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -618,10 +626,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
fl_chart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1049,10 +1057,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
||||
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
version: "4.3.0"
|
||||
image_cropper:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1089,26 +1097,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: d3e5e00fdfeca8fd4ffb3227001264d449cc8950414c2ff70b0e06b9c628e643
|
||||
sha256: d34e0d9e024e81321b2aeed7b202ec6181cc282e6a1c0c0b4e6ad07ef1065d82
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+15"
|
||||
version: "0.8.12+16"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50"
|
||||
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.6"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447"
|
||||
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12"
|
||||
version: "0.8.12+1"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1249,10 +1257,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
macos_window_utils:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1393,10 +1401,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
version: "2.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1433,10 +1441,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef"
|
||||
sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.3"
|
||||
version: "8.1.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1529,10 +1537,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa"
|
||||
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.12"
|
||||
version: "12.0.13"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1577,10 +1585,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
version: "3.1.6"
|
||||
platform_detect:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1825,10 +1833,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11
|
||||
sha256: "334fcdf0ef9c0df0e3b428faebcac9568f35c747d59831474b2fc56e156d244e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.3"
|
||||
version: "10.1.0"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2014,10 +2022,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "852cf80f9e974ac8e1b613758a8aa640215f7701352b66a7f468e95711eb570b"
|
||||
sha256: c5f63dff8677407ddcddfa4744c176ea6dc44286c47ba9e69e76d8071398034d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.38.1"
|
||||
version: "0.39.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2118,10 +2126,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.4.0"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2206,10 +2214,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
|
||||
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
version: "3.1.3"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -2342,10 +2350,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd
|
||||
sha256: "2294c64768987ea280b43a3d8357d42d5679f3e2b5b69b602be45b2abbd165b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.6.0"
|
||||
version: "5.6.1"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2,7 +2,7 @@ name: solian
|
||||
description: "The Solar Network App"
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.3.8+13
|
||||
version: 1.4.0+17
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.4 <4.0.0"
|
||||
@ -90,6 +90,7 @@ dependencies:
|
||||
flutter_resizable_container: ^3.0.0
|
||||
file_saver: ^0.2.14
|
||||
marquee: ^2.3.0
|
||||
confetti: ^0.8.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
x
Reference in New Issue
Block a user