Compare commits

...

12 Commits

Author SHA1 Message Date
e4b741ff0c 🚀 Launch 1.2.1+40 2024-09-17 16:02:13 +08:00
e69abb7f9d Notification preferences 2024-09-17 15:59:17 +08:00
565a8e41cc Realm avatar, banner 2024-09-17 14:21:37 +08:00
c9fbe47337 Channel isPublic and isCommunity 2024-09-17 13:50:04 +08:00
01db63e297 🐛 Fix compability on iOS 18 and macOS 15 2024-09-17 13:39:08 +08:00
d87e67bd17 Subscriptions 2024-09-17 02:14:23 +08:00
06aa1fb359 🐛 Fix post last read at 2024-09-17 01:23:49 +08:00
62733bf29f 💄 Optimize featured reply style 2024-09-16 23:39:15 +08:00
ce16de9c71 Featured replies on post 2024-09-16 23:35:44 +08:00
47eb6cbc66 Chat list will also show wild group channel 2024-09-16 21:09:19 +08:00
029e72fb0b Improve sticker loading 2024-09-16 21:00:19 +08:00
152efd97a0 💄 Unified design of single attachment uploader 2024-09-16 20:33:34 +08:00
60 changed files with 877 additions and 577 deletions

View File

@ -157,6 +157,9 @@
"postListNews": "News", "postListNews": "News",
"postListFriends": "Friends", "postListFriends": "Friends",
"postListShuffle": "Random", "postListShuffle": "Random",
"attachmentThumbnail": "Thumbnail",
"attachmentThumbnailAttachmentNew": "Upload thumbnail",
"attachmentThumbnailAttachment": "Attachment serial number",
"postEditorModeStory": "Post a post", "postEditorModeStory": "Post a post",
"postEditorModeArticle": "Post an article", "postEditorModeArticle": "Post an article",
"postEditor": "Post editor", "postEditor": "Post editor",
@ -225,6 +228,8 @@
"realmDescription": "Description", "realmDescription": "Description",
"realmPublic": "Public Realm", "realmPublic": "Public Realm",
"realmCommunity": "Community Realm", "realmCommunity": "Community Realm",
"realmAvatar": "Realm avatar",
"realmBanner": "Realm banner",
"realmDetail": "Realm detail", "realmDetail": "Realm detail",
"realmMember": "Realm member", "realmMember": "Realm member",
"realmMembers": "Realm members", "realmMembers": "Realm members",
@ -250,7 +255,8 @@
"channelName": "Name", "channelName": "Name",
"channelDescription": "Description", "channelDescription": "Description",
"channelDirectDescription": "Direct message with @username", "channelDirectDescription": "Direct message with @username",
"channelEncrypted": "Encrypted Channel", "channelPublic": "Public channel",
"channelCommunity": "Community channel",
"channelMember": "Channel member", "channelMember": "Channel member",
"channelMembers": "Channel members", "channelMembers": "Channel members",
"channelMembersAdd": "Add channel members", "channelMembersAdd": "Add channel members",
@ -407,5 +413,14 @@
"userLevel13": "Immortal", "userLevel13": "Immortal",
"postBrowsingIn": "Browsing in @region", "postBrowsingIn": "Browsing in @region",
"needRestartToApply": "Restart the application to take effect", "needRestartToApply": "Restart the application to take effect",
"holdToSeeDetail": "Long press / Mouse hover to see detail" "holdToSeeDetail": "Long press / Mouse hover to see detail",
"subscribe": "Subscribe",
"subscribed": "Subscribed",
"unsubscribe": "Unsubscribe",
"preferences": "Preferences",
"notificationPreferences": "Notification preferences",
"notificationTopicPostFeedback": "Post feedbacks",
"notificationTopicPostSubscription": "Post subscriptions",
"preferencesApplied": "Preferences has been applied.",
"save": "Save"
} }

View File

@ -168,6 +168,9 @@
"postListNews": "新鲜事", "postListNews": "新鲜事",
"postListFriends": "好友圈", "postListFriends": "好友圈",
"postListShuffle": "打乱看", "postListShuffle": "打乱看",
"attachmentThumbnail": "附件缩略图",
"attachmentThumbnailAttachmentNew": "上传附件作为缩略图",
"attachmentThumbnailAttachment": "附件序列号",
"postNew": "创建新帖子", "postNew": "创建新帖子",
"postNewInRealmHint": "在领域 @realm 里发表新帖子", "postNewInRealmHint": "在领域 @realm 里发表新帖子",
"postAction": "发表", "postAction": "发表",
@ -226,6 +229,8 @@
"realmDescription": "领域简介", "realmDescription": "领域简介",
"realmPublic": "公开领域", "realmPublic": "公开领域",
"realmCommunity": "社区领域", "realmCommunity": "社区领域",
"realmAvatar": "领域头像",
"realmBanner": "领域横幅",
"realmDetail": "领域详情", "realmDetail": "领域详情",
"realmMember": "领域成员", "realmMember": "领域成员",
"realmMembers": "领域成员", "realmMembers": "领域成员",
@ -251,7 +256,8 @@
"channelName": "显示名称", "channelName": "显示名称",
"channelDescription": "频道简介", "channelDescription": "频道简介",
"channelDirectDescription": "与 @username 的私聊", "channelDirectDescription": "与 @username 的私聊",
"channelEncrypted": "加密频道", "channelPublic": "公开频道",
"channelCommunity": "社区频道",
"channelMember": "频道成员", "channelMember": "频道成员",
"channelMembers": "频道成员", "channelMembers": "频道成员",
"channelMembersAdd": "添加频道成员", "channelMembersAdd": "添加频道成员",
@ -408,5 +414,14 @@
"userLevel13": "万古流芳", "userLevel13": "万古流芳",
"postBrowsingIn": "浏览 @region 内的帖子中", "postBrowsingIn": "浏览 @region 内的帖子中",
"needRestartToApply": "需要重启应用来生效", "needRestartToApply": "需要重启应用来生效",
"holdToSeeDetail": "长按 / 鼠标悬浮来查看详情" "holdToSeeDetail": "长按 / 鼠标悬浮来查看详情",
"subscribe": "订阅",
"subscribed": "已订阅",
"unsubscribe": "取消订阅",
"preferences": "偏好设置",
"notificationPreferences": "通知偏好设置",
"notificationTopicPostFeedback": "帖子反馈",
"notificationTopicPostSubscription": "订阅源",
"preferencesApplied": "偏好设置已应用",
"save": "保存"
} }

View File

@ -73,7 +73,7 @@ PODS:
- Firebase/Performance (= 11.0.0) - Firebase/Performance (= 11.0.0)
- firebase_core - firebase_core
- Flutter - Flutter
- FirebaseABTesting (11.1.0): - FirebaseABTesting (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseAnalytics (11.0.0): - FirebaseAnalytics (11.0.0):
- FirebaseAnalytics/AdIdSupport (= 11.0.0) - FirebaseAnalytics/AdIdSupport (= 11.0.0)
@ -97,9 +97,9 @@ PODS:
- FirebaseCoreInternal (~> 11.0) - FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.1.0): - FirebaseCoreExtension (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreInternal (11.1.0): - FirebaseCoreInternal (11.2.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseCrashlytics (11.0.0): - FirebaseCrashlytics (11.0.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
@ -110,7 +110,7 @@ PODS:
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseInstallations (11.1.0): - FirebaseInstallations (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
@ -134,7 +134,7 @@ PODS:
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseRemoteConfig (11.1.0): - FirebaseRemoteConfig (11.2.0):
- FirebaseABTesting (~> 11.0) - FirebaseABTesting (~> 11.0)
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
@ -142,8 +142,8 @@ PODS:
- FirebaseSharedSwift (~> 11.0) - FirebaseSharedSwift (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseRemoteConfigInterop (11.1.0) - FirebaseRemoteConfigInterop (11.2.0)
- FirebaseSessions (11.1.0): - FirebaseSessions (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreExtension (~> 11.0) - FirebaseCoreExtension (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
@ -152,7 +152,7 @@ PODS:
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1) - PromisesSwift (~> 2.1)
- FirebaseSharedSwift (11.1.0) - FirebaseSharedSwift (11.2.0)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_background_service_ios (0.0.3): - flutter_background_service_ios (0.0.3):
- Flutter - Flutter
@ -450,19 +450,19 @@ SPEC CHECKSUMS:
firebase_crashlytics: 4111f8198b78c99471c955af488cecd8224967e6 firebase_crashlytics: 4111f8198b78c99471c955af488cecd8224967e6
firebase_messaging: c40f84e7a98da956d5262fada373b5c458edcf13 firebase_messaging: c40f84e7a98da956d5262fada373b5c458edcf13
firebase_performance: 8b7b9ca5adf3a9b3afa12b4eb96b9cabefc2c248 firebase_performance: 8b7b9ca5adf3a9b3afa12b4eb96b9cabefc2c248
FirebaseABTesting: c2e22c3aab99afa81d0561708b2c1c356c556976 FirebaseABTesting: 2104d957ce33888a3d6f3bde298cdee376dde8f1
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383 FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa FirebaseCoreExtension: cda74ddfb001224bd8fd1d6e74698b4ed07803de
FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c FirebaseCoreInternal: 0c569513412da9f3b31bd0b340013bbee8f295c5
FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57 FirebaseInstallations: 771177d89d6c451dc6e50085ec82e2fc77ed0a4a
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742 FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
FirebasePerformance: efdc02bacb1b4710588c9f867011605c081cdf79 FirebasePerformance: efdc02bacb1b4710588c9f867011605c081cdf79
FirebaseRemoteConfig: 05521e937b72e01847a7128da5a492327364c705 FirebaseRemoteConfig: fca0b2d017fc1de52b28a4e5bcf2007c1a840457
FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87 FirebaseRemoteConfigInterop: 477b26fdeb8fb5fbaf22fa9db5343b42289dc7db
FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a FirebaseSessions: adcec8b72d0066a385e3affcd1bcb1ebb3908ce6
FirebaseSharedSwift: 260a35e08943ec810d820a70bc0359136351d0c5 FirebaseSharedSwift: 7a0d78d155ede78407f0fdc89fbc914014c7c540
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069

View File

@ -616,6 +616,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@ -920,6 +921,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@ -947,6 +949,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";

View File

@ -9,7 +9,6 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@ -115,7 +114,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
try { try {
await Future.wait([ await Future.wait([
Get.find<StickerProvider>().refreshAvailableStickers(),
if (auth.isAuthorized.isTrue) if (auth.isAuthorized.isTrue)
Get.find<ChannelProvider>().refreshAvailableChannel(), Get.find<ChannelProvider>().refreshAvailableChannel(),
if (auth.isAuthorized.isTrue) if (auth.isAuthorized.isTrue)

View File

@ -20,6 +20,7 @@ import 'package:solian/providers/last_read.dart';
import 'package:solian/providers/link_expander.dart'; import 'package:solian/providers/link_expander.dart';
import 'package:solian/providers/navigation.dart'; import 'package:solian/providers/navigation.dart';
import 'package:solian/providers/stickers.dart'; import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/subscription.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
@ -151,6 +152,7 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => LinkExpandProvider()); Get.lazyPut(() => LinkExpandProvider());
Get.lazyPut(() => DailySignProvider()); Get.lazyPut(() => DailySignProvider());
Get.lazyPut(() => LastReadProvider()); Get.lazyPut(() => LastReadProvider());
Get.lazyPut(() => SubscriptionProvider());
Get.find<WebSocketProvider>().requestPermissions(); Get.find<WebSocketProvider>().requestPermissions();
} }

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'account.g.dart'; part 'account.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'account_status.g.dart'; part 'account_status.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
part 'attachment.g.dart'; part 'attachment.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
part 'auth.g.dart'; part 'auth.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
@ -19,7 +19,8 @@ class Channel {
int accountId; int accountId;
Realm? realm; Realm? realm;
int? realmId; int? realmId;
bool isEncrypted; bool isPublic;
bool isCommunity;
@JsonKey(includeFromJson: false, includeToJson: true) @JsonKey(includeFromJson: false, includeToJson: true)
bool isAvailable = false; bool isAvailable = false;
@ -36,7 +37,8 @@ class Channel {
required this.members, required this.members,
required this.account, required this.account,
required this.accountId, required this.accountId,
required this.isEncrypted, required this.isPublic,
required this.isCommunity,
required this.realm, required this.realm,
required this.realmId, required this.realmId,
}); });

View File

@ -22,7 +22,8 @@ Channel _$ChannelFromJson(Map<String, dynamic> json) => Channel(
.toList(), .toList(),
account: Account.fromJson(json['account'] as Map<String, dynamic>), account: Account.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
isEncrypted: json['is_encrypted'] as bool, isPublic: json['is_public'] as bool,
isCommunity: json['is_community'] as bool,
realm: json['realm'] == null realm: json['realm'] == null
? null ? null
: Realm.fromJson(json['realm'] as Map<String, dynamic>), : Realm.fromJson(json['realm'] as Map<String, dynamic>),
@ -43,7 +44,8 @@ Map<String, dynamic> _$ChannelToJson(Channel instance) => <String, dynamic>{
'account_id': instance.accountId, 'account_id': instance.accountId,
'realm': instance.realm?.toJson(), 'realm': instance.realm?.toJson(),
'realm_id': instance.realmId, 'realm_id': instance.realmId,
'is_encrypted': instance.isEncrypted, 'is_public': instance.isPublic,
'is_community': instance.isCommunity,
'is_available': instance.isAvailable, 'is_available': instance.isAvailable,
}; };

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
part 'event.g.dart'; part 'event.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'link.g.dart'; part 'link.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'notification.g.dart'; part 'notification.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'packet.g.dart'; part 'packet.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'pagination.g.dart'; part 'pagination.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/post_categories.dart'; import 'package:solian/models/post_categories.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'post_categories.g.dart'; part 'post_categories.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
part 'realm.g.dart'; part 'realm.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
part 'relations.g.dart'; part 'relations.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';

View File

@ -0,0 +1,41 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/post_categories.dart';
part 'subscription.g.dart';
@JsonSerializable()
class Subscription {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
int followerId;
Account follower;
int? accountId;
Account? account;
int? tagId;
Tag? tag;
int? categoryId;
Category? category;
Subscription({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.followerId,
required this.follower,
required this.accountId,
required this.account,
required this.tagId,
required this.tag,
required this.categoryId,
required this.category,
});
factory Subscription.fromJson(Map<String, dynamic> json) =>
_$SubscriptionFromJson(json);
Map<String, dynamic> toJson() => _$SubscriptionToJson(this);
}

View File

@ -0,0 +1,46 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'subscription.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Subscription _$SubscriptionFromJson(Map<String, dynamic> json) => Subscription(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
followerId: (json['follower_id'] as num).toInt(),
follower: Account.fromJson(json['follower'] as Map<String, dynamic>),
accountId: (json['account_id'] as num?)?.toInt(),
account: json['account'] == null
? null
: Account.fromJson(json['account'] as Map<String, dynamic>),
tagId: (json['tag_id'] as num?)?.toInt(),
tag: json['tag'] == null
? null
: Tag.fromJson(json['tag'] as Map<String, dynamic>),
categoryId: (json['category_id'] as num?)?.toInt(),
category: json['category'] == null
? null
: Category.fromJson(json['category'] as Map<String, dynamic>),
);
Map<String, dynamic> _$SubscriptionToJson(Subscription instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'follower_id': instance.followerId,
'follower': instance.follower.toJson(),
'account_id': instance.accountId,
'account': instance.account?.toJson(),
'tag_id': instance.tagId,
'tag': instance.tag?.toJson(),
'category_id': instance.categoryId,
'category': instance.category?.toJson(),
};

View File

@ -1,6 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart'; import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart'; import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@ -96,6 +97,15 @@ class PostProvider extends GetConnect {
return resp; return resp;
} }
Future<List<Post>> listPostFeaturedReply(String alias, {int take = 1}) async {
final resp = await get('/posts/$alias/replies/featured?take=$take');
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return List<Post>.from(resp.body.map((x) => Post.fromJson(x)));
}
Future<Response> getPost(String alias) async { Future<Response> getPost(String alias) async {
final resp = await get('/posts/$alias'); final resp = await get('/posts/$alias');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {

View File

@ -1,34 +1,48 @@
import 'dart:async';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/exceptions/request.dart';
import 'package:solian/models/stickers.dart'; import 'package:solian/models/stickers.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
class StickerProvider extends GetxController { class StickerProvider extends GetxController {
final RxMap<String, String> aliasImageMapping = RxMap(); final RxMap<String, FutureOr<Sticker?>> stickerCache = RxMap();
final RxList<Sticker> availableStickers = RxList.empty(growable: true);
Future<void> refreshAvailableStickers() async { Future<Sticker?> getStickerByAlias(String alias) {
availableStickers.clear(); if (stickerCache.containsKey(alias)) {
aliasImageMapping.clear(); return Future.value(stickerCache[alias]);
}
stickerCache[alias] = Future(() async {
final client = await ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/lookup/$alias',
);
if (resp.statusCode != 200) {
if (resp.statusCode == 404) {
stickerCache[alias] = null;
}
throw RequestException(resp);
}
return Sticker.fromJson(resp.body);
}).then((result) {
stickerCache[alias] = result;
return result;
});
return Future.value(stickerCache[alias]);
}
Future<List<Sticker>> searchStickerByAlias(String alias) async {
final client = await ServiceFinder.configureClient('files'); final client = await ServiceFinder.configureClient('files');
final resp = await client.get( final resp = await client.get(
'/stickers/manifest?take=100', '/stickers/lookup?probe=$alias',
); );
if (resp.statusCode == 200) { if (resp.statusCode != 200) {
final result = PaginationResult.fromJson(resp.body); throw RequestException(resp);
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
if (out == null) return;
for (final pack in out) {
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
sticker.pack = pack;
aliasImageMapping[sticker.textPlaceholder.toUpperCase()] =
sticker.imageUrl;
availableStickers.add(sticker);
}
}
} }
availableStickers.refresh();
return List<Sticker>.from(resp.body.map((x) => Sticker.fromJson(x)));
} }
} }

View File

@ -0,0 +1,46 @@
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/subscription.dart';
import 'package:solian/providers/auth.dart';
class SubscriptionProvider extends GetxController {
Future<Subscription?> getSubscriptionOnUser(int userId) async {
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw const UnauthorizedException();
final client = await auth.configureClient('co');
final resp = await client.get('/subscriptions/users/$userId');
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Subscription.fromJson(resp.body);
}
Future<Subscription> subscribeToUser(int userId) async {
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw const UnauthorizedException();
final client = await auth.configureClient('co');
final resp = await client.post('/subscriptions/users/$userId', {});
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Subscription.fromJson(resp.body);
}
Future<void> unsubscribeFromUser(int userId) async {
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw const UnauthorizedException();
final client = await auth.configureClient('co');
final resp = await client.delete('/subscriptions/users/$userId');
if (resp.statusCode != 200) {
throw RequestException(resp);
}
}
}

View File

@ -5,9 +5,9 @@ import 'package:solian/models/realm.dart';
import 'package:solian/screens/about.dart'; import 'package:solian/screens/about.dart';
import 'package:solian/screens/account.dart'; import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/account/personalize.dart'; import 'package:solian/screens/account/preferences/notifications.dart';
import 'package:solian/screens/account/profile_edit.dart';
import 'package:solian/screens/account/profile_page.dart'; import 'package:solian/screens/account/profile_page.dart';
import 'package:solian/screens/account/stickers.dart';
import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signin.dart';
import 'package:solian/screens/auth/signup.dart'; import 'package:solian/screens/auth/signup.dart';
import 'package:solian/screens/channel/channel_chat.dart'; import 'package:solian/screens/channel/channel_chat.dart';
@ -238,14 +238,6 @@ abstract class AppRouter {
name: 'accountFriend', name: 'accountFriend',
builder: (context, state) => const FriendScreen(), builder: (context, state) => const FriendScreen(),
), ),
GoRoute(
path: '/account/stickers',
name: 'accountStickers',
builder: (context, state) => TitleShell(
state: state,
child: const StickerScreen(),
),
),
GoRoute( GoRoute(
path: '/account/personalize', path: '/account/personalize',
name: 'accountProfile', name: 'accountProfile',
@ -254,6 +246,14 @@ abstract class AppRouter {
child: const PersonalizeScreen(), child: const PersonalizeScreen(),
), ),
), ),
GoRoute(
path: '/account/preferences/notifications',
name: 'notificationPreferences',
builder: (context, state) => TitleShell(
state: state,
child: const NotificationPreferencesScreen(),
),
),
GoRoute( GoRoute(
path: '/account/view/:name', path: '/account/view/:name',
name: 'accountProfilePage', name: 'accountProfilePage',

View File

@ -45,11 +45,6 @@ class _AccountScreenState extends State<AccountScreen> {
'accountFriend'.tr, 'accountFriend'.tr,
'accountFriend', 'accountFriend',
), ),
(
const Icon(Icons.emoji_symbols),
'accountStickers'.tr,
'accountStickers',
),
]; ];
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
@ -136,6 +131,15 @@ class _AccountScreenState extends State<AccountScreen> {
AppRouter.instance.pushNamed('settings'); AppRouter.instance.pushNamed('settings');
}, },
), ),
if (auth.isAuthorized.value)
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.edit_notifications),
title: Text('notificationPreferences'.tr),
onTap: () {
AppRouter.instance.pushNamed('notificationPreferences');
},
),
const Divider(thickness: 0.3, height: 1) const Divider(thickness: 0.3, height: 1)
.paddingSymmetric(vertical: 4), .paddingSymmetric(vertical: 4),
ListTile( ListTile(

View File

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/providers/auth.dart';
class NotificationPreferencesScreen extends StatefulWidget {
const NotificationPreferencesScreen({super.key});
@override
State<NotificationPreferencesScreen> createState() =>
_NotificationPreferencesScreenState();
}
class _NotificationPreferencesScreenState
extends State<NotificationPreferencesScreen> {
bool _isBusy = true;
Map<String, bool> _config = {};
final Map<String, String> _topicMap = {
'interactive.feedback': 'notificationTopicPostFeedback'.tr,
'interactive.subscription': 'notificationTopicPostSubscription'.tr,
};
Future<void> _getPreferences() async {
setState(() => _isBusy = true);
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw UnauthorizedException();
final client = await auth.configureClient('id');
final resp = await client.get('/preferences/notifications');
if (resp.statusCode != 200 && resp.statusCode != 404) {
context.showErrorDialog(RequestException(resp));
}
if (resp.statusCode == 200) {
_config = resp.body['config']
.map((k, v) => MapEntry(k, v as bool))
.cast<String, bool>();
}
setState(() => _isBusy = false);
}
Future<void> _savePreferences() async {
setState(() => _isBusy = true);
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw UnauthorizedException();
final client = await auth.configureClient('id');
final resp = await client.put('/preferences/notifications', {
'config': _config,
});
if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp));
}
context.showSnackbar('preferencesApplied'.tr);
setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
_getPreferences();
}
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainer,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.save),
title: Text('save'.tr),
enabled: !_isBusy,
onTap: () {
_savePreferences();
},
),
Expanded(
child: ListView.builder(
itemCount: _topicMap.length,
itemBuilder: (context, index) {
final element = _topicMap.entries.elementAt(index);
return CheckboxListTile(
title: Text(element.value),
subtitle: Text(
element.key,
style: GoogleFonts.robotoMono(fontSize: 12),
),
value: _config[element.key] ?? true,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onChanged: (value) {
setState(() {
_config[element.key] = value ?? false;
});
},
);
},
),
),
],
),
);
}
}

View File

@ -8,8 +8,10 @@ import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/models/subscription.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/providers/subscription.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
@ -37,12 +39,21 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
bool _isBusy = true; bool _isBusy = true;
bool _isMakingFriend = false; bool _isMakingFriend = false;
bool _isSubscribing = false;
bool _showMature = false; bool _showMature = false;
Account? _userinfo; Account? _userinfo;
Subscription? _subscription;
List<Post> _pinnedPosts = List.empty(); List<Post> _pinnedPosts = List.empty();
int _totalUpvote = 0, _totalDownvote = 0; int _totalUpvote = 0, _totalDownvote = 0;
Future<void> _getSubscription() async {
setState(() => _isSubscribing = true);
_subscription = await Get.find<SubscriptionProvider>()
.getSubscriptionOnUser(_userinfo!.id);
setState(() => _isSubscribing = false);
}
Future<void> _getUserinfo() async { Future<void> _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -70,7 +81,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
Future<void> getPinnedPosts() async { Future<void> _getPinnedPosts() async {
final client = await ServiceFinder.configureClient('interactive'); final client = await ServiceFinder.configureClient('interactive');
final resp = await client.get('/users/${widget.name}/pin'); final resp = await client.get('/users/${widget.name}/pin');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
@ -115,8 +126,10 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
} }
}); });
_getUserinfo(); _getUserinfo().then((_) {
getPinnedPosts(); _getSubscription();
_getPinnedPosts();
});
} }
Widget _buildStatisticsEntry(String label, String content) { Widget _buildStatisticsEntry(String label, String content) {
@ -180,6 +193,40 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
], ],
), ),
), ),
if (_userinfo != null && _subscription == null)
OutlinedButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
),
onPressed: _isSubscribing
? null
: () async {
setState(() => _isSubscribing = true);
_subscription =
await Get.find<SubscriptionProvider>()
.subscribeToUser(_userinfo!.id);
setState(() => _isSubscribing = false);
},
child: Text('subscribe'.tr),
)
else if (_userinfo != null)
OutlinedButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
),
onPressed: _isSubscribing
? null
: () async {
setState(() => _isSubscribing = true);
await Get.find<SubscriptionProvider>()
.unsubscribeFromUser(_userinfo!.id);
_subscription = null;
setState(() => _isSubscribing = false);
},
child: Text('unsubscribe'.tr),
),
if (_userinfo != null && if (_userinfo != null &&
!_relationshipProvider.hasFriend(_userinfo!)) !_relationshipProvider.hasFriend(_userinfo!))
IconButton( IconButton(
@ -245,7 +292,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
RefreshIndicator( RefreshIndicator(
onRefresh: () => Future.wait([ onRefresh: () => Future.wait([
_postController.reloadAllOver(), _postController.reloadAllOver(),
getPinnedPosts(), _getPinnedPosts(),
]), ]),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
@ -302,6 +349,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
isClickable: true, isClickable: true,
isNestedClickable: true, isNestedClickable: true,
isShowEmbed: true, isShowEmbed: true,
showFeaturedReply: true,
onUpdate: () { onUpdate: () {
_postController.reloadAllOver(); _postController.reloadAllOver();
}, },
@ -352,7 +400,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
child: AttachmentListEntry( child: AttachmentListEntry(
item: item, item: item,
isDense: true, isDense: true,
parentId: 'album', parentId: 'album-$index',
showMature: _showMature, showMature: _showMature,
onReveal: (value) { onReveal: (value) {
setState(() => _showMature = value); setState(() => _showMature = value);

View File

@ -1,186 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.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/stickers.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/stickers/sticker_uploader.dart';
class StickerScreen extends StatefulWidget {
const StickerScreen({super.key});
@override
State<StickerScreen> createState() => _StickerScreenState();
}
class _StickerScreenState extends State<StickerScreen> {
final PagingController<int, StickerPack> _pagingController =
PagingController(firstPageKey: 0);
Future<bool> _promptDelete(Sticker item, String prefix) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return false;
final confirm = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('stickerDeletionConfirm'.tr),
content: Text(
'stickerDeletionConfirmCaption'.trParams({
'name': ':${'$prefix${item.alias}'.camelCase}:',
}),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('cancel'.tr),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('confirm'.tr),
),
],
),
);
if (confirm != true) return false;
final client = await auth.configureClient('files');
final resp = await client.delete('/stickers/${item.id}');
return resp.statusCode == 200;
}
Future<bool?> _promptUploadSticker({Sticker? edit}) {
return showDialog(
context: context,
builder: (context) => StickerUploadDialog(
edit: edit,
),
);
}
Widget _buildEmoteEntry(Sticker item, String prefix) {
final imageUrl = ServiceFinder.buildUrl(
'files',
'/attachments/${item.attachment.rid}',
);
return ListTile(
title: Text(item.name),
subtitle: Text(item.textWarpedPlaceholder),
contentPadding: const EdgeInsets.only(left: 16, right: 14),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit_square),
onPressed: () {
_promptUploadSticker(edit: item).then((value) {
if (value == true) _pagingController.refresh();
});
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
_promptDelete(item, prefix).then((value) {
if (value == true) _pagingController.refresh();
});
},
),
],
),
leading: AutoCacheImage(
imageUrl,
width: 28,
height: 28,
noErrorWidget: true,
),
);
}
@override
void initState() {
final AuthProvider auth = Get.find();
final name = auth.userProfile.value!['name'];
_pagingController.addPageRequestListener((pageKey) async {
final client = await ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/manifest?take=10&offset=$pageKey&author=$name',
);
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
if (out != null && result.data!.length >= 10) {
_pagingController.appendPage(out, pageKey + out.length);
} else if (out != null) {
_pagingController.appendLastPage(out);
}
} else {
_pagingController.error = resp.bodyString;
}
});
super.initState();
}
@override
void dispose() {
final StickerProvider sticker = Get.find();
sticker.refreshAvailableStickers();
_pagingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
_promptUploadSticker().then((value) {
if (value == true) _pagingController.refresh();
});
},
),
body: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView(
slivers: [
PagedSliverList<int, StickerPack>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (BuildContext context, item, int index) {
return ExpansionTile(
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(item.name),
const Gap(6),
Badge(
label: Text('#${item.id}'),
)
],
),
subtitle: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
children: item.stickers?.map((x) {
x.pack = item;
return _buildEmoteEntry(x, item.prefix);
}).toList() ??
List.empty(),
);
},
),
),
],
),
),
);
}
}

View File

@ -7,6 +7,9 @@ import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/auth.dart'; import 'package:solian/models/auth.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
@ -164,6 +167,7 @@ class _SignInScreenState extends State<SignInScreen> {
final result = AuthResult.fromJson(resp.body); final result = AuthResult.fromJson(resp.body);
_currentTicket = result.ticket; _currentTicket = result.ticket;
_passwordController.clear();
// Finish sign in if possible // Finish sign in if possible
if (result.isFinished) { if (result.isFinished) {
@ -173,6 +177,9 @@ class _SignInScreenState extends State<SignInScreen> {
await auth.refreshAuthorizeStatus(); await auth.refreshAuthorizeStatus();
await auth.refreshUserProfile(); await auth.refreshUserProfile();
Get.find<ChannelProvider>().refreshAvailableChannel();
Get.find<RealmProvider>().refreshAvailableRealms();
Get.find<RelationshipProvider>().refreshRelativeList();
Get.find<WebSocketProvider>().registerPushNotifications(); Get.find<WebSocketProvider>().registerPushNotifications();
autoConfigureBackgroundNotificationService(); autoConfigureBackgroundNotificationService();
autoStartBackgroundNotificationService(); autoStartBackgroundNotificationService();

View File

@ -114,7 +114,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: Text('channelSettings'.tr.capitalize!), title: Text('channelSettings'.tr),
onTap: () async { onTap: () async {
AppRouter.instance AppRouter.instance
.pushNamed( .pushNamed(
@ -173,7 +173,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.notifications_active), leading: const Icon(Icons.notifications_active),
title: Text('channelNotifyLevel'.tr.capitalize!), title: Text('channelNotifyLevel'.tr),
trailing: DropdownButtonHideUnderline( trailing: DropdownButtonHideUnderline(
child: DropdownButton2<int>( child: DropdownButton2<int>(
isExpanded: true, isExpanded: true,
@ -208,7 +208,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
ListTile( ListTile(
leading: const Icon(Icons.supervisor_account), leading: const Icon(Icons.supervisor_account),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: Text('channelMembers'.tr.capitalize!), title: Text('channelMembers'.tr),
onTap: () => showMemberList(), onTap: () => showMemberList(),
), ),
...(_isOwned ? ownerActions : List.empty()), ...(_isOwned ? ownerActions : List.empty()),

View File

@ -35,13 +35,14 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
final _nameController = TextEditingController(); final _nameController = TextEditingController();
final _descriptionController = TextEditingController(); final _descriptionController = TextEditingController();
bool _isEncrypted = false; bool _isPublic = false;
bool _isCommunity = false;
void applyChannel() async { void _applyChannel() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
if (_aliasController.value.text.isEmpty) randomizeAlias(); if (_aliasController.value.text.isEmpty) _randomizeAlias();
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -52,7 +53,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
'alias': _aliasController.value.text.toLowerCase(), 'alias': _aliasController.value.text.toLowerCase(),
'name': _nameController.value.text, 'name': _nameController.value.text,
'description': _descriptionController.value.text, 'description': _descriptionController.value.text,
'is_encrypted': _isEncrypted, 'is_encrypted': _isPublic,
}; };
Response? resp; Response? resp;
@ -71,35 +72,44 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void randomizeAlias() { void _randomizeAlias() {
_aliasController.text = _aliasController.text =
const Uuid().v4().replaceAll('-', '').substring(0, 12); const Uuid().v4().replaceAll('-', '').substring(0, 12);
} }
void syncWidget() { void _syncWidget() {
if (widget.edit != null) { if (widget.edit != null) {
_aliasController.text = widget.edit!.alias; _aliasController.text = widget.edit!.alias;
_nameController.text = widget.edit!.name; _nameController.text = widget.edit!.name;
_descriptionController.text = widget.edit!.description; _descriptionController.text = widget.edit!.description;
_isEncrypted = widget.edit!.isEncrypted; _isPublic = widget.edit!.isPublic;
_isCommunity = widget.edit!.isCommunity;
} }
} }
void cancelAction() { void _cancelAction() {
AppRouter.instance.pop(); AppRouter.instance.pop();
} }
@override @override
void initState() { void initState() {
syncWidget(); _syncWidget();
super.initState(); super.initState();
} }
@override
void dispose() {
_aliasController.dispose();
_nameController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final notifyBannerActions = [ final notifyBannerActions = [
TextButton( TextButton(
onPressed: cancelAction, onPressed: _cancelAction,
child: Text('cancel'.tr), child: Text('cancel'.tr),
), ),
]; ];
@ -113,7 +123,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
toolbarHeight: AppTheme.toolbarHeight(context), toolbarHeight: AppTheme.toolbarHeight(context),
actions: [ actions: [
TextButton( TextButton(
onPressed: _isBusy ? null : () => applyChannel(), onPressed: _isBusy ? null : () => _applyChannel(),
child: Text('apply'.tr.toUpperCase()), child: Text('apply'.tr.toUpperCase()),
) )
], ],
@ -164,7 +174,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
visualDensity: visualDensity:
const VisualDensity(horizontal: -2, vertical: -2), const VisualDensity(horizontal: -2, vertical: -2),
), ),
onPressed: () => randomizeAlias(), onPressed: () => _randomizeAlias(),
child: const Icon(Icons.refresh), child: const Icon(Icons.refresh),
) )
], ],
@ -196,12 +206,17 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
), ),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
CheckboxListTile( CheckboxListTile(
title: Text('channelEncrypted'.tr), title: Text('channelPublic'.tr),
value: _isEncrypted, value: _isPublic,
onChanged: (widget.edit?.isEncrypted ?? false) onChanged: (value) =>
? null setState(() => _isPublic = value ?? false),
: (newValue) => controlAffinity: ListTileControlAffinity.leading,
setState(() => _isEncrypted = newValue ?? false), ),
CheckboxListTile(
title: Text('channelCommunity'.tr),
value: _isCommunity,
onChanged: (value) =>
setState(() => _isCommunity = value ?? false),
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
), ),
], ],

View File

@ -125,7 +125,11 @@ class _ChatScreenState extends State<ChatScreen> {
child: Obx( child: Obx(
() => ChannelListWidget( () => ChannelListWidget(
noCategory: true, noCategory: true,
channels: _channels.directChannels, channels: List.from([
..._channels.groupChannels
.where((x) => x.realmId == null),
..._channels.directChannels
]),
selfId: selfId, selfId: selfId,
useReplace: true, useReplace: true,
), ),

View File

@ -379,6 +379,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
isClickable: true, isClickable: true,
isShowEmbed: true, isShowEmbed: true,
isNestedClickable: true, isNestedClickable: true,
showFeaturedReply: true,
onUpdate: (_) { onUpdate: (_) {
_pullPosts(); _pullPosts();
}, },

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/last_read.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/posts/post_replies.dart'; import 'package:solian/widgets/posts/post_replies.dart';
@ -26,6 +27,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
Future<Post?> getDetail() async { Future<Post?> getDetail() async {
if (widget.post != null) { if (widget.post != null) {
item = widget.post; item = widget.post;
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
return widget.post; return widget.post;
} }
@ -38,6 +40,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
context.showErrorDialog(e).then((_) => Navigator.pop(context)); context.showErrorDialog(e).then((_) => Navigator.pop(context));
} }
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
return item; return item;
} }

View File

@ -7,10 +7,13 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/services.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
@ -128,19 +131,34 @@ class _RealmListScreenState extends State<RealmListScreen> {
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: (element.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoCacheImage(
ServiceFinder.buildUrl(
'uc',
'/attachments/${element.banner}',
),
fit: BoxFit.cover,
),
), ),
const Positioned( Positioned(
bottom: -30, bottom: -30,
left: 18, left: 18,
child: CircleAvatar( child: (element.avatar?.isEmpty ?? true)
radius: 24, ? CircleAvatar(
backgroundColor: Colors.indigo, radius: 24,
child: FaIcon( backgroundColor:
FontAwesomeIcons.globe, Theme.of(context).colorScheme.primary,
color: Colors.white, child: const FaIcon(
size: 18, FontAwesomeIcons.globe,
), color: Colors.white,
), size: 18,
),
)
: AccountAvatar(
content: element.avatar!,
bgColor: Theme.of(context).colorScheme.primary,
),
), ),
], ],
), ),

View File

@ -69,7 +69,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: Text('realmSettings'.tr.capitalize!), title: Text('realmSettings'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () async { onTap: () async {
AppRouter.instance AppRouter.instance
.pushNamed( .pushNamed(
@ -120,14 +121,16 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
child: ListView( child: ListView(
children: [ children: [
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.supervisor_account), leading: const Icon(Icons.supervisor_account),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: Text('realmMembers'.tr.capitalize!), title: Text('realmMembers'.tr),
onTap: () => showMemberList(), onTap: () => showMemberList(),
), ),
...(_isOwned ? ownerActions : List.empty()), ...(_isOwned ? ownerActions : List.empty()),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: _isOwned leading: _isOwned
? const Icon(Icons.delete) ? const Icon(Icons.delete)
: const Icon(Icons.exit_to_app), : const Icon(Icons.exit_to_app),

View File

@ -1,9 +1,15 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
@ -29,17 +35,19 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
bool _isBusy = false; bool _isBusy = false;
final _aliasController = TextEditingController(); final _aliasController = TextEditingController();
final _avatarController = TextEditingController();
final _bannerController = TextEditingController();
final _nameController = TextEditingController(); final _nameController = TextEditingController();
final _descriptionController = TextEditingController(); final _descriptionController = TextEditingController();
bool _isCommunity = false; bool _isCommunity = false;
bool _isPublic = false; bool _isPublic = false;
void applyRealm() async { void _applyRealm() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
if (_aliasController.value.text.isEmpty) randomizeAlias(); if (_aliasController.value.text.isEmpty) _randomizeAlias();
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -49,6 +57,8 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
'alias': _aliasController.value.text.toLowerCase(), 'alias': _aliasController.value.text.toLowerCase(),
'name': _nameController.value.text, 'name': _nameController.value.text,
'description': _descriptionController.value.text, 'description': _descriptionController.value.text,
'avatar': _avatarController.value.text,
'banner': _bannerController.value.text,
'is_public': _isPublic, 'is_public': _isPublic,
'is_community': _isCommunity, 'is_community': _isCommunity,
}; };
@ -68,31 +78,110 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void randomizeAlias() { final _imagePicker = ImagePicker();
Future<void> _editImage(String position) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return;
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: image.path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'cropImage'.tr,
toolbarColor: Theme.of(context).colorScheme.primary,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
),
IOSUiSettings(
title: 'cropImage'.tr,
aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
),
WebUiSettings(
context: context,
),
],
);
if (croppedFile == null) return;
final file = File(croppedFile.path);
setState(() => _isBusy = true);
final AttachmentProvider attach = Get.find();
Attachment? attachResult;
try {
attachResult = await attach.createAttachmentDirectly(
await file.readAsBytes(),
file.path,
'avatar',
null,
);
} catch (e) {
setState(() => _isBusy = false);
context.showErrorDialog(e);
return;
}
switch (position) {
case 'avatar':
_avatarController.text = attachResult.rid;
break;
case 'banner':
_bannerController.text = attachResult.rid;
break;
}
setState(() => _isBusy = false);
}
void _randomizeAlias() {
_aliasController.text = _aliasController.text =
const Uuid().v4().replaceAll('-', '').substring(0, 12); const Uuid().v4().replaceAll('-', '').substring(0, 12);
} }
void syncWidget() { void _syncWidget() {
if (widget.edit != null) { if (widget.edit != null) {
_aliasController.text = widget.edit!.alias; _aliasController.text = widget.edit!.alias;
_nameController.text = widget.edit!.name; _nameController.text = widget.edit!.name;
_descriptionController.text = widget.edit!.description; _descriptionController.text = widget.edit!.description;
_avatarController.text = widget.edit!.avatar ?? '';
_bannerController.text = widget.edit!.banner ?? '';
_isPublic = widget.edit!.isPublic; _isPublic = widget.edit!.isPublic;
_isCommunity = widget.edit!.isCommunity; _isCommunity = widget.edit!.isCommunity;
} }
} }
void cancelAction() { void _cancelAction() {
AppRouter.instance.pop(); AppRouter.instance.pop();
} }
@override @override
void initState() { void initState() {
syncWidget(); _syncWidget();
super.initState(); super.initState();
} }
@override
void dispose() {
_aliasController.dispose();
_avatarController.dispose();
_bannerController.dispose();
_nameController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
@ -105,7 +194,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
toolbarHeight: AppTheme.toolbarHeight(context), toolbarHeight: AppTheme.toolbarHeight(context),
actions: [ actions: [
TextButton( TextButton(
onPressed: _isBusy ? null : () => applyRealm(), onPressed: _isBusy ? null : () => _applyRealm(),
child: Text('apply'.tr.toUpperCase()), child: Text('apply'.tr.toUpperCase()),
) )
], ],
@ -126,7 +215,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: cancelAction, onPressed: _cancelAction,
child: Text('cancel'.tr), child: Text('cancel'.tr),
), ),
], ],
@ -150,7 +239,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
visualDensity: visualDensity:
const VisualDensity(horizontal: -2, vertical: -2), const VisualDensity(horizontal: -2, vertical: -2),
), ),
onPressed: () => randomizeAlias(), onPressed: () => _randomizeAlias(),
child: const Icon(Icons.refresh), child: const Icon(Icons.refresh),
) )
], ],
@ -166,6 +255,55 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
).paddingSymmetric(horizontal: 16, vertical: 8), ).paddingSymmetric(horizontal: 16, vertical: 8),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
Row(
children: [
Expanded(
child: TextField(
autofocus: true,
controller: _avatarController,
decoration: InputDecoration.collapsed(
hintText: 'realmAvatar'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
TextButton(
style: TextButton.styleFrom(
shape: const CircleBorder(),
visualDensity:
const VisualDensity(horizontal: -2, vertical: -2),
),
onPressed: _isBusy ? null : () => _editImage('avatar'),
child: const Icon(Icons.upload),
)
],
).paddingSymmetric(horizontal: 16, vertical: 2),
Row(
children: [
Expanded(
child: TextField(
autofocus: true,
controller: _bannerController,
decoration: InputDecoration.collapsed(
hintText: 'realmBanner'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
TextButton(
style: TextButton.styleFrom(
shape: const CircleBorder(),
visualDensity:
const VisualDensity(horizontal: -2, vertical: -2),
),
onPressed: _isBusy ? null : () => _editImage('banner'),
child: const Icon(Icons.upload),
)
],
).paddingSymmetric(horizontal: 16, vertical: 2),
const Divider(thickness: 0.3),
Expanded( Expanded(
child: TextField( child: TextField(
minLines: 5, minLines: 5,
@ -202,3 +340,11 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
); );
} }
} }
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
@override
(int, int)? get data => (16, 7);
@override
String get name => '16x7';
}

View File

@ -93,14 +93,14 @@ class _AttachmentEditorThumbnailDialogState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text('postThumbnail'.tr), title: Text('attachmentThumbnail'.tr),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Card( Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: ListTile( child: ListTile(
title: Text('postThumbnailAttachmentNew'.tr), title: Text('attachmentThumbnailAttachmentNew'.tr),
contentPadding: const EdgeInsets.only(left: 12, right: 9), contentPadding: const EdgeInsets.only(left: 12, right: 9),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
@ -122,7 +122,7 @@ class _AttachmentEditorThumbnailDialogState
isDense: true, isDense: true,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
prefixText: '#', prefixText: '#',
labelText: 'postThumbnailAttachment'.tr, labelText: 'attachmentThumbnailAttachment'.tr,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),

View File

@ -282,6 +282,8 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
children: [ children: [
Text( Text(
widget.item.alt, widget.item.alt,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
shadows: labelShadows, shadows: labelShadows,
color: Colors.white, color: Colors.white,
@ -447,6 +449,8 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
children: [ children: [
Text( Text(
widget.item.alt, widget.item.alt,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
shadows: labelShadows, shadows: labelShadows,
color: Colors.white, color: Colors.white,

View File

@ -1,5 +1,8 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/controllers/chat_events_controller.dart'; import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
@ -213,6 +216,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
return _buildEntry(element); return _buildEntry(element);
}, },
), ),
SliverGap(max(16, MediaQuery.of(context).padding.bottom)),
], ],
); );
} }

View File

@ -405,12 +405,9 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
if (emojiMatch != null) { if (emojiMatch != null) {
final StickerProvider stickers = Get.find(); final StickerProvider stickers = Get.find();
final emoteSearch = emojiMatch[2]!; final emoteSearch = emojiMatch[2]!;
return stickers.availableStickers final result = await stickers
.where( .searchStickerByAlias(emoteSearch.substring(1));
(x) => x.textWarpedPlaceholder return result
.toUpperCase()
.contains(emoteSearch.toUpperCase()),
)
.map( .map(
(x) => ChatMessageSuggestion( (x) => ChatMessageSuggestion(
type: 'emotes', type: 'emotes',

View File

@ -89,7 +89,6 @@ class MarkdownTextContent extends StatelessWidget {
case 'stickers': case 'stickers':
double radius = 8; double radius = 8;
final StickerProvider sticker = Get.find(); final StickerProvider sticker = Get.find();
url = sticker.aliasImageMapping[segments[1].toUpperCase()]!;
if (emojiMatch.length <= 1 && isOnlyEmoji) { if (emojiMatch.length <= 1 && isOnlyEmoji) {
width = 128; width = 128;
height = 128; height = 128;
@ -106,11 +105,20 @@ class MarkdownTextContent extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(radius)), borderRadius: BorderRadius.all(Radius.circular(radius)),
child: Container( child: Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: AutoCacheImage( child: FutureBuilder(
url, future: sticker.getStickerByAlias(segments[1]),
width: width, builder: (context, snapshot) {
height: height, if (!snapshot.hasData) {
fit: fit, return const Center(child: CircularProgressIndicator());
}
return AutoCacheImage(
snapshot.data!.imageUrl,
width: width,
height: height,
fit: fit,
noErrorWidget: true,
);
},
), ),
), ),
).paddingSymmetric(vertical: 4); ).paddingSymmetric(vertical: 4);
@ -172,7 +180,8 @@ class _CustomEmoteInlineSyntax extends InlineSyntax {
bool onMatch(markdown.InlineParser parser, Match match) { bool onMatch(markdown.InlineParser parser, Match match) {
final StickerProvider sticker = Get.find(); final StickerProvider sticker = Get.find();
final alias = match[1]!.toUpperCase(); final alias = match[1]!.toUpperCase();
if (sticker.aliasImageMapping[alias] == null) { if (sticker.stickerCache.containsKey(alias) &&
sticker.stickerCache[alias] == null) {
parser.advanceBy(1); parser.advanceBy(1);
return false; return false;
} }

View File

@ -6,7 +6,9 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/navigation.dart'; import 'package:solian/providers/navigation.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/channel/channel_list.dart'; import 'package:solian/widgets/channel/channel_list.dart';
class AppNavigationRegion extends StatefulWidget { class AppNavigationRegion extends StatefulWidget {
@ -169,6 +171,19 @@ class _AppNavigationRegionState extends State<AppNavigationRegion> {
) )
: Column( : Column(
children: [ children: [
if (!widget.isCollapsed &&
(navState.focusedRealm.value!.banner?.isNotEmpty ??
false))
AspectRatio(
aspectRatio: 16 / 7,
child: AutoCacheImage(
ServiceFinder.buildUrl(
'uc',
'/attachments/${navState.focusedRealm.value!.banner}',
),
fit: BoxFit.cover,
),
),
if (widget.isCollapsed) if (widget.isCollapsed)
Tooltip( Tooltip(
message: navState.focusedRealm.value!.name, message: navState.focusedRealm.value!.name,

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/controllers/post_editor_controller.dart'; import 'package:solian/controllers/post_editor_controller.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart'; import 'package:solian/widgets/attachments/attachment_editor.dart';
@ -58,18 +57,25 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile( Card(
title: Text('postThumbnailAttachmentNew'.tr), margin: EdgeInsets.zero,
contentPadding: const EdgeInsets.only(left: 16, right: 13), child: ListTile(
trailing: const Icon(Icons.chevron_right), title: Text('postThumbnailAttachmentNew'.tr),
shape: const RoundedRectangleBorder( contentPadding: const EdgeInsets.only(left: 12, right: 9),
borderRadius: BorderRadius.all(Radius.circular(8)), trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
onTap: () {
_promptUploadNewAttachment();
},
), ),
onTap: () {
_promptUploadNewAttachment();
},
), ),
const Gap(8), const Row(children: <Widget>[
Expanded(child: Divider()),
Text('OR'),
Expanded(child: Divider()),
]).paddingOnly(top: 12, bottom: 16, left: 16, right: 16),
TextField( TextField(
controller: _attachmentController, controller: _attachmentController,
decoration: InputDecoration( decoration: InputDecoration(

View File

@ -1,11 +1,13 @@
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get_utils/get_utils.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/shells/title_shell.dart'; import 'package:solian/shells/title_shell.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
@ -30,6 +32,7 @@ class PostItem extends StatefulWidget {
final bool isFullDate; final bool isFullDate;
final bool isFullContent; final bool isFullContent;
final bool isContentSelectable; final bool isContentSelectable;
final bool showFeaturedReply;
final String? attachmentParent; final String? attachmentParent;
final Color? backgroundColor; final Color? backgroundColor;
@ -45,6 +48,7 @@ class PostItem extends StatefulWidget {
this.isFullDate = false, this.isFullDate = false,
this.isFullContent = false, this.isFullContent = false,
this.isContentSelectable = false, this.isContentSelectable = false,
this.showFeaturedReply = false,
this.attachmentParent, this.attachmentParent,
this.backgroundColor, this.backgroundColor,
}); });
@ -103,7 +107,7 @@ class _PostItemState extends State<PostItem> {
children: [ children: [
if (widget.isCompact) if (widget.isCompact)
AccountAvatar( AccountAvatar(
content: item.author.avatar.toString(), content: item.author.avatar,
radius: 10, radius: 10,
).paddingOnly(left: 2, top: 1), ).paddingOnly(left: 2, top: 1),
Expanded( Expanded(
@ -320,6 +324,102 @@ class _PostItemState extends State<PostItem> {
} }
} }
Widget _buildFeaturedReply() {
if ((widget.item.metric?.replyCount ?? 0) == 0) {
return const SizedBox.shrink();
}
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
return FutureBuilder(
future: Get.find<PostProvider>().listPostFeaturedReply(
widget.item.id.toString(),
),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const SizedBox.shrink();
}
return Card(
margin: EdgeInsets.zero,
child: Column(
children: snapshot.data!
.map(
(x) => Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountAvatar(content: x.author.avatar, radius: 10),
const Gap(6),
Text(
x.author.nick,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const Gap(6),
Text(
format(
x.publishedAt?.toLocal() ?? DateTime.now(),
locale: 'en_short',
),
).paddingOnly(top: 0.5),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownTextContent(
content: x.body['content'],
parentId: 'p${item.id}-featured-reply${x.id}',
),
if (x.body['attachments'] is List &&
x.body['attachments'].length > 0)
Row(
children: [
Icon(
Icons.file_copy,
size: 15,
color: unFocusColor,
).paddingOnly(right: 5),
Text(
'attachmentHint'.trParams(
{
'count': x.body['attachments'].length
.toString()
},
),
style: TextStyle(color: unFocusColor),
)
],
),
],
),
),
],
).paddingSymmetric(horizontal: 12, vertical: 8),
)
.toList(),
),
)
.animate()
.fadeIn(
duration: 300.ms,
curve: Curves.easeIn,
)
.paddingOnly(
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 10
: 6,
left:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 24
: 60,
right: 16,
);
},
);
}
double _contentHeight = 0; double _contentHeight = 0;
@override @override
@ -417,7 +517,7 @@ class _PostItemState extends State<PostItem> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( GestureDetector(
child: AccountAvatar(content: item.author.avatar.toString()), child: AccountAvatar(content: item.author.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
@ -506,6 +606,7 @@ class _PostItemState extends State<PostItem> {
left: 16, left: 16,
), ),
_buildAttachments(), _buildAttachments(),
if (widget.showFeaturedReply) _buildFeaturedReply(),
if (widget.isShowReply || widget.isReactable) if (widget.isShowReply || widget.isReactable)
PostQuickAction( PostQuickAction(
isShowReply: widget.isShowReply, isShowReply: widget.isShowReply,

View File

@ -33,6 +33,7 @@ class PostListWidget extends StatelessWidget {
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable, isNestedClickable: isNestedClickable,
isClickable: isClickable, isClickable: isClickable,
showFeaturedReply: true,
item: item, item: item,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
onUpdate: () { onUpdate: () {
@ -51,6 +52,7 @@ class PostListEntryWidget extends StatelessWidget {
final bool isShowEmbed; final bool isShowEmbed;
final bool isNestedClickable; final bool isNestedClickable;
final bool isClickable; final bool isClickable;
final bool showFeaturedReply;
final Post item; final Post item;
final Function onUpdate; final Function onUpdate;
final Color? backgroundColor; final Color? backgroundColor;
@ -61,6 +63,7 @@ class PostListEntryWidget extends StatelessWidget {
required this.isShowEmbed, required this.isShowEmbed,
required this.isNestedClickable, required this.isNestedClickable,
required this.isClickable, required this.isClickable,
required this.showFeaturedReply,
required this.item, required this.item,
required this.onUpdate, required this.onUpdate,
this.backgroundColor, this.backgroundColor,
@ -74,6 +77,7 @@ class PostListEntryWidget extends StatelessWidget {
item: item, item: item,
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isClickable: isNestedClickable, isClickable: isNestedClickable,
showFeaturedReply: showFeaturedReply,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
).paddingSymmetric(vertical: 8), ).paddingSymmetric(vertical: 8),
onLongPress: () { onLongPress: () {

View File

@ -23,6 +23,7 @@ class PostSingleDisplay extends StatelessWidget {
isClickable: true, isClickable: true,
isShowEmbed: true, isShowEmbed: true,
isNestedClickable: true, isNestedClickable: true,
showFeaturedReply: true,
onUpdate: onUpdate, onUpdate: onUpdate,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
), ),

View File

@ -36,6 +36,7 @@ class PostWarpedListWidget extends StatelessWidget {
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable, isNestedClickable: isNestedClickable,
isClickable: isClickable, isClickable: isClickable,
showFeaturedReply: true,
item: item, item: item,
onUpdate: onUpdate ?? () {}, onUpdate: onUpdate ?? () {},
); );

View File

@ -1,212 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/stickers.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart';
class StickerUploadDialog extends StatefulWidget {
final Sticker? edit;
const StickerUploadDialog({super.key, this.edit});
@override
State<StickerUploadDialog> createState() => _StickerUploadDialogState();
}
class _StickerUploadDialogState extends State<StickerUploadDialog> {
final TextEditingController _attachmentController = TextEditingController();
final TextEditingController _packController = TextEditingController();
final TextEditingController _aliasController = TextEditingController();
final TextEditingController _nameController = TextEditingController();
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
bool _isBusy = false;
void _promptUploadNewAttachment() {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditorPopup(
pool: 'sticker',
singleMode: true,
imageOnly: true,
autoUpload: true,
imageMaxHeight: 28,
imageMaxWidth: 28,
onAdd: (value) {
setState(() {
_attachmentController.text = value.toString();
});
},
initialAttachments: const [],
onRemove: (_) {},
),
);
}
Future<void> _applySticker() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
if ([
_nameController.text.isEmpty,
_aliasController.text.isEmpty,
_packController.text.isEmpty,
_attachmentController.text.isEmpty,
].any((x) => x)) {
return;
}
setState(() => _isBusy = true);
Response resp;
final client = await auth.configureClient('files');
if (widget.edit == null) {
resp = await client.post('/stickers', {
'name': _nameController.text,
'alias': _aliasController.text,
'pack_id': int.tryParse(_packController.text),
'attachment_id': int.tryParse(_attachmentController.text),
});
} else {
resp = await client.put('/stickers/${widget.edit!.id}', {
'name': _nameController.text,
'alias': _aliasController.text,
'pack_id': int.tryParse(_packController.text),
'attachment_id': int.tryParse(_attachmentController.text),
});
}
setState(() => _isBusy = false);
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString);
} else {
Navigator.pop(context, true);
}
}
@override
void initState() {
super.initState();
if (widget.edit != null) {
_attachmentController.text = widget.edit!.attachmentId.toString();
_packController.text = widget.edit!.packId.toString();
_aliasController.text = widget.edit!.alias;
_nameController.text = widget.edit!.name;
}
}
@override
void dispose() {
_attachmentController.dispose();
_packController.dispose();
_aliasController.dispose();
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('stickerUploader'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text('stickerUploaderAttachmentNew'.tr),
contentPadding: const EdgeInsets.only(left: 16, right: 13),
trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
onTap: () {
_promptUploadNewAttachment();
},
),
const Gap(8),
TextField(
controller: _attachmentController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
prefixText: '#',
labelText: 'stickerUploaderAttachment'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(8),
TextField(
controller: _packController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
prefixText: '#',
labelText: 'stickerUploaderPack'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Container(
padding:
const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 6),
child: Text(
'stickerUploaderPackHint'.tr,
style: TextStyle(color: _unFocusColor),
),
),
TextField(
controller: _aliasController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'stickerUploaderAlias'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Container(
padding:
const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 6),
child: Text(
'stickerUploaderAliasHint'.tr,
style: TextStyle(color: _unFocusColor),
),
),
TextField(
controller: _nameController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'stickerUploaderName'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Container(
padding: const EdgeInsets.only(left: 8, right: 8, top: 4),
child: Text(
'stickerUploaderNameHint'.tr,
style: TextStyle(color: _unFocusColor),
),
),
],
),
actions: [
TextButton(
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
onPressed: _isBusy ? null : () => Navigator.pop(context),
child: Text('cancel'.tr),
),
TextButton(
onPressed: _isBusy ? null : () => _applySticker(),
child: Text('apply'.tr),
),
],
);
}
}

View File

@ -60,9 +60,9 @@ PODS:
- FirebaseCoreInternal (~> 11.0) - FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.1.0): - FirebaseCoreExtension (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreInternal (11.1.0): - FirebaseCoreInternal (11.2.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseCrashlytics (11.0.0): - FirebaseCrashlytics (11.0.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
@ -73,7 +73,7 @@ PODS:
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseInstallations (11.1.0): - FirebaseInstallations (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
@ -87,8 +87,8 @@ PODS:
- GoogleUtilities/Reachability (~> 8.0) - GoogleUtilities/Reachability (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseRemoteConfigInterop (11.1.0) - FirebaseRemoteConfigInterop (11.2.0)
- FirebaseSessions (11.1.0): - FirebaseSessions (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreExtension (~> 11.0) - FirebaseCoreExtension (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
@ -344,13 +344,13 @@ SPEC CHECKSUMS:
firebase_messaging: ce70e6615f0cd906d80b7a651b960d76dad6de56 firebase_messaging: ce70e6615f0cd906d80b7a651b960d76dad6de56
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383 FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa FirebaseCoreExtension: cda74ddfb001224bd8fd1d6e74698b4ed07803de
FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c FirebaseCoreInternal: 0c569513412da9f3b31bd0b340013bbee8f295c5
FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57 FirebaseInstallations: 771177d89d6c451dc6e50085ec82e2fc77ed0a4a
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742 FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87 FirebaseRemoteConfigInterop: 477b26fdeb8fb5fbaf22fa9db5343b42289dc7db
FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a FirebaseSessions: adcec8b72d0066a385e3affcd1bcb1ebb3908ce6
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9 flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9

View File

@ -701,6 +701,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
@ -843,6 +844,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
@ -870,6 +872,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";

View File

@ -909,14 +909,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.7.0" version: "10.7.0"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -1894,10 +1886,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.4+2" version: "2.5.4+3"
sqlite3: sqlite3:
dependency: transitive dependency: transitive
description: description:

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.1+39 version: 1.2.1+40
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@ -68,7 +68,6 @@ dependencies:
flutter_svg: ^2.0.10+1 flutter_svg: ^2.0.10+1
cross_file: ^0.3.4+2 cross_file: ^0.3.4+2
google_fonts: ^6.2.1 google_fonts: ^6.2.1
freezed_annotation: ^2.4.4
json_annotation: ^4.9.0 json_annotation: ^4.9.0
gap: ^3.0.1 gap: ^3.0.1
fl_chart: ^0.69.0 fl_chart: ^0.69.0