Compare commits

...

16 Commits

Author SHA1 Message Date
f3375070a0 🚀 Launch 1.2.1+37 2024-09-15 17:46:48 +08:00
204df3306e 🐛 Fix notification services 2024-09-15 17:19:55 +08:00
aeaade9590 🐛 Fix unauthorized things 2024-09-15 16:54:07 +08:00
306ce9e2b4 Optimize notification background service 2024-09-15 16:02:56 +08:00
a487924300 Android background notification service 2024-09-15 15:55:14 +08:00
ad66c11593 ♻️ Implement delete (recreate) local database 2024-09-15 12:25:50 +08:00
40b885b27b 🔀 Merge pull request '♻️ 使用 Drift 作为本地数据库' (#3) from refactor/drift-as-local-db into master
Reviewed-on: #3
2024-09-15 02:56:59 +00:00
2183a2ca55 Improve loading of chat events 2024-09-15 10:55:27 +08:00
00449f3f83 🐛 Fix Too many elements 2024-09-14 00:42:17 +08:00
b14e55355f ♻️ Use drift instead for floor 2024-09-14 00:30:33 +08:00
db808650e3 💄 Optimized settings 2024-09-13 23:19:33 +08:00
c1cbcbe734 ⬆️ Upgrade flutter + deps 2024-09-13 22:51:17 +08:00
2c4040096f 🚀 Launch 1.2.1+36 2024-09-13 20:22:33 +08:00
b449735bf5 Better side navigation
🐛 Bug fixes and optimizations
2024-09-13 20:22:10 +08:00
dd01f964d4 Focused realm linked with feed stream 2024-09-13 00:17:56 +08:00
6daa04c208 Brand new app navigation region 2024-09-12 23:55:31 +08:00
94 changed files with 1926 additions and 1210 deletions

View File

@ -3,6 +3,7 @@
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
@ -51,6 +52,14 @@
to determine the Window background behind the Flutter UI. -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="solink" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@ -61,14 +70,6 @@
<data android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="solink" />
</intent-filter>
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
@ -84,6 +85,11 @@
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<service
android:name="id.flutter.flutter_background_service.BackgroundService"
android:foregroundServiceType="remoteMessaging"
/>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -54,6 +54,8 @@
"edit": "Edit",
"delete": "Delete",
"settings": "Settings",
"settingsNotificationBgService": "Background Notification Service",
"settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.",
"search": "Search",
"post": "Post",
"article": "Article",
@ -76,8 +78,8 @@
"firstName": "First Name",
"lastName": "Last Name",
"account": "Account",
"accountPersonalize": "Personalize",
"accountPersonalizeApplied": "Account personalize settings has been saved.",
"accountProfile": "Your profile",
"accountProfileApplied": "Account profile has been saved.",
"accountStickers": "Stickers",
"accountFriend": "Friend",
"accountFriendNew": "New friend",
@ -373,7 +375,8 @@
"callStatusReconnected": "Reconnecting",
"messageOutOfSync": "May Out of Sync with Server",
"messageOutOfSyncCaption": "Since the App has entered the background, there may be a time difference between the message list and the server. Click to Refresh.",
"messageHistoryWipe": "Wipe local message history",
"localDatabaseWipe": "Wipe local database",
"localDatabaseSize": "Overall database size: @size",
"unknown": "Unknown",
"collapse": "Collapse",
"expand": "Expand",
@ -391,5 +394,8 @@
"userLevel10": "Grandmaster",
"userLevel11": "Legend",
"userLevel12": "Mythic",
"userLevel13": "Immortal"
"userLevel13": "Immortal",
"postBrowsingIn": "Browsing in @region",
"needRestartToApply": "Restart the application to take effect",
"holdToSeeDetail": "Long press / Mouse hover to see detail"
}

View File

@ -14,6 +14,8 @@
"edit": "编辑",
"delete": "删除",
"settings": "设置",
"settingsNotificationBgService": "常驻通知服务",
"settingsNotificationBgServiceDesc": "在设备常驻一个通知服务,使得部分不支持推送通知的设备可以在后台收到通知;启用该功能的情况下不会向服务器注册推送通知,并且你会始终在他人眼中成为在线(隐身除外);可能需要在设置中关闭电量与流量优化。",
"page": "页面",
"draft": "草稿",
"draftSave": "存为草稿",
@ -81,8 +83,8 @@
"firstName": "名称",
"lastName": "姓氏",
"account": "账号",
"accountPersonalize": "个性化",
"accountPersonalizeApplied": "账户的个性化设置已保存。",
"accountProfile": "个人资料",
"accountProfileApplied": "账户的资料已保存。",
"accountStickers": "贴图",
"accountFriend": "好友",
"accountFriendNew": "添加好友",
@ -374,7 +376,8 @@
"callStatusReconnected": "重连中",
"messageOutOfSync": "消息可能与服务器脱节",
"messageOutOfSyncCaption": "由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。",
"messageHistoryWipe": "清除消息记录",
"localDatabaseWipe": "清除本地数据库",
"localDatabaseSize": "本地数据库大小:@size",
"unknown": "未知",
"collapse": "折叠",
"expand": "展开",
@ -392,5 +395,8 @@
"userLevel10": "出神入化",
"userLevel11": "名垂千古",
"userLevel12": "独占鳌头",
"userLevel13": "万古流芳"
"userLevel13": "万古流芳",
"postBrowsingIn": "浏览 @region 内的帖子中",
"needRestartToApply": "需要重启应用来生效",
"holdToSeeDetail": "长按 / 鼠标悬浮来查看详情"
}

View File

@ -1,4 +1,5 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
- provider: true
- provider: true
- drift: true

View File

@ -54,22 +54,22 @@ PODS:
- Firebase/Performance (11.0.0):
- Firebase/CoreOnly
- FirebasePerformance (~> 11.0.0)
- firebase_analytics (11.3.0):
- firebase_analytics (11.3.1):
- Firebase/Analytics (= 11.0.0)
- firebase_core
- Flutter
- firebase_core (3.4.0):
- firebase_core (3.4.1):
- Firebase/CoreOnly (= 11.0.0)
- Flutter
- firebase_crashlytics (4.1.0):
- firebase_crashlytics (4.1.1):
- Firebase/Crashlytics (= 11.0.0)
- firebase_core
- Flutter
- firebase_messaging (15.1.0):
- firebase_messaging (15.1.1):
- Firebase/Messaging (= 11.0.0)
- firebase_core
- Flutter
- firebase_performance (0.10.0-5):
- firebase_performance (0.10.0-6):
- Firebase/Performance (= 11.0.0)
- firebase_core
- Flutter
@ -154,8 +154,12 @@ PODS:
- PromisesSwift (~> 2.1)
- FirebaseSharedSwift (11.1.0)
- Flutter (1.0.0)
- flutter_background_service_ios (0.0.3):
- Flutter
- flutter_keyboard_visibility (0.0.1):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
@ -221,7 +225,7 @@ PODS:
- TOCropViewController (~> 2.7.4)
- image_picker_ios (0.0.1):
- Flutter
- livekit_client (2.2.4):
- livekit_client (2.2.5):
- Flutter
- WebRTC-SDK (= 125.6422.04)
- media_kit_libs_ios_video (1.0.4):
@ -264,6 +268,24 @@ PODS:
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
- "sqlite3 (3.46.1+1)":
- "sqlite3/common (= 3.46.1+1)"
- "sqlite3/common (3.46.1+1)"
- "sqlite3/dbstatvtab (3.46.1+1)":
- sqlite3/common
- "sqlite3/fts5 (3.46.1+1)":
- sqlite3/common
- "sqlite3/perf-threadsafe (3.46.1+1)":
- sqlite3/common
- "sqlite3/rtree (3.46.1+1)":
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- "sqlite3 (~> 3.46.0+1)"
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- SwiftyGif (5.4.5)
- TOCropViewController (2.7.4)
- url_launcher_ios (0.0.1):
@ -284,7 +306,9 @@ DEPENDENCIES:
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- firebase_performance (from `.symlinks/plugins/firebase_performance/ios`)
- Flutter (from `Flutter`)
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
@ -305,6 +329,7 @@ DEPENDENCIES:
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
@ -334,6 +359,7 @@ SPEC REPOS:
- PromisesObjC
- PromisesSwift
- SDWebImage
- sqlite3
- SwiftyGif
- TOCropViewController
- WebRTC-SDK
@ -357,8 +383,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_performance/ios"
Flutter:
:path: Flutter
flutter_background_service_ios:
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
@ -399,6 +429,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller:
@ -413,11 +445,11 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
firebase_analytics: 1a66fe8d4375eccff44671ea37897683a78b2675
firebase_core: ceec591a66629daaee82d3321551692c4a871493
firebase_crashlytics: e4f04180f443d5a8b56fbc0685bdbd7d90dd26f0
firebase_messaging: 15d8b557010f3bb7b98d0302e1c7c8fbcd244425
firebase_performance: d373c742649e2d85d92cc223b4511c3d132887ef
firebase_analytics: b8ce6c2c4b245d3c3bb3a147965d09da0f455959
firebase_core: ba84e940cf5cbbc601095f86556560937419195c
firebase_crashlytics: 4111f8198b78c99471c955af488cecd8224967e6
firebase_messaging: c40f84e7a98da956d5262fada373b5c458edcf13
firebase_performance: 8b7b9ca5adf3a9b3afa12b4eb96b9cabefc2c248
FirebaseABTesting: c2e22c3aab99afa81d0561708b2c1c356c556976
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
@ -432,7 +464,9 @@ SPEC CHECKSUMS:
FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a
FirebaseSharedSwift: 260a35e08943ec810d820a70bc0359136351d0c5
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
@ -442,7 +476,7 @@ SPEC CHECKSUMS:
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: d079c5f040d4bf2b80440ff0ae997725a183e4bc
livekit_client: 9c8080879256a0fb16da13c9be4845248209d896
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
@ -460,6 +494,8 @@ SPEC CHECKSUMS:
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe

109
lib/background.dart Normal file
View File

@ -0,0 +1,109 @@
import 'dart:ui';
import 'package:flutter/material.dart' hide Notification;
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/models/notification.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/websocket.dart';
FlutterBackgroundService? bgNotificationService;
void autoConfigureBackgroundNotificationService() async {
if (bgNotificationService != null) return;
final prefs = await SharedPreferences.getInstance();
if (prefs.getBool('service_background_notification') != true) return;
bgNotificationService = FlutterBackgroundService();
await bgNotificationService!.configure(
androidConfiguration: AndroidConfiguration(
onStart: onBackgroundNotificationServiceStart,
autoStart: true,
autoStartOnBoot: true,
isForegroundMode: false,
),
// This feature won't be able to use on iOS
// We got APNs support covered
iosConfiguration: IosConfiguration(
autoStart: false,
),
);
}
void autoStartBackgroundNotificationService() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.getBool('service_background_notification') != true) return;
if (bgNotificationService == null) return;
bgNotificationService!.startService();
}
void autoStopBackgroundNotificationService() async {
if (bgNotificationService == null) return;
if (await bgNotificationService!.isRunning()) {
bgNotificationService?.invoke('stopService');
}
}
@pragma('vm:entry-point')
void onBackgroundNotificationServiceStart(ServiceInstance service) async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
Get.put(AuthProvider());
Get.put(WebSocketProvider());
service.on('stopService').listen((event) {
service.stopSelf();
});
final auth = Get.find<AuthProvider>();
await auth.refreshAuthorizeStatus();
await auth.ensureCredentials();
if (!auth.isAuthorized.value) {
debugPrint(
'Background notification do nothing due to user didn\'t sign in.',
);
return;
}
const notificationChannelId = 'solian_notification_service';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
final ws = Get.find<WebSocketProvider>();
await ws.connect();
debugPrint('Background notification has been started');
ws.stream.stream.listen(
(event) {
debugPrint(
'Background notification service incoming message: ${event.method} ${event.message}',
);
if (event.method == 'notifications.new' && event.payload != null) {
final data = Notification.fromJson(event.payload!);
debugPrint(
'Background notification service got a notification id=${data.id}',
);
flutterLocalNotificationsPlugin.show(
data.id,
data.title,
[data.subtitle, data.body].where((x) => x != null).join('\n'),
const NotificationDetails(
android: AndroidNotificationDetails(
notificationChannelId,
'Solian Notification Service',
channelDescription: 'Notifications that sent via Solar Network',
importance: Importance.high,
icon: 'mipmap/ic_launcher',
),
),
);
}
},
);
}

View File

@ -2,45 +2,32 @@ import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/message/adaptor.dart';
import 'package:solian/providers/message/events.dart';
import 'package:solian/providers/database/database.dart';
import 'package:solian/providers/database/services/messages.dart';
class ChatEventController {
late final MessageHistoryDb database;
late final MessagesFetchingProvider src;
final RxList<LocalEvent> currentEvents = RxList.empty(growable: true);
final RxList<LocalMessageEventTableData> currentEvents =
RxList.empty(growable: true);
final RxInt totalEvents = 0.obs;
final RxBool isLoading = false.obs;
final RxBool isLoading = true.obs;
Channel? channel;
String? scope;
Future<void> initialize() async {
if (!PlatformInfo.isWeb) {
database = await createHistoryDb();
}
src = Get.find();
currentEvents.clear();
}
Future<LocalEvent?> getEvent(int id) async {
Future<LocalMessageEventTableData?> getEvent(int id) async {
if (channel == null || scope == null) return null;
if (PlatformInfo.isWeb) {
final remoteRecord = await getRemoteEvent(id, channel!, scope!);
if (remoteRecord == null) return null;
return LocalEvent(
remoteRecord.id,
remoteRecord,
remoteRecord.channelId,
remoteRecord.createdAt,
);
} else {
return await database.getEvent(id, channel!, scope: scope!);
}
return await src.getEvent(id, channel!, scope: scope!);
}
Future<void> getEvents(Channel channel, String scope) async {
Future<void> getInitialEvents(Channel channel, String scope) async {
this.channel = channel;
this.scope = scope;
@ -48,24 +35,30 @@ class ChatEventController {
isLoading.value = true;
if (PlatformInfo.isWeb) {
final result = await getRemoteEvents(
final result = await src.fetchRemoteEvents(
channel,
scope,
remainDepth: 3,
depth: 1,
offset: 0,
);
totalEvents.value = result?.$2 ?? 0;
if (result != null) {
for (final x in result.$1.reversed) {
final entry = LocalEvent(x.id, x, x.channelId, x.createdAt);
final entry = LocalMessageEventTableData(
id: x.id,
channelId: x.channelId,
createdAt: x.createdAt,
data: x,
);
insertEvent(entry);
applyEvent(entry);
}
}
} else {
final result = await database.syncRemoteEvents(
final result = await src.pullRemoteEvents(
channel,
scope: scope,
depth: 1,
);
totalEvents.value = result?.$2 ?? 0;
await syncLocal(channel);
@ -76,22 +69,27 @@ class ChatEventController {
Future<void> loadEvents(Channel channel, String scope) async {
isLoading.value = true;
if (PlatformInfo.isWeb) {
final result = await getRemoteEvents(
final result = await src.fetchRemoteEvents(
channel,
scope,
remainDepth: 3,
depth: 3,
offset: currentEvents.length,
);
if (result != null) {
totalEvents.value = result.$2;
for (final x in result.$1.reversed) {
final entry = LocalEvent(x.id, x, x.channelId, x.createdAt);
final entry = LocalMessageEventTableData(
id: x.id,
channelId: x.channelId,
createdAt: x.createdAt,
data: x,
);
currentEvents.add(entry);
applyEvent(entry);
}
}
} else {
final result = await database.syncRemoteEvents(
final result = await src.pullRemoteEvents(
channel,
depth: 3,
scope: scope,
@ -105,7 +103,7 @@ class ChatEventController {
Future<bool> syncLocal(Channel channel) async {
if (PlatformInfo.isWeb) return false;
final data = await database.localEvents.findAllByChannel(channel.id);
final data = await src.listEvents(channel);
currentEvents.replaceRange(0, currentEvents.length, data);
for (final x in data.reversed) {
applyEvent(x);
@ -114,26 +112,29 @@ class ChatEventController {
}
receiveEvent(Event remote) async {
LocalEvent entry;
LocalMessageEventTableData entry;
if (PlatformInfo.isWeb) {
entry = LocalEvent(
remote.id,
remote,
remote.channelId,
remote.createdAt,
entry = LocalMessageEventTableData(
id: remote.id,
channelId: remote.channelId,
createdAt: remote.createdAt,
data: remote,
);
} else {
entry = await database.receiveEvent(remote);
entry = await src.receiveEvent(remote);
}
totalEvents.value++;
insertEvent(entry);
applyEvent(entry);
}
insertEvent(LocalEvent entry) {
void insertEvent(LocalMessageEventTableData entry) {
if (entry.channelId != channel?.id) return;
final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid);
final idx = currentEvents.indexWhere(
(x) => x.data!.uuid == entry.data!.uuid,
);
if (idx != -1) {
currentEvents[idx] = entry;
} else {
@ -141,36 +142,36 @@ class ChatEventController {
}
}
applyEvent(LocalEvent entry) {
void applyEvent(LocalMessageEventTableData entry) {
if (entry.channelId != channel?.id) return;
switch (entry.data.type) {
switch (entry.data!.type) {
case 'messages.edit':
final body = EventMessageBody.fromJson(entry.data.body);
final body = EventMessageBody.fromJson(entry.data!.body);
if (body.relatedEvent != null) {
final idx =
currentEvents.indexWhere((x) => x.data.id == body.relatedEvent);
currentEvents.indexWhere((x) => x.data!.id == body.relatedEvent);
if (idx != -1) {
currentEvents[idx].data.body = entry.data.body;
currentEvents[idx].data.updatedAt = entry.data.updatedAt;
currentEvents[idx].data!.body = entry.data!.body;
currentEvents[idx].data!.updatedAt = entry.data!.updatedAt;
}
}
case 'messages.delete':
final body = EventMessageBody.fromJson(entry.data.body);
final body = EventMessageBody.fromJson(entry.data!.body);
if (body.relatedEvent != null) {
currentEvents.removeWhere((x) => x.id == body.relatedEvent);
}
}
}
addPendingEvent(Event info) async {
Future<void> addPendingEvent(Event info) async {
currentEvents.insert(
0,
LocalEvent(
info.id,
info,
info.channelId,
DateTime.now(),
LocalMessageEventTableData(
id: info.id,
channelId: info.channelId,
createdAt: DateTime.now(),
data: info,
),
);
}

View File

@ -155,13 +155,14 @@ class PostEditorController extends GetxController {
);
}
void localRead() {
SharedPreferences.getInstance().then((inst) {
if (inst.containsKey('post_editor_local_save')) {
isRestoreFromLocal.value = true;
payload = jsonDecode(inst.getString('post_editor_local_save')!);
}
});
Future<bool> localRead() async {
final inst = await SharedPreferences.getInstance();
if (inst.containsKey('post_editor_local_save')) {
isRestoreFromLocal.value = true;
payload = jsonDecode(inst.getString('post_editor_local_save')!);
return true;
}
return false;
}
void localClear() {

View File

@ -9,6 +9,7 @@ import 'package:solian/providers/last_read.dart';
class PostListController extends GetxController {
String? author;
String? realm;
/// The polling source modifier.
/// - `0`: default recommendations
@ -99,8 +100,10 @@ class PostListController extends GetxController {
final idx = <dynamic>{};
postList.retainWhere((x) => idx.add(x.id));
var lastId = postList.map((x) => x.id).reduce(max);
Get.find<LastReadProvider>().feedLastReadAt = lastId;
if (postList.isNotEmpty) {
var lastId = postList.map((x) => x.id).reduce(max);
Get.find<LastReadProvider>().feedLastReadAt = lastId;
}
return result;
}
@ -123,16 +126,21 @@ class PostListController extends GetxController {
resp = await provider.listRecommendations(
pageKey,
channel: 'shuffle',
realm: realm,
);
break;
case 1:
resp = await provider.listRecommendations(
pageKey,
channel: 'friends',
realm: realm,
);
break;
default:
resp = await provider.listRecommendations(pageKey);
resp = await provider.listRecommendations(
pageKey,
realm: realm,
);
break;
}
}

View File

@ -1,9 +1,11 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
extension SolianExtenions on BuildContext {
extension AppExtensions on BuildContext {
void showSnackbar(String content, {SnackBarAction? action}) {
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
content: Text(content),
@ -102,3 +104,24 @@ extension SolianExtenions on BuildContext {
);
}
}
extension ByteFormatter on int {
String formatBytes({int decimals = 2}) {
if (this == 0) return '0 Bytes';
const k = 1024;
final dm = decimals < 0 ? 0 : decimals;
final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(this) / math.log(k)).floor().toInt();
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
}
}

View File

@ -2,19 +2,23 @@ import 'dart:ui';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide Notification;
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:get/get.dart';
import 'package:go_router/go_router.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:provider/provider.dart';
import 'package:solian/background.dart';
import 'package:solian/bootstrapper.dart';
import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/daily_sign.dart';
import 'package:solian/providers/database/database.dart';
import 'package:solian/providers/database/services/messages.dart';
import 'package:solian/providers/last_read.dart';
import 'package:solian/providers/link_expander.dart';
import 'package:solian/providers/navigation.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart';
@ -38,10 +42,12 @@ void main() async {
await Future.wait([
_initializeFirebase(),
_initializePlatformComponents(),
_initializeBackgroundNotificationService(),
]);
GoRouter.optionURLReflectsImperativeAPIs = true;
Get.put(DatabaseProvider());
Get.put(AppTranslations());
await AppTranslations.init();
@ -60,6 +66,11 @@ Future<void> _initializeFirebase() async {
};
}
Future<void> _initializeBackgroundNotificationService() async {
autoConfigureBackgroundNotificationService();
autoStartBackgroundNotificationService();
}
Future<void> _initializePlatformComponents() async {
if (!PlatformInfo.isWeb) {
await protocolHandler.register('solink');
@ -79,8 +90,8 @@ Future<void> _initializePlatformComponents() async {
}
final themeSwitcher = ThemeSwitcher(
lightThemeData: SolianTheme.build(Brightness.light),
darkThemeData: SolianTheme.build(Brightness.dark),
lightThemeData: AppTheme.build(Brightness.light),
darkThemeData: AppTheme.build(Brightness.dark),
);
class SolianApp extends StatelessWidget {
@ -123,6 +134,8 @@ class SolianApp extends StatelessWidget {
}
void _initializeProviders(BuildContext context) async {
Get.put(NavigationStateProvider());
Get.lazyPut(() => AuthProvider());
Get.lazyPut(() => RelationshipProvider());
Get.lazyPut(() => PostProvider());
@ -132,10 +145,13 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => StatusProvider());
Get.lazyPut(() => ChannelProvider());
Get.lazyPut(() => RealmProvider());
Get.lazyPut(() => MessagesFetchingProvider());
Get.lazyPut(() => ChatCallProvider());
Get.lazyPut(() => AttachmentUploaderController());
Get.lazyPut(() => LinkExpandProvider());
Get.lazyPut(() => DailySignProvider());
Get.lazyPut(() => LastReadProvider());
Get.find<WebSocketProvider>().requestPermissions();
}
}

View File

@ -12,9 +12,12 @@ class Realm {
String alias;
String name;
String description;
String? avatar;
String? banner;
bool isPublic;
bool isCommunity;
int? accountId;
int? externalId;
Realm({
required this.id,
@ -24,9 +27,12 @@ class Realm {
required this.alias,
required this.name,
required this.description,
required this.avatar,
required this.banner,
required this.isPublic,
required this.isCommunity,
this.accountId,
this.externalId,
});
factory Realm.fromJson(Map<String, dynamic> json) => _$RealmFromJson(json);

View File

@ -16,9 +16,12 @@ Realm _$RealmFromJson(Map<String, dynamic> json) => Realm(
alias: json['alias'] as String,
name: json['name'] as String,
description: json['description'] as String,
avatar: json['avatar'] as String?,
banner: json['banner'] as String?,
isPublic: json['is_public'] as bool,
isCommunity: json['is_community'] as bool,
accountId: (json['account_id'] as num?)?.toInt(),
externalId: (json['external_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$RealmToJson(Realm instance) => <String, dynamic>{
@ -29,9 +32,12 @@ Map<String, dynamic> _$RealmToJson(Realm instance) => <String, dynamic>{
'alias': instance.alias,
'name': instance.name,
'description': instance.description,
'avatar': instance.avatar,
'banner': instance.banner,
'is_public': instance.isPublic,
'is_community': instance.isCommunity,
'account_id': instance.accountId,
'external_id': instance.externalId,
};
RealmMember _$RealmMemberFromJson(Map<String, dynamic> json) => RealmMember(

View File

@ -6,9 +6,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/http/src/request/request.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/background.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/providers/database/database.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart';
@ -199,10 +200,8 @@ class AuthProvider extends GetConnect {
Get.find<WebSocketProvider>().notifications.clear();
Get.find<WebSocketProvider>().notificationUnread.value = 0;
final chatHistory = ChatEventController();
chatHistory.initialize().then((_) async {
await chatHistory.database.localEvents.wipeLocalEvents();
});
AppDatabase.removeDatabase();
autoStopBackgroundNotificationService();
storage.deleteAll();
}
@ -217,6 +216,7 @@ class AuthProvider extends GetConnect {
}
Future<void> refreshUserProfile() async {
if (!isAuthorized.value) return;
final client = configureClient('auth');
final resp = await client.get('/users/me');
if (resp.statusCode != 200) {

View File

@ -0,0 +1,45 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:get/get.dart' hide Value;
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/database/tables/messages.dart';
import 'package:solian/models/event.dart';
part 'database.g.dart';
@DriftDatabase(tables: [LocalMessageEventTable])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(name: 'solar_network_local_db');
}
static Future<int> getDatabaseSize() async {
if (PlatformInfo.isWeb) return 0;
final basepath = await getApplicationDocumentsDirectory();
return await File(join(basepath.path, 'solar_network_local_db.sqlite'))
.length();
}
static Future<void> removeDatabase() async {
if (PlatformInfo.isWeb) return;
final basepath = await getApplicationDocumentsDirectory();
final file = File(join(basepath.path, 'solar_network_local_db.sqlite'));
await Get.find<DatabaseProvider>().database.close();
await file.delete();
Get.find<DatabaseProvider>().database = AppDatabase();
}
}
class DatabaseProvider extends GetxController {
var database = AppDatabase();
}

View File

@ -0,0 +1,429 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'database.dart';
// ignore_for_file: type=lint
class $LocalMessageEventTableTable extends LocalMessageEventTable
with TableInfo<$LocalMessageEventTableTable, LocalMessageEventTableData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$LocalMessageEventTableTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _channelIdMeta =
const VerificationMeta('channelId');
@override
late final GeneratedColumn<int> channelId = GeneratedColumn<int>(
'channel_id', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
static const VerificationMeta _dataMeta = const VerificationMeta('data');
@override
late final GeneratedColumnWithTypeConverter<Event?, String> data =
GeneratedColumn<String>('data', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<Event?>($LocalMessageEventTableTable.$converterdata);
static const VerificationMeta _createdAtMeta =
const VerificationMeta('createdAt');
@override
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at', aliasedName, false,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: Constant(DateTime.now()));
@override
List<GeneratedColumn> get $columns => [id, channelId, data, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'local_message_event_table';
@override
VerificationContext validateIntegrity(
Insertable<LocalMessageEventTableData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('channel_id')) {
context.handle(_channelIdMeta,
channelId.isAcceptableOrUnknown(data['channel_id']!, _channelIdMeta));
} else if (isInserting) {
context.missing(_channelIdMeta);
}
context.handle(_dataMeta, const VerificationResult.success());
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
LocalMessageEventTableData map(Map<String, dynamic> data,
{String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return LocalMessageEventTableData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
channelId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}channel_id'])!,
data: $LocalMessageEventTableTable.$converterdata.fromSql(attachedDatabase
.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}data'])!),
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
}
@override
$LocalMessageEventTableTable createAlias(String alias) {
return $LocalMessageEventTableTable(attachedDatabase, alias);
}
static TypeConverter<Event?, String> $converterdata =
const MessageEventConverter();
}
class LocalMessageEventTableData extends DataClass
implements Insertable<LocalMessageEventTableData> {
final int id;
final int channelId;
final Event? data;
final DateTime createdAt;
const LocalMessageEventTableData(
{required this.id,
required this.channelId,
this.data,
required this.createdAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['channel_id'] = Variable<int>(channelId);
if (!nullToAbsent || data != null) {
map['data'] = Variable<String>(
$LocalMessageEventTableTable.$converterdata.toSql(data));
}
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
LocalMessageEventTableCompanion toCompanion(bool nullToAbsent) {
return LocalMessageEventTableCompanion(
id: Value(id),
channelId: Value(channelId),
data: data == null && nullToAbsent ? const Value.absent() : Value(data),
createdAt: Value(createdAt),
);
}
factory LocalMessageEventTableData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return LocalMessageEventTableData(
id: serializer.fromJson<int>(json['id']),
channelId: serializer.fromJson<int>(json['channelId']),
data: serializer.fromJson<Event?>(json['data']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'channelId': serializer.toJson<int>(channelId),
'data': serializer.toJson<Event?>(data),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
LocalMessageEventTableData copyWith(
{int? id,
int? channelId,
Value<Event?> data = const Value.absent(),
DateTime? createdAt}) =>
LocalMessageEventTableData(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
data: data.present ? data.value : this.data,
createdAt: createdAt ?? this.createdAt,
);
LocalMessageEventTableData copyWithCompanion(
LocalMessageEventTableCompanion data) {
return LocalMessageEventTableData(
id: data.id.present ? data.id.value : this.id,
channelId: data.channelId.present ? data.channelId.value : this.channelId,
data: data.data.present ? data.data.value : this.data,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('LocalMessageEventTableData(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('data: $data, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, channelId, data, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is LocalMessageEventTableData &&
other.id == this.id &&
other.channelId == this.channelId &&
other.data == this.data &&
other.createdAt == this.createdAt);
}
class LocalMessageEventTableCompanion
extends UpdateCompanion<LocalMessageEventTableData> {
final Value<int> id;
final Value<int> channelId;
final Value<Event?> data;
final Value<DateTime> createdAt;
const LocalMessageEventTableCompanion({
this.id = const Value.absent(),
this.channelId = const Value.absent(),
this.data = const Value.absent(),
this.createdAt = const Value.absent(),
});
LocalMessageEventTableCompanion.insert({
this.id = const Value.absent(),
required int channelId,
required Event? data,
this.createdAt = const Value.absent(),
}) : channelId = Value(channelId),
data = Value(data);
static Insertable<LocalMessageEventTableData> custom({
Expression<int>? id,
Expression<int>? channelId,
Expression<String>? data,
Expression<DateTime>? createdAt,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (channelId != null) 'channel_id': channelId,
if (data != null) 'data': data,
if (createdAt != null) 'created_at': createdAt,
});
}
LocalMessageEventTableCompanion copyWith(
{Value<int>? id,
Value<int>? channelId,
Value<Event?>? data,
Value<DateTime>? createdAt}) {
return LocalMessageEventTableCompanion(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
data: data ?? this.data,
createdAt: createdAt ?? this.createdAt,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (channelId.present) {
map['channel_id'] = Variable<int>(channelId.value);
}
if (data.present) {
map['data'] = Variable<String>(
$LocalMessageEventTableTable.$converterdata.toSql(data.value));
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('LocalMessageEventTableCompanion(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('data: $data, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e);
$AppDatabaseManager get managers => $AppDatabaseManager(this);
late final $LocalMessageEventTableTable localMessageEventTable =
$LocalMessageEventTableTable(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [localMessageEventTable];
}
typedef $$LocalMessageEventTableTableCreateCompanionBuilder
= LocalMessageEventTableCompanion Function({
Value<int> id,
required int channelId,
required Event? data,
Value<DateTime> createdAt,
});
typedef $$LocalMessageEventTableTableUpdateCompanionBuilder
= LocalMessageEventTableCompanion Function({
Value<int> id,
Value<int> channelId,
Value<Event?> data,
Value<DateTime> createdAt,
});
class $$LocalMessageEventTableTableFilterComposer
extends FilterComposer<_$AppDatabase, $LocalMessageEventTableTable> {
$$LocalMessageEventTableTableFilterComposer(super.$state);
ColumnFilters<int> get id => $state.composableBuilder(
column: $state.table.id,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnFilters<int> get channelId => $state.composableBuilder(
column: $state.table.channelId,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnWithTypeConverterFilters<Event?, Event, String> get data =>
$state.composableBuilder(
column: $state.table.data,
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
column,
joinBuilders: joinBuilders));
ColumnFilters<DateTime> get createdAt => $state.composableBuilder(
column: $state.table.createdAt,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
}
class $$LocalMessageEventTableTableOrderingComposer
extends OrderingComposer<_$AppDatabase, $LocalMessageEventTableTable> {
$$LocalMessageEventTableTableOrderingComposer(super.$state);
ColumnOrderings<int> get id => $state.composableBuilder(
column: $state.table.id,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<int> get channelId => $state.composableBuilder(
column: $state.table.channelId,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get data => $state.composableBuilder(
column: $state.table.data,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<DateTime> get createdAt => $state.composableBuilder(
column: $state.table.createdAt,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
}
class $$LocalMessageEventTableTableTableManager extends RootTableManager<
_$AppDatabase,
$LocalMessageEventTableTable,
LocalMessageEventTableData,
$$LocalMessageEventTableTableFilterComposer,
$$LocalMessageEventTableTableOrderingComposer,
$$LocalMessageEventTableTableCreateCompanionBuilder,
$$LocalMessageEventTableTableUpdateCompanionBuilder,
(
LocalMessageEventTableData,
BaseReferences<_$AppDatabase, $LocalMessageEventTableTable,
LocalMessageEventTableData>
),
LocalMessageEventTableData,
PrefetchHooks Function()> {
$$LocalMessageEventTableTableTableManager(
_$AppDatabase db, $LocalMessageEventTableTable table)
: super(TableManagerState(
db: db,
table: table,
filteringComposer: $$LocalMessageEventTableTableFilterComposer(
ComposerState(db, table)),
orderingComposer: $$LocalMessageEventTableTableOrderingComposer(
ComposerState(db, table)),
updateCompanionCallback: ({
Value<int> id = const Value.absent(),
Value<int> channelId = const Value.absent(),
Value<Event?> data = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
}) =>
LocalMessageEventTableCompanion(
id: id,
channelId: channelId,
data: data,
createdAt: createdAt,
),
createCompanionCallback: ({
Value<int> id = const Value.absent(),
required int channelId,
required Event? data,
Value<DateTime> createdAt = const Value.absent(),
}) =>
LocalMessageEventTableCompanion.insert(
id: id,
channelId: channelId,
data: data,
createdAt: createdAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$LocalMessageEventTableTableProcessedTableManager
= ProcessedTableManager<
_$AppDatabase,
$LocalMessageEventTableTable,
LocalMessageEventTableData,
$$LocalMessageEventTableTableFilterComposer,
$$LocalMessageEventTableTableOrderingComposer,
$$LocalMessageEventTableTableCreateCompanionBuilder,
$$LocalMessageEventTableTableUpdateCompanionBuilder,
(
LocalMessageEventTableData,
BaseReferences<_$AppDatabase, $LocalMessageEventTableTable,
LocalMessageEventTableData>
),
LocalMessageEventTableData,
PrefetchHooks Function()>;
class $AppDatabaseManager {
final _$AppDatabase _db;
$AppDatabaseManager(this._db);
$$LocalMessageEventTableTableTableManager get localMessageEventTable =>
$$LocalMessageEventTableTableTableManager(
_db, _db.localMessageEventTable);
}

View File

@ -0,0 +1,203 @@
import 'package:drift/drift.dart';
import 'package:get/get.dart' hide Value;
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/database/database.dart';
class MessagesFetchingProvider extends GetxController {
Future<(List<Event>, int)?> getWhatsNewEvents(int pivot, {take = 10}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/whats-new?pivot=$pivot&take=$take',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
return (result, response.count);
}
Future<Event?> fetchRemoteEvent(int id, Channel channel, String scope) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events/$id',
);
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Event.fromJson(resp.body);
}
Future<(List<Event>, int)?> fetchRemoteEvents(
Channel channel,
String scope, {
required int depth,
bool Function(List<Event> items)? onBrake,
take = 10,
offset = 0,
}) async {
if (depth <= 0) {
return null;
}
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
if (onBrake != null && onBrake(result)) {
return (result, response.count);
}
final expandResult = (await fetchRemoteEvents(
channel,
scope,
depth: depth - 1,
take: take,
offset: offset + result.length,
))
?.$1 ??
List.empty();
return ([...result, ...expandResult], response.count);
}
Future<LocalMessageEventTableData> receiveEvent(Event remote) async {
// Insert record
final database = Get.find<DatabaseProvider>().database;
final entry = await database
.into(database.localMessageEventTable)
.insertReturning(LocalMessageEventTableCompanion.insert(
id: Value(remote.id),
channelId: remote.channelId,
data: remote,
createdAt: Value(remote.createdAt),
));
// Handle side-effect like editing and deleting
switch (remote.type) {
case 'messages.edit':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
final target = await (database.select(database.localMessageEventTable)
..where((x) => x.id.equals(body.relatedEvent!)))
.getSingleOrNull();
if (target != null) {
target.data!.body = remote.body;
target.data!.updatedAt = remote.updatedAt;
await (database.update(database.localMessageEventTable)
..where((x) => x.id.equals(target.id)))
.write(
LocalMessageEventTableCompanion(data: Value(target.data)),
);
}
}
case 'messages.delete':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
await (database.delete(database.localMessageEventTable)
..where((x) => x.id.equals(body.relatedEvent!)))
.go();
}
}
return entry;
}
Future<LocalMessageEventTableData?> getEvent(int id, Channel channel,
{String scope = 'global'}) async {
final database = Get.find<DatabaseProvider>().database;
final localRecord = await (database.select(database.localMessageEventTable)
..where((x) => x.id.equals(id)))
.getSingleOrNull();
if (localRecord != null) return localRecord;
final remoteRecord = await fetchRemoteEvent(id, channel, scope);
if (remoteRecord == null) return null;
return await receiveEvent(remoteRecord);
}
/// Pull the remote events to local database
Future<(List<Event>, int)?> pullRemoteEvents(Channel channel,
{String scope = 'global', depth = 10, offset = 0}) async {
final database = Get.find<DatabaseProvider>().database;
final lastOne = await (database.select(database.localMessageEventTable)
..where((x) => x.channelId.equals(channel.id))
..orderBy([(t) => OrderingTerm.desc(t.id)])
..limit(1))
.getSingleOrNull();
final data = await fetchRemoteEvents(
channel,
scope,
depth: depth,
offset: offset,
onBrake: (items) {
return items.any((x) => x.id == lastOne?.id);
},
);
if (data != null) {
await database.batch((batch) {
batch.insertAllOnConflictUpdate(
database.localMessageEventTable,
data.$1.map((x) => LocalMessageEventTableCompanion(
id: Value(x.id),
channelId: Value(x.channelId),
data: Value(x),
createdAt: Value(x.createdAt),
)),
);
});
}
return data;
}
Future<List<LocalMessageEventTableData>> listEvents(Channel channel) async {
final database = Get.find<DatabaseProvider>().database;
return await (database.select(database.localMessageEventTable)
..where((x) => x.channelId.equals(channel.id))
..orderBy([(t) => OrderingTerm.desc(t.id)]))
.get();
}
Future<LocalMessageEventTableData?> getLastInChannel(Channel channel) async {
final database = Get.find<DatabaseProvider>().database;
return await (database.select(database.localMessageEventTable)
..where((x) => x.channelId.equals(channel.id))
..orderBy([(t) => OrderingTerm.desc(t.id)]))
.getSingleOrNull();
}
}

View File

@ -0,0 +1,13 @@
import 'dart:convert';
import 'package:drift/drift.dart';
class JsonConverter extends TypeConverter<Object?, String> {
const JsonConverter();
@override
Object? fromSql(String fromDb) => jsonDecode(fromDb);
@override
String toSql(Object? value) => jsonEncode(value);
}

View File

@ -0,0 +1,22 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:solian/models/event.dart';
class LocalMessageEventTable extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get channelId => integer()();
TextColumn get data => text().map(const MessageEventConverter())();
DateTimeColumn get createdAt =>
dateTime().withDefault(Constant(DateTime.now()))();
}
class MessageEventConverter extends TypeConverter<Event?, String> {
const MessageEventConverter();
@override
Event? fromSql(String fromDb) => Event.fromJson(jsonDecode(fromDb));
@override
String toSql(Event? value) => jsonEncode(value?.toJson());
}

View File

@ -1,173 +0,0 @@
import 'package:floor/floor.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/message/events.dart';
Future<MessageHistoryDb> createHistoryDb() async {
final migration1to2 = Migration(1, 2, (database) async {
await database.execute('DROP TABLE IF EXISTS LocalMessage');
});
return await $FloorMessageHistoryDb
.databaseBuilder('messaging_data.dart')
.addMigrations([migration1to2]).build();
}
Future<(List<Event>, int)?> getWhatsNewEvents(int pivot, {take = 10}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/whats-new?pivot=$pivot&take=$take',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
return (result, response.count);
}
Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events/$id',
);
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Event.fromJson(resp.body);
}
Future<(List<Event>, int)?> getRemoteEvents(
Channel channel,
String scope, {
required int remainDepth,
bool Function(List<Event> items)? onBrake,
take = 10,
offset = 0,
}) async {
if (remainDepth <= 0) {
return null;
}
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
if (onBrake != null && onBrake(result)) {
return (result, response.count);
}
final expandResult = (await getRemoteEvents(
channel,
scope,
remainDepth: remainDepth - 1,
take: take,
offset: offset + result.length,
))
?.$1 ??
List.empty();
return ([...result, ...expandResult], response.count);
}
extension MessageHistoryAdaptor on MessageHistoryDb {
Future<LocalEvent> receiveEvent(Event remote) async {
final entry = LocalEvent(
remote.id,
remote,
remote.channelId,
remote.createdAt,
);
await localEvents.insert(entry);
switch (remote.type) {
case 'messages.edit':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
final target = await localEvents.findById(body.relatedEvent!);
if (target != null) {
target.data.body = remote.body;
target.data.updatedAt = remote.updatedAt;
await localEvents.update(target);
}
}
case 'messages.delete':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
await localEvents.delete(body.relatedEvent!);
}
}
return entry;
}
Future<LocalEvent?> getEvent(int id, Channel channel,
{String scope = 'global'}) async {
final localRecord = await localEvents.findById(id);
if (localRecord != null) return localRecord;
final remoteRecord = await getRemoteEvent(id, channel, scope);
if (remoteRecord == null) return null;
return await receiveEvent(remoteRecord);
}
Future<(List<Event>, int)?> syncRemoteEvents(Channel channel,
{String scope = 'global', depth = 10, offset = 0}) async {
final lastOne = await localEvents.findLastByChannel(channel.id);
final data = await getRemoteEvents(
channel,
scope,
remainDepth: depth,
offset: offset,
onBrake: (items) {
return items.any((x) => x.id == lastOne?.id);
},
);
if (data != null) {
await localEvents.insertBulk(
data.$1
.map((x) => LocalEvent(x.id, x, x.channelId, x.createdAt))
.toList(),
);
}
return data;
}
Future<List<LocalEvent>> listEvents(Channel channel) async {
return await localEvents.findAllByChannel(channel.id);
}
}

View File

@ -1,83 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:floor/floor.dart';
import 'package:solian/models/event.dart';
import 'package:sqflite/sqflite.dart' as sqflite;
part 'events.g.dart';
@entity
class LocalEvent {
@primaryKey
final int id;
final Event data;
final int channelId;
final DateTime createdAt;
LocalEvent(this.id, this.data, this.channelId, this.createdAt);
}
class DateTimeConverter extends TypeConverter<DateTime, int> {
@override
DateTime decode(int databaseValue) {
return DateTime.fromMillisecondsSinceEpoch(databaseValue);
}
@override
int encode(DateTime value) {
return value.millisecondsSinceEpoch;
}
}
class RemoteEventConverter extends TypeConverter<Event, String> {
@override
Event decode(String databaseValue) {
return Event.fromJson(jsonDecode(databaseValue));
}
@override
String encode(Event value) {
return jsonEncode(value.toJson());
}
}
@dao
abstract class LocalEventDao {
@Query('SELECT COUNT(id) FROM LocalEvent WHERE channelId = :channelId')
Future<int?> countByChannel(int channelId);
@Query('SELECT * FROM LocalEvent WHERE id = :id')
Future<LocalEvent?> findById(int id);
@Query('SELECT * FROM LocalEvent WHERE channelId = :channelId ORDER BY createdAt DESC')
Future<List<LocalEvent>> findAllByChannel(int channelId);
@Query('SELECT * FROM LocalEvent WHERE channelId = :channelId ORDER BY createdAt DESC LIMIT 1')
Future<LocalEvent?> findLastByChannel(int channelId);
@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insert(LocalEvent m);
@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insertBulk(List<LocalEvent> m);
@Update(onConflict: OnConflictStrategy.replace)
Future<void> update(LocalEvent m);
@Query('DELETE FROM LocalEvent WHERE id = :id')
Future<void> delete(int id);
@Query('DELETE FROM LocalEvent WHERE channelId = :channelId')
Future<List<LocalEvent>> deleteByChannel(int channelId);
@Query('DELETE FROM LocalEvent')
Future<void> wipeLocalEvents();
}
@TypeConverters([DateTimeConverter, RemoteEventConverter])
@Database(version: 2, entities: [LocalEvent])
abstract class MessageHistoryDb extends FloorDatabase {
LocalEventDao get localEvents;
}

View File

@ -1,228 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'events.dart';
// **************************************************************************
// FloorGenerator
// **************************************************************************
abstract class $MessageHistoryDbBuilderContract {
/// Adds migrations to the builder.
$MessageHistoryDbBuilderContract addMigrations(List<Migration> migrations);
/// Adds a database [Callback] to the builder.
$MessageHistoryDbBuilderContract addCallback(Callback callback);
/// Creates the database and initializes it.
Future<MessageHistoryDb> build();
}
// ignore: avoid_classes_with_only_static_members
class $FloorMessageHistoryDb {
/// Creates a database builder for a persistent database.
/// Once a database is built, you should keep a reference to it and re-use it.
static $MessageHistoryDbBuilderContract databaseBuilder(String name) =>
_$MessageHistoryDbBuilder(name);
/// Creates a database builder for an in memory database.
/// Information stored in an in memory database disappears when the process is killed.
/// Once a database is built, you should keep a reference to it and re-use it.
static $MessageHistoryDbBuilderContract inMemoryDatabaseBuilder() =>
_$MessageHistoryDbBuilder(null);
}
class _$MessageHistoryDbBuilder implements $MessageHistoryDbBuilderContract {
_$MessageHistoryDbBuilder(this.name);
final String? name;
final List<Migration> _migrations = [];
Callback? _callback;
@override
$MessageHistoryDbBuilderContract addMigrations(List<Migration> migrations) {
_migrations.addAll(migrations);
return this;
}
@override
$MessageHistoryDbBuilderContract addCallback(Callback callback) {
_callback = callback;
return this;
}
@override
Future<MessageHistoryDb> build() async {
final path = name != null
? await sqfliteDatabaseFactory.getDatabasePath(name!)
: ':memory:';
final database = _$MessageHistoryDb();
database.database = await database.open(
path,
_migrations,
_callback,
);
return database;
}
}
class _$MessageHistoryDb extends MessageHistoryDb {
_$MessageHistoryDb([StreamController<String>? listener]) {
changeListener = listener ?? StreamController<String>.broadcast();
}
LocalEventDao? _localEventsInstance;
Future<sqflite.Database> open(
String path,
List<Migration> migrations, [
Callback? callback,
]) async {
final databaseOptions = sqflite.OpenDatabaseOptions(
version: 2,
onConfigure: (database) async {
await database.execute('PRAGMA foreign_keys = ON');
await callback?.onConfigure?.call(database);
},
onOpen: (database) async {
await callback?.onOpen?.call(database);
},
onUpgrade: (database, startVersion, endVersion) async {
await MigrationAdapter.runMigrations(
database, startVersion, endVersion, migrations);
await callback?.onUpgrade?.call(database, startVersion, endVersion);
},
onCreate: (database, version) async {
await database.execute(
'CREATE TABLE IF NOT EXISTS `LocalEvent` (`id` INTEGER NOT NULL, `data` TEXT NOT NULL, `channelId` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY (`id`))');
await callback?.onCreate?.call(database, version);
},
);
return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions);
}
@override
LocalEventDao get localEvents {
return _localEventsInstance ??= _$LocalEventDao(database, changeListener);
}
}
class _$LocalEventDao extends LocalEventDao {
_$LocalEventDao(
this.database,
this.changeListener,
) : _queryAdapter = QueryAdapter(database),
_localEventInsertionAdapter = InsertionAdapter(
database,
'LocalEvent',
(LocalEvent item) => <String, Object?>{
'id': item.id,
'data': _remoteEventConverter.encode(item.data),
'channelId': item.channelId,
'createdAt': _dateTimeConverter.encode(item.createdAt)
}),
_localEventUpdateAdapter = UpdateAdapter(
database,
'LocalEvent',
['id'],
(LocalEvent item) => <String, Object?>{
'id': item.id,
'data': _remoteEventConverter.encode(item.data),
'channelId': item.channelId,
'createdAt': _dateTimeConverter.encode(item.createdAt)
});
final sqflite.DatabaseExecutor database;
final StreamController<String> changeListener;
final QueryAdapter _queryAdapter;
final InsertionAdapter<LocalEvent> _localEventInsertionAdapter;
final UpdateAdapter<LocalEvent> _localEventUpdateAdapter;
@override
Future<int?> countByChannel(int channelId) async {
return _queryAdapter.query(
'SELECT COUNT(id) FROM LocalEvent WHERE channelId = ?1',
mapper: (Map<String, Object?> row) => row.values.first as int,
arguments: [channelId]);
}
@override
Future<LocalEvent?> findById(int id) async {
return _queryAdapter.query('SELECT * FROM LocalEvent WHERE id = ?1',
mapper: (Map<String, Object?> row) => LocalEvent(
row['id'] as int,
_remoteEventConverter.decode(row['data'] as String),
row['channelId'] as int,
_dateTimeConverter.decode(row['createdAt'] as int)),
arguments: [id]);
}
@override
Future<List<LocalEvent>> findAllByChannel(int channelId) async {
return _queryAdapter.queryList(
'SELECT * FROM LocalEvent WHERE channelId = ?1 ORDER BY createdAt DESC',
mapper: (Map<String, Object?> row) => LocalEvent(
row['id'] as int,
_remoteEventConverter.decode(row['data'] as String),
row['channelId'] as int,
_dateTimeConverter.decode(row['createdAt'] as int)),
arguments: [channelId]);
}
@override
Future<LocalEvent?> findLastByChannel(int channelId) async {
return _queryAdapter.query(
'SELECT * FROM LocalEvent WHERE channelId = ?1 ORDER BY createdAt DESC LIMIT 1',
mapper: (Map<String, Object?> row) => LocalEvent(row['id'] as int, _remoteEventConverter.decode(row['data'] as String), row['channelId'] as int, _dateTimeConverter.decode(row['createdAt'] as int)),
arguments: [channelId]);
}
@override
Future<void> delete(int id) async {
await _queryAdapter
.queryNoReturn('DELETE FROM LocalEvent WHERE id = ?1', arguments: [id]);
}
@override
Future<List<LocalEvent>> deleteByChannel(int channelId) async {
return _queryAdapter.queryList(
'DELETE FROM LocalEvent WHERE channelId = ?1',
mapper: (Map<String, Object?> row) => LocalEvent(
row['id'] as int,
_remoteEventConverter.decode(row['data'] as String),
row['channelId'] as int,
_dateTimeConverter.decode(row['createdAt'] as int)),
arguments: [channelId]);
}
@override
Future<void> wipeLocalEvents() async {
await _queryAdapter.queryNoReturn('DELETE FROM LocalEvent');
}
@override
Future<void> insert(LocalEvent m) async {
await _localEventInsertionAdapter.insert(m, OnConflictStrategy.replace);
}
@override
Future<void> insertBulk(List<LocalEvent> m) async {
await _localEventInsertionAdapter.insertList(m, OnConflictStrategy.replace);
}
@override
Future<void> update(LocalEvent m) async {
await _localEventUpdateAdapter.update(m, OnConflictStrategy.replace);
}
}
// ignore_for_file: unused_element
final _dateTimeConverter = DateTimeConverter();
final _remoteEventConverter = RemoteEventConverter();

View File

@ -0,0 +1,6 @@
import 'package:get/get.dart';
import 'package:solian/models/realm.dart';
class NavigationStateProvider extends GetxController {
final Rx<Realm?> focusedRealm = Rx(null);
}

View File

@ -16,8 +16,8 @@ class ThemeSwitcher extends ChangeNotifier {
if (prefs.containsKey('global_theme_color')) {
final value = prefs.getInt('global_theme_color')!;
final color = Color(value);
lightThemeData = SolianTheme.build(Brightness.light, seedColor: color);
darkThemeData = SolianTheme.build(Brightness.dark, seedColor: color);
lightThemeData = AppTheme.build(Brightness.light, seedColor: color);
darkThemeData = AppTheme.build(Brightness.dark, seedColor: color);
notifyListeners();
}
}

View File

@ -5,7 +5,9 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/notification.dart';
import 'package:solian/models/packet.dart';
@ -29,20 +31,46 @@ class WebSocketProvider extends GetxController {
@override
onInit() {
FirebaseMessaging.instance
.requestPermission(
alert: true,
announcement: true,
carPlay: true,
badge: true,
sound: true)
.then((status) {
notifyPrefetch();
});
notifyPrefetch();
super.onInit();
}
void requestPermissions() {
try {
FirebaseMessaging.instance.requestPermission(
alert: true,
announcement: true,
carPlay: true,
badge: true,
sound: true);
} catch (_) {
// When firebase isn't initialized (background service)
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
}
Future<void> connect({noRetry = false}) async {
if (isConnected.value) {
return;
@ -90,6 +118,10 @@ class WebSocketProvider extends GetxController {
final packet = NetworkPackage.fromJson(jsonDecode(event));
log('Websocket incoming message: ${packet.method} ${packet.message}');
stream.sink.add(packet);
if (packet.method == 'notifications.new') {
notifications.add(Notification.fromJson(packet.payload!));
notificationUnread.value++;
}
},
onDone: () {
isConnected.value = false;
@ -120,6 +152,12 @@ class WebSocketProvider extends GetxController {
}
Future<void> registerPushNotifications() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.getBool('service_background_notification') == true) {
log('Background notification service has been enabled, skip register push notifications');
return;
}
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;

View File

@ -154,6 +154,7 @@ abstract class AppRouter {
name: 'channelChat',
builder: (context, state) {
return ChannelChatScreen(
key: UniqueKey(),
alias: state.pathParameters['alias']!,
realm: state.uri.queryParameters['realm'] ?? 'global',
);
@ -245,7 +246,7 @@ abstract class AppRouter {
),
GoRoute(
path: '/account/personalize',
name: 'accountPersonalize',
name: 'accountProfile',
builder: (context, state) => TitleShell(
state: state,
child: const PersonalizeScreen(),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/models/account.dart';
import 'package:solian/providers/auth.dart';
@ -23,9 +24,9 @@ class _AccountScreenState extends State<AccountScreen> {
Widget build(BuildContext context) {
final actionItems = [
(
const Icon(Icons.color_lens),
'accountPersonalize'.tr,
'accountPersonalize',
const Icon(Icons.face),
'accountProfile'.tr,
'accountProfile',
),
(
Obx(() {
@ -47,9 +48,9 @@ class _AccountScreenState extends State<AccountScreen> {
'accountFriend',
),
(
const Icon(Icons.emoji_symbols),
'accountStickers'.tr,
'accountStickers',
const Icon(Icons.emoji_symbols),
'accountStickers'.tr,
'accountStickers',
),
];
@ -64,7 +65,7 @@ class _AccountScreenState extends State<AccountScreen> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ActionCard(
_ActionCard(
icon: Icon(
Icons.login,
color: Theme.of(context).colorScheme.onPrimary,
@ -85,7 +86,7 @@ class _AccountScreenState extends State<AccountScreen> {
});
},
),
ActionCard(
_ActionCard(
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onPrimary,
@ -104,6 +105,19 @@ class _AccountScreenState extends State<AccountScreen> {
});
},
),
const Gap(4),
TextButton(
style: const ButtonStyle(
visualDensity: VisualDensity(
horizontal: -4,
vertical: -2,
),
),
onPressed: () {
AppRouter.instance.pushNamed('settings');
},
child: Text('settings'.tr),
),
],
),
);
@ -126,6 +140,18 @@ class _AccountScreenState extends State<AccountScreen> {
},
),
)),
const Divider(thickness: 0.3, height: 1)
.paddingSymmetric(vertical: 4),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.settings),
title: Text('settings'.tr),
onTap: () {
AppRouter.instance.pushNamed('settings');
},
),
const Divider(thickness: 0.3, height: 1)
.paddingSymmetric(vertical: 4),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.logout),
@ -186,13 +212,13 @@ class _AccountHeadingState extends State<AccountHeading> {
}
}
class ActionCard extends StatelessWidget {
class _ActionCard extends StatelessWidget {
final Widget icon;
final String title;
final String caption;
final Function onTap;
const ActionCard({
const _ActionCard({
super.key,
required this.onTap,
required this.title,

View File

@ -133,7 +133,7 @@ class _FriendScreenState extends State<FriendScreen>
).paddingAll(14),
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
bottom: TabBar(

View File

@ -134,7 +134,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
);
if (resp.statusCode == 200) {
_syncWidget();
context.showSnackbar('accountPersonalizeApplied'.tr);
context.showSnackbar('accountProfileApplied'.tr);
} else {
context.showErrorDialog(resp.bodyString);
}
@ -163,7 +163,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
);
if (resp.statusCode == 200) {
_syncWidget();
context.showSnackbar('accountPersonalizeApplied'.tr);
context.showSnackbar('accountProfileApplied'.tr);
} else {
context.showErrorDialog(resp.bodyString);
}

View File

@ -152,7 +152,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
SliverAppBar(
centerTitle: false,
floating: true,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
leadingWidth: 24,
automaticallyImplyLeading: false,
flexibleSpace: Row(
@ -207,7 +207,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
onPressed: null,
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
),

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:solian/background.dart';
import 'package:solian/exts.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart';
@ -22,7 +23,7 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
void requestResetPassword() async {
void _requestResetPassword() async {
final username = _usernameController.value.text;
if (username.isEmpty) {
context.showErrorDialog('signinResetPasswordHint'.tr);
@ -52,7 +53,7 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
context.showModalDialog('done'.tr, 'signinResetPasswordSent'.tr);
}
void performAction() async {
void _performAction() async {
final AuthProvider auth = Get.find();
final username = _usernameController.value.text;
@ -100,6 +101,8 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
}
Get.find<WebSocketProvider>().registerPushNotifications();
autoConfigureBackgroundNotificationService();
autoStartBackgroundNotificationService();
Navigator.pop(context, true);
}
@ -121,7 +124,7 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
final uri = url.replaceFirst('solink://', '');
if (uri == 'auth?status=done') {
closeInAppWebView();
performAction();
_performAction();
}
}
@ -175,19 +178,19 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) => performAction(),
onSubmitted: (_) => _performAction(),
),
const Gap(12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: _isBusy ? null : () => requestResetPassword(),
onPressed: _isBusy ? null : () => _requestResetPassword(),
style: TextButton.styleFrom(foregroundColor: Colors.grey),
child: Text('forgotPassword'.tr),
),
TextButton(
onPressed: _isBusy ? null : () => performAction(),
onPressed: _isBusy ? null : () => _performAction(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [

View File

@ -205,7 +205,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
: AppBar(
leading: AppBarLeadingButton.adaptive(context),
centerTitle: true,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
title: Obx(
() => RichText(
textAlign: TextAlign.center,

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/exts.dart';
@ -156,7 +155,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
void _keepUpdateWithServer() {
_getOngoingCall();
_chatController.getEvents(_channel!, widget.realm);
_chatController.getInitialEvents(_channel!, widget.realm);
setState(() => _isOutOfSyncSince = null);
}
@ -193,7 +192,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
_getOngoingCall();
_getChannel().then((_) {
_chatController.getEvents(_channel!, widget.realm);
_chatController.getInitialEvents(_channel!, widget.realm);
_listenMessages();
});
}
@ -217,8 +216,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle(title),
centerTitle: false,
titleSpacing: SolianTheme.titleSpacing(context),
toolbarHeight: SolianTheme.toolbarHeight(context),
titleSpacing: AppTheme.titleSpacing(context),
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
const BackgroundStateWidget(),
Builder(builder: (context) {
@ -255,7 +254,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
},
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
),
@ -276,7 +275,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
channel: _channel!,
ongoingCall: _ongoingCall!,
onJoin: () {
if (!SolianTheme.isLargeScreen(context)) {
if (!AppTheme.isLargeScreen(context)) {
final ChatCallProvider call = Get.find();
call.gotoScreen(context);
}
@ -295,13 +294,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
},
),
),
Obx(() {
if (_chatController.isLoading.isTrue) {
return const LinearProgressIndicator().animate().slideY();
} else {
return const SizedBox.shrink();
}
}),
ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
@ -337,7 +329,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
),
Obx(() {
final ChatCallProvider call = Get.find();
if (call.isMounted.value && SolianTheme.isLargeScreen(context)) {
if (call.isMounted.value && AppTheme.isLargeScreen(context)) {
return const Expanded(
child: Row(children: [
VerticalDivider(width: 0.3, thickness: 0.3),

View File

@ -110,7 +110,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
appBar: AppBar(
title: AppBarTitle('channelOrganizing'.tr),
centerTitle: false,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
TextButton(
onPressed: _isBusy ? null : () => applyChannel(),

View File

@ -47,7 +47,7 @@ class _ChatScreenState extends State<ChatScreen> {
leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle('chat'.tr),
centerTitle: true,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
const BackgroundStateWidget(),
const NotificationButton(),
@ -95,7 +95,7 @@ class _ChatScreenState extends State<ChatScreen> {
],
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
),

View File

@ -18,8 +18,8 @@ import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/daily_sign.dart';
import 'package:solian/providers/database/services/messages.dart';
import 'package:solian/providers/last_read.dart';
import 'package:solian/providers/message/adaptor.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart';
@ -72,7 +72,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
Future<void> _pullMessages() async {
if (_lastRead.messagesLastReadAt == null) return;
log('[Dashboard] Pulling messages with pivot: ${_lastRead.messagesLastReadAt}');
final out = await getWhatsNewEvents(_lastRead.messagesLastReadAt!);
final src = Get.find<MessagesFetchingProvider>();
final out = await src.getWhatsNewEvents(_lastRead.messagesLastReadAt!);
if (out == null) return;
setState(() {
_currentMessages = out.$1;
@ -315,7 +316,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
Card(
child: ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
const EdgeInsets.only(left: 24, right: 32),
trailing: const Icon(Icons.inbox_outlined),
title: Text('notifyEmpty'.tr),
subtitle: Text('notifyEmptyCaption'.tr),
@ -368,18 +369,23 @@ class _DashboardScreenState extends State<DashboardScreen> {
return SizedBox(
width: min(480, width),
child: Card(
child: SingleChildScrollView(
child: PostListEntryWidget(
item: item,
isClickable: true,
isShowEmbed: true,
isNestedClickable: true,
onUpdate: (_) {
_pullPosts();
},
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerLow,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: SingleChildScrollView(
child: PostListEntryWidget(
item: item,
isClickable: true,
isShowEmbed: true,
isNestedClickable: true,
onUpdate: (_) {
_pullPosts();
},
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerLow,
),
),
),
).paddingSymmetric(horizontal: 8),
@ -499,7 +505,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
/// Footer
Column(
mainAxisAlignment: SolianTheme.isLargeScreen(context)
mainAxisAlignment: AppTheme.isLargeScreen(context)
? MainAxisAlignment.start
: MainAxisAlignment.center,
children: [

View File

@ -1,8 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/navigation.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart';
@ -25,21 +28,34 @@ class _FeedScreenState extends State<FeedScreen>
late final PostListController _postController;
late final TabController _tabController;
List<StreamSubscription>? _subscriptions;
@override
void initState() {
super.initState();
final navState = Get.find<NavigationStateProvider>();
_postController = PostListController();
_postController.realm = navState.focusedRealm.value?.alias;
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() {
if (_postController.mode.value == _tabController.index) return;
_postController.mode.value = _tabController.index;
_postController.reloadAllOver();
});
_subscriptions = [
Get.find<NavigationStateProvider>().focusedRealm.listen((value) {
if (value?.alias != _postController.realm) {
_postController.realm = value?.alias;
_postController.reloadAllOver();
}
}),
];
}
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
final NavigationStateProvider navState = Get.find();
return Material(
color: Theme.of(context).colorScheme.surface,
@ -69,13 +85,13 @@ class _FeedScreenState extends State<FeedScreen>
title: AppBarTitle('feed'.tr),
centerTitle: false,
floating: true,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
leading: AppBarLeadingButton.adaptive(context),
actions: [
const BackgroundStateWidget(),
const NotificationButton(),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
bottom: TabBar(
@ -96,37 +112,53 @@ class _FeedScreenState extends State<FeedScreen>
);
}
return TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: _tabController,
return Column(
children: [
RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [
PostWarpedListWidget(
controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
if (navState.focusedRealm.value != null)
MaterialBanner(
leading: const Icon(Icons.layers),
content: Text(
'postBrowsingIn'.trParams({
'region': '#${navState.focusedRealm.value!.alias}',
}),
),
]),
actions: const [SizedBox.shrink()],
),
Expanded(
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: _tabController,
children: [
RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [
PostWarpedListWidget(
controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
),
]),
),
Obx(() {
if (auth.isAuthorized.value) {
return RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [
PostWarpedListWidget(
controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
),
]),
);
} else {
return SigninRequiredOverlay(
onSignedIn: () => _postController.reloadAllOver(),
);
}
}),
PostShuffleSwiper(controller: _postController),
],
),
),
Obx(() {
if (auth.isAuthorized.value) {
return RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [
PostWarpedListWidget(
controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
),
]),
);
} else {
return SigninRequiredOverlay(
onSignedIn: () => _postController.reloadAllOver(),
);
}
}),
PostShuffleSwiper(controller: _postController),
],
);
}),
@ -138,6 +170,11 @@ class _FeedScreenState extends State<FeedScreen>
@override
void dispose() {
_postController.dispose();
if (_subscriptions != null) {
for (final subscription in _subscriptions!) {
subscription.cancel();
}
}
super.dispose();
}
}

View File

@ -61,10 +61,10 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle('draftBox'.tr),
centerTitle: false,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
),

View File

@ -11,6 +11,7 @@ import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/navigation.dart';
import 'package:solian/router.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart';
@ -124,7 +125,12 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
void initState() {
super.initState();
if (widget.edit == null && widget.reply == null && widget.repost == null) {
_editorController.localRead();
_editorController.localRead().then((res) {
if (!res) {
final navState = Get.find<NavigationStateProvider>();
_editorController.realmZone.value = navState.focusedRealm.value;
}
});
}
if (widget.reply != null) {
_editorController.replyTo.value = widget.reply;
@ -158,7 +164,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
),
),
centerTitle: false,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
TextButton(
onPressed: _isBusy ? null : () => _applyPost(),
@ -177,23 +183,19 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
children: [
ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
title: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Text(
_editorController.title ?? 'title'.tr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
title: Row(
children: [
Text(
_editorController.title ?? 'title'.tr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const Gap(6),
if (_editorController.aliasController.text.isNotEmpty)
Badge(
label: Text('#${_editorController.aliasController.text}'),
),
const Gap(6),
if (_editorController.aliasController.text.isNotEmpty)
Badge(
label:
Text('#${_editorController.aliasController.text}'),
),
],
),
],
),
subtitle: Text(
_editorController.description ?? 'description'.tr,
@ -365,12 +367,12 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
],
),
),
if (SolianTheme.isLargeScreen(context))
if (AppTheme.isLargeScreen(context))
const VerticalDivider(width: 0.3, thickness: 0.3)
.paddingSymmetric(
horizontal: 16,
),
if (SolianTheme.isLargeScreen(context))
if (AppTheme.isLargeScreen(context))
Expanded(
child: SingleChildScrollView(
child: MarkdownTextContent(

View File

@ -62,7 +62,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle('realm'.tr),
centerTitle: true,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
const BackgroundStateWidget(),
const NotificationButton(),
@ -77,7 +77,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
},
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
),

View File

@ -102,7 +102,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle('realmOrganizing'.tr),
centerTitle: false,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
TextButton(
onPressed: _isBusy ? null : () => applyRealm(),

View File

@ -114,7 +114,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
},
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
bottom: const TabBar(

View File

@ -2,8 +2,9 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/exts.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/database/database.dart';
import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/router.dart';
import 'package:solian/theme.dart';
@ -16,7 +17,7 @@ class SettingScreen extends StatefulWidget {
}
class _SettingScreenState extends State<SettingScreen> {
late final SharedPreferences _prefs;
SharedPreferences? _prefs;
Widget _buildCaptionHeader(String title) {
return Container(
@ -33,16 +34,16 @@ class _SettingScreenState extends State<SettingScreen> {
tooltip: label,
onPressed: () {
context.read<ThemeSwitcher>().setTheme(
SolianTheme.build(
AppTheme.build(
Brightness.light,
seedColor: color,
),
SolianTheme.build(
AppTheme.build(
Brightness.dark,
seedColor: color,
),
);
_prefs.setInt('global_theme_color', color.value);
_prefs?.setInt('global_theme_color', color.value);
context.clearSnackbar();
context.showSnackbar('themeColorApplied'.tr);
},
@ -62,6 +63,9 @@ class _SettingScreenState extends State<SettingScreen> {
super.initState();
SharedPreferences.getInstance().then((inst) {
_prefs = inst;
if (mounted) {
setState(() {});
}
});
}
@ -81,33 +85,69 @@ class _SettingScreenState extends State<SettingScreen> {
.toList(),
).paddingSymmetric(horizontal: 12, vertical: 8),
),
_buildCaptionHeader('notification'.tr),
Tooltip(
message: 'settingsNotificationBgServiceDesc'.tr,
child: CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
secondary: const Icon(Icons.system_security_update_warning),
enabled: PlatformInfo.isAndroid,
title: Text('settingsNotificationBgService'.tr),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('holdToSeeDetail'.tr),
Text(
'needRestartToApply'.tr,
style: const TextStyle(fontWeight: FontWeight.bold),
)
],
),
value:
_prefs?.getBool('service_background_notification') ?? false,
onChanged: (value) {
_prefs
?.setBool('service_background_notification', value ?? false)
.then((_) {
setState(() {});
});
},
),
),
_buildCaptionHeader('more'.tr),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
TextButton(
style: const ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
child: Text('about'.tr),
onPressed: () {
AppRouter.instance.pushNamed('about');
},
),
TextButton(
style: const ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
child: Text('messageHistoryWipe'.tr),
onPressed: () {
final chatHistory = ChatEventController();
chatHistory.initialize().then((_) async {
await chatHistory.database.localEvents.wipeLocalEvents();
});
},
),
],
).paddingSymmetric(horizontal: 12, vertical: 8),
ListTile(
leading: const Icon(Icons.delete_sweep),
trailing: const Icon(Icons.chevron_right),
subtitle: FutureBuilder(
future: AppDatabase.getDatabaseSize(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text('localDatabaseSize'.trParams(
{'size': 'unknown'.tr},
));
}
return Text('localDatabaseSize'.trParams(
{'size': snapshot.data!.formatBytes()},
));
},
),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('localDatabaseWipe'.tr),
onTap: () {
AppDatabase.removeDatabase().then((_) {
setState(() {});
});
},
),
ListTile(
leading: const Icon(Icons.info_outline),
trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('about'.tr),
onTap: () {
AppRouter.instance.pushNamed('about');
},
),
],
),
);

View File

@ -25,7 +25,7 @@ class CenteredShell extends StatelessWidget {
leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle(state.topRoute?.name?.tr ?? 'page'.tr),
centerTitle: false,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
)
: null,
body: Center(

View File

@ -41,10 +41,10 @@ class RootShell extends StatelessWidget {
return Scaffold(
key: rootScaffoldKey,
drawer: SolianTheme.isLargeScreen(context)
drawer: AppTheme.isLargeScreen(context)
? null
: AppNavigationDrawer(routeName: routeName),
body: SolianTheme.isLargeScreen(context)
body: AppTheme.isLargeScreen(context)
? Row(
children: [
if (showNavigation) AppNavigationDrawer(routeName: routeName),

View File

@ -29,9 +29,9 @@ class SidebarShell extends StatelessWidget {
flex: 2,
child: child,
),
if (SolianTheme.isExtraLargeScreen(context))
if (AppTheme.isExtraLargeScreen(context))
const VerticalDivider(thickness: 0.3, width: 1),
if (SolianTheme.isExtraLargeScreen(context))
if (AppTheme.isExtraLargeScreen(context))
Flexible(
flex: 1,
child: sidebar ?? const SidebarPlaceholder(),
@ -47,10 +47,10 @@ class SidebarShell extends StatelessWidget {
leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle(state.topRoute?.name?.tr ?? 'page'.tr),
centerTitle: false,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
)
: null,
body: SolianTheme.isLargeScreen(context)
body: AppTheme.isLargeScreen(context)
? Row(
children: sidebarFirst
? buildContent(context).reversed.toList()

View File

@ -32,11 +32,11 @@ class TitleShell extends StatelessWidget {
title ?? (state!.topRoute?.name?.tr ?? 'page'.tr),
),
centerTitle: isCenteredTitle,
toolbarHeight: SolianTheme.toolbarHeight(context),
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
const BackgroundStateWidget(),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
)

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:solian/platform.dart';
abstract class SolianTheme {
abstract class AppTheme {
static bool isLargeScreen(BuildContext context) =>
MediaQuery.of(context).size.width > 640;
@ -9,13 +9,13 @@ abstract class SolianTheme {
MediaQuery.of(context).size.width > 720;
static bool isSpecializedMacOS(BuildContext context) =>
PlatformInfo.isMacOS && !SolianTheme.isLargeScreen(context);
PlatformInfo.isMacOS && !AppTheme.isLargeScreen(context);
static double? titleSpacing(BuildContext context) {
if (SolianTheme.isSpecializedMacOS(context)) {
if (AppTheme.isSpecializedMacOS(context)) {
return 24;
} else {
return SolianTheme.isLargeScreen(context) ? null : 24;
return AppTheme.isLargeScreen(context) ? null : 24;
}
}

View File

@ -8,7 +8,7 @@ class AppBarTitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (SolianTheme.isSpecializedMacOS(context)) {
if (AppTheme.isSpecializedMacOS(context)) {
return Text(title);
} else {
return Text(title);

View File

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'package:desktop_drop/desktop_drop.dart';
import 'package:dismissible_page/dismissible_page.dart';
@ -209,25 +208,6 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
});
}
String _formatBytes(int bytes, {int decimals = 2}) {
if (bytes == 0) return '0 Bytes';
const k = 1024;
final dm = decimals < 0 ? 0 : decimals;
final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(bytes) / math.log(k)).floor().toInt();
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
}
void _revertMetadataList() {
final AttachmentProvider attach = Get.find();
@ -367,7 +347,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
return const SizedBox.shrink();
}
return Text(
_formatBytes(snapshot.data!),
snapshot.data!.formatBytes(),
style: Theme.of(context).textTheme.bodySmall,
);
},
@ -502,7 +482,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
),
),
Text(
'${fileType[0].toUpperCase()}${fileType.substring(1)} · ${_formatBytes(element.size)}',
'${fileType[0].toUpperCase()}${fileType.substring(1)} · ${element.size.formatBytes()}',
style: const TextStyle(fontSize: 12),
),
],

View File

@ -38,25 +38,6 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
String _formatBytes(int bytes, {int decimals = 2}) {
if (bytes == 0) return '0 Bytes';
const k = 1024;
final dm = decimals < 0 ? 0 : decimals;
final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(bytes) / math.log(k)).floor().toInt();
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
}
double _getRatio() {
final value = widget.item.metadata?['ratio'];
if (value == null) return 1;
@ -274,7 +255,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
style: metaTextStyle,
),
Text(
_formatBytes(widget.item.size),
widget.item.size.formatBytes(),
style: metaTextStyle,
),
Text(

View File

@ -7,6 +7,7 @@ import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/durations.dart';
import 'package:solian/services.dart';
@ -377,25 +378,6 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
);
}
String _formatBytes(int bytes, {int decimals = 2}) {
if (bytes == 0) return '0 Bytes';
const k = 1024;
final dm = decimals < 0 ? 0 : decimals;
final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(bytes) / math.log(k)).floor().toInt();
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
}
@override
void initState() {
super.initState();
@ -471,7 +453,7 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
),
),
Text(
_formatBytes(widget.item.size),
widget.item.size.formatBytes(),
style: GoogleFonts.robotoMono(
fontSize: 12,
shadows: labelShadows,

View File

@ -98,12 +98,12 @@ class ChannelCallIndicator extends StatelessWidget {
child: Text('callJoin'.tr),
);
} else if (call.channel.value?.id == channel.id &&
!SolianTheme.isLargeScreen(context)) {
!AppTheme.isLargeScreen(context)) {
return TextButton(
onPressed: () => onJoin(),
child: Text('callResume'.tr),
);
} else if (!SolianTheme.isLargeScreen(context)) {
} else if (!AppTheme.isLargeScreen(context)) {
return TextButton(
onPressed: null,
child: Text('callJoin'.tr),

View File

@ -11,6 +11,7 @@ class ChannelListWidget extends StatefulWidget {
final List<Channel> channels;
final int selfId;
final bool isDense;
final bool isCollapsed;
final bool noCategory;
final bool useReplace;
final Function(Channel)? onSelected;
@ -20,6 +21,7 @@ class ChannelListWidget extends StatefulWidget {
required this.channels,
required this.selfId,
this.isDense = false,
this.isCollapsed = false,
this.noCategory = false,
this.useReplace = false,
this.onSelected,
@ -103,7 +105,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
return FutureBuilder(
future: Future.delayed(
const Duration(milliseconds: 500),
() => _eventController.database.localEvents.findLastByChannel(item.id),
() => _eventController.src.getLastInChannel(item),
),
builder: (context, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
@ -112,8 +114,9 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
));
}
final data = snapshot.data!.data!;
return Text(
'${snapshot.data!.data.sender.account.nick}: ${snapshot.data!.data.body['text'] ?? 'Unsupported message to preview'}',
'${data.sender.account.nick}: ${data.body['text'] ?? 'Unsupported message to preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
@ -130,13 +133,25 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
final otherside =
item.members!.where((e) => e.account.id != widget.selfId).first;
final avatar = AccountAvatar(
content: otherside.account.avatar,
radius: widget.isDense ? 12 : 20,
bgColor: Theme.of(context).colorScheme.primary,
feColor: Theme.of(context).colorScheme.onPrimary,
);
if (widget.isCollapsed) {
return Tooltip(
message: otherside.account.nick,
child: InkWell(
child: avatar.paddingSymmetric(vertical: 12),
onTap: () => _gotoChannel(item),
),
);
}
return ListTile(
leading: AccountAvatar(
content: otherside.account.avatar,
radius: widget.isDense ? 12 : 20,
bgColor: Theme.of(context).colorScheme.primary,
feColor: Theme.of(context).colorScheme.onPrimary,
),
leading: avatar,
contentPadding: padding,
title: Text(otherside.account.nick),
subtitle: !widget.isDense
@ -145,24 +160,42 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
onTap: () => _gotoChannel(item),
);
} else {
final avatar = CircleAvatar(
backgroundColor: item.realmId == null
? Theme.of(context).colorScheme.primary
: Colors.transparent,
radius: widget.isDense ? 12 : 20,
child: FaIcon(
FontAwesomeIcons.hashtag,
color: item.realmId == null
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.primary,
size: widget.isDense ? 12 : 16,
),
);
if (widget.isCollapsed) {
return Tooltip(
message: item.name,
child: InkWell(
child: avatar.paddingSymmetric(vertical: 12),
onTap: () => _gotoChannel(item),
),
);
}
return ListTile(
minTileHeight: widget.isDense ? 48 : null,
leading: CircleAvatar(
backgroundColor: item.realmId == null
? Theme.of(context).colorScheme.primary
: Colors.transparent,
radius: widget.isDense ? 12 : 20,
child: FaIcon(
FontAwesomeIcons.hashtag,
color: item.realmId == null
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.primary,
size: widget.isDense ? 12 : 16,
),
),
leading: avatar,
contentPadding: padding,
title: Text(item.name),
subtitle: !widget.isDense ? Text(item.description) : null,
subtitle: !widget.isDense
? Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: null,
onTap: () => _gotoChannel(item),
);
}

View File

@ -75,9 +75,6 @@ class ChatEvent extends StatelessWidget {
key: Key('m${item.uuid}attachments-box'),
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.only(top: isMerged ? 0 : 4, bottom: 4),
constraints: const BoxConstraints(
maxHeight: 720,
),
child: AttachmentList(
key: Key('m${item.uuid}attachments'),
parentId: item.uuid,
@ -100,7 +97,7 @@ class ChatEvent extends StatelessWidget {
return Container(
constraints: const BoxConstraints(maxWidth: 480),
child: ChatEvent(
item: snapshot.data!.data,
item: snapshot.data!.data!,
isMerged: false,
isQuote: true,
),
@ -301,7 +298,10 @@ class ChatEvent extends StatelessWidget {
],
).paddingSymmetric(horizontal: 12),
_buildLinkExpansion().paddingOnly(left: 52, right: 8),
_buildAttachment(context).paddingOnly(left: 56, right: 8),
_buildAttachment(
context,
isMinimal: ['messages.edit'].contains(item.type),
).paddingOnly(left: 56, right: 8),
],
);
}

View File

@ -6,6 +6,7 @@ import 'package:solian/models/event.dart';
import 'package:solian/providers/last_read.dart';
import 'package:solian/widgets/chat/chat_event.dart';
import 'package:solian/widgets/chat/chat_event_action.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class ChatEventList extends StatelessWidget {
final String scope;
@ -36,8 +37,9 @@ class ChatEventList extends StatelessWidget {
reverse: true,
slivers: [
Obx(() {
return SliverList.builder(
return SliverInfiniteList(
key: Key('chat-history#${channel.id}'),
isLoading: chatController.isLoading.value,
itemCount: chatController.currentEvents.length,
itemBuilder: (context, index) {
Get.find<LastReadProvider>().messagesLastReadAt =
@ -62,7 +64,7 @@ class ChatEventList extends StatelessWidget {
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: ChatEvent(
key: Key('m${item.uuid}'),
key: Key('m${item!.uuid}'),
item: item,
isMerged: isMerged,
chatController: chatController,
@ -89,28 +91,12 @@ class ChatEventList extends StatelessWidget {
},
);
},
);
}),
Obx(() {
final amount =
chatController.totalEvents - chatController.currentEvents.length;
if (amount.value <= 0 || chatController.isLoading.isTrue) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SliverToBoxAdapter(
child: ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
leading: const Icon(Icons.sync_disabled),
title: Text('messageUnSync'.tr),
subtitle: Text('messageUnSyncCaption'.trParams({
'count': amount.string,
})),
onTap: () {
chatController.loadEvents(channel, scope);
},
),
onFetchData: () {
chatController.loadEvents(
chatController.channel!,
chatController.scope!,
);
},
);
}),
],

View File

@ -245,7 +245,8 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
_editTo = widget.edit!;
_textController.text = body.text;
_attachments.addAll(
widget.edit!.body['attachments']?.cast<int>() ?? List.empty());
widget.edit!.body['attachments']?.cast<String>() ?? List.empty(),
);
}
if (widget.reply != null) {
_replyTo = widget.reply!;

View File

@ -20,7 +20,7 @@ abstract class AppNavigation {
),
AppNavigationDestination(
icon: Icons.forum,
label: 'channelTypeDirect'.tr,
label: 'chat'.tr,
page: 'chat',
),
];

View File

@ -192,9 +192,9 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer>
}
void _autoResize() {
if (SolianTheme.isExtraLargeScreen(context)) {
if (AppTheme.isExtraLargeScreen(context)) {
_expandDrawer();
} else if (SolianTheme.isLargeScreen(context)) {
} else if (AppTheme.isLargeScreen(context)) {
_collapseDrawer();
} else {
_drawerAnimationController.value = 1;
@ -204,7 +204,8 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer>
@override
void initState() {
super.initState();
_getStatus();
final AuthProvider auth = Get.find();
if (auth.isAuthorized.value) _getStatus();
Future.delayed(Duration.zero, () => _autoResize());
_drawerAnimationController.addListener(() {
if (_drawerAnimation.value > 180 && _isCollapsed) {
@ -229,7 +230,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer>
return Drawer(
width: _drawerAnimation.value,
backgroundColor:
SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
AppTheme.isLargeScreen(context) ? Colors.transparent : null,
child: child,
);
},
@ -239,80 +240,46 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer>
children: [
_buildUserInfo().paddingSymmetric(vertical: 8),
const Divider(thickness: 0.3, height: 1),
Column(
children: AppNavigation.destinations
.map(
(e) => _isCollapsed
? Tooltip(
message: e.label,
child: InkWell(
child: Icon(e.icon, size: 20).paddingSymmetric(
horizontal: 28,
vertical: 16,
),
onTap: () {
AppRouter.instance.goNamed(e.page);
_closeDrawer();
},
),
)
: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
leading: Icon(e.icon, size: 20).paddingAll(2),
title: !_isCollapsed ? Text(e.label) : null,
enabled: true,
onTap: () {
AppRouter.instance.goNamed(e.page);
_closeDrawer();
},
),
)
.toList(),
SizedBox(
width: double.infinity,
child: Wrap(
runSpacing: 8,
spacing: 8,
alignment: WrapAlignment.spaceAround,
children: AppNavigation.destinations
.map(
(e) => Tooltip(
message: e.label,
child: InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: Icon(
e.icon,
size: 22,
color: Theme.of(context).colorScheme.onSurface,
).paddingAll(16),
onTap: () {
AppRouter.instance.goNamed(e.page);
_closeDrawer();
},
),
),
)
.toList(),
).paddingSymmetric(vertical: 8, horizontal: 12),
),
const Divider(thickness: 0.3, height: 1),
Expanded(
child: AppNavigationRegion(
isCollapsed: _isCollapsed,
onSelected: (item) {
_closeDrawer();
},
child: Material(
color: Theme.of(context).colorScheme.surface,
child: AppNavigationRegion(
isCollapsed: _isCollapsed,
),
),
),
const Divider(thickness: 0.3, height: 1),
Column(
children: [
if (_isCollapsed)
Tooltip(
message: 'settings'.tr,
child: InkWell(
child: const Icon(
Icons.settings,
size: 20,
).paddingSymmetric(
horizontal: 28,
vertical: 10,
),
onTap: () {
AppRouter.instance.pushNamed('settings');
_closeDrawer();
},
),
)
else
ListTile(
minTileHeight: 0,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
leading: const Icon(Icons.settings, size: 20).paddingAll(2),
title: Text('settings'.tr),
onTap: () {
AppRouter.instance.pushNamed('settings');
_closeDrawer();
},
),
if (_isCollapsed)
Tooltip(
message: 'expand'.tr,

View File

@ -1,48 +1,110 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart';
import 'package:collection/collection.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/navigation.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/channel/channel_list.dart';
class AppNavigationRegion extends StatelessWidget {
class AppNavigationRegion extends StatefulWidget {
final bool isCollapsed;
final Function(Channel item) onSelected;
const AppNavigationRegion({
super.key,
required this.onSelected,
this.isCollapsed = false,
});
void _gotoChannel(Channel item) {
AppRouter.instance.goNamed(
'channelChat',
pathParameters: {'alias': item.alias},
queryParameters: {
if (item.realmId != null) 'realm': item.realm!.alias,
},
);
@override
State<AppNavigationRegion> createState() => _AppNavigationRegionState();
}
onSelected(item);
class _AppNavigationRegionState extends State<AppNavigationRegion> {
bool _isTryingExit = false;
void _focusRealm(Realm item) {
setState(
() => Get.find<NavigationStateProvider>().focusedRealm.value = item,
);
}
Widget _buildEntry(BuildContext context, Channel item) {
const padding = EdgeInsets.symmetric(horizontal: 20);
void _unFocusRealm() {
setState(
() => Get.find<NavigationStateProvider>().focusedRealm.value = null,
);
}
if (isCollapsed) {
return InkWell(
child: const Icon(Icons.tag_outlined, size: 20).paddingSymmetric(
horizontal: 20,
vertical: 16,
@override
void dispose() {
super.dispose();
}
Widget _buildRealmFocusAvatar() {
final focusedRealm = Get.find<NavigationStateProvider>().focusedRealm.value;
return GestureDetector(
child: MouseRegion(
child: AnimatedSwitcher(
switchInCurve: Curves.fastOutSlowIn,
switchOutCurve: Curves.fastOutSlowIn,
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: _isTryingExit
? CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary,
child: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 16,
),
).paddingSymmetric(
vertical: 8,
)
: _buildEntryAvatar(focusedRealm!),
),
onTap: () => _gotoChannel(item),
onEnter: (_) => setState(() => _isTryingExit = true),
onExit: (_) => setState(() => _isTryingExit = false),
),
onTap: () => _unFocusRealm(),
);
}
Widget _buildEntryAvatar(Realm item) {
return Hero(
tag: Key('region-realm-avatar-${item.id}'),
child: (item.avatar?.isNotEmpty ?? false)
? AccountAvatar(content: item.avatar)
: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary,
child: const Icon(
Icons.workspaces,
color: Colors.white,
size: 16,
),
).paddingSymmetric(
vertical: 8,
),
);
}
Widget _buildEntry(BuildContext context, Realm item) {
const padding = EdgeInsets.symmetric(horizontal: 20, vertical: 8);
if (widget.isCollapsed) {
return InkWell(
child: _buildEntryAvatar(item).paddingSymmetric(vertical: 8),
onTap: () => _focusRealm(item),
);
}
return ListTile(
minTileHeight: 0,
leading: const Icon(Icons.tag_outlined),
leading: _buildEntryAvatar(item),
contentPadding: padding,
title: Text(item.name),
subtitle: Text(
@ -50,76 +112,104 @@ class AppNavigationRegion extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => _gotoChannel(item),
onTap: () => _focusRealm(item),
);
}
@override
Widget build(BuildContext context) {
final RealmProvider realms = Get.find();
final ChannelProvider channels = Get.find();
final AuthProvider auth = Get.find();
final NavigationStateProvider navState = Get.find();
return Obx(() {
final List<Channel> noRealmGroupChannels = channels.availableChannels
.where((x) => x.type == 0 && x.realmId == null)
.toList();
final List<Channel> hasRealmGroupChannels = channels.availableChannels
.where((x) => x.type == 0 && x.realmId != null)
.toList();
if (isCollapsed) {
return CustomScrollView(
slivers: [
const SliverPadding(padding: EdgeInsets.only(top: 8)),
SliverList.builder(
itemCount:
noRealmGroupChannels.length + hasRealmGroupChannels.length,
itemBuilder: (context, index) {
final element = index >= noRealmGroupChannels.length
? hasRealmGroupChannels[index - noRealmGroupChannels.length]
: noRealmGroupChannels[index];
return Tooltip(
message: element.name,
child: _buildEntry(context, element),
);
},
return Obx(
() => AnimatedSwitcher(
switchInCurve: Curves.fastOutSlowIn,
switchOutCurve: Curves.fastOutSlowIn,
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: Material(
color: Theme.of(context).colorScheme.surface,
child: child,
),
],
);
}
return CustomScrollView(
slivers: [
const SliverPadding(padding: EdgeInsets.only(top: 8)),
SliverList.builder(
itemCount: noRealmGroupChannels.length,
itemBuilder: (context, index) {
final element = noRealmGroupChannels[index];
return _buildEntry(context, element);
},
),
SliverList.list(
children: hasRealmGroupChannels
.groupListsBy((x) => x.realm)
.entries
.map((element) {
return ExpansionTile(
minTileHeight: 0,
initiallyExpanded: true,
tilePadding: const EdgeInsets.only(left: 20, right: 24),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer,
title: Text(element.value.first.realm!.name),
leading: const Icon(Icons.workspaces, size: 16)
.paddingSymmetric(horizontal: 4),
children:
element.value.map((x) => _buildEntry(context, x)).toList(),
);
}).toList(),
),
const SliverPadding(padding: EdgeInsets.only(bottom: 8)),
],
);
});
);
},
child: navState.focusedRealm.value == null
? widget.isCollapsed
? CustomScrollView(
slivers: [
const SliverPadding(padding: EdgeInsets.only(top: 16)),
SliverList.builder(
itemCount: realms.availableRealms.length,
itemBuilder: (context, index) {
final element = realms.availableRealms[index];
return Tooltip(
message: element.name,
child: _buildEntry(context, element),
);
},
),
],
)
: CustomScrollView(
slivers: [
SliverList.builder(
itemCount: realms.availableRealms.length,
itemBuilder: (context, index) {
final element = realms.availableRealms[index];
return _buildEntry(context, element);
},
),
],
)
: Column(
children: [
if (widget.isCollapsed)
Tooltip(
message: navState.focusedRealm.value!.name,
child: _buildRealmFocusAvatar().paddingOnly(
top: 24,
bottom: 8,
),
)
else
ListTile(
minTileHeight: 0,
tileColor:
Theme.of(context).colorScheme.surfaceContainerLow,
leading: _buildRealmFocusAvatar(),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 8),
title: Text(navState.focusedRealm.value!.name),
subtitle: Text(
navState.focusedRealm.value!.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
child: Obx(
() => ChannelListWidget(
useReplace: true,
channels: channels.availableChannels
.where((x) =>
x.realm?.id == navState.focusedRealm.value?.id)
.toList(),
isCollapsed: widget.isCollapsed,
selfId: auth.userProfile.value!['id'],
noCategory: true,
),
),
),
],
),
),
);
}
}

View File

@ -8,6 +8,7 @@ import 'package:intl/intl.dart';
import 'package:solian/models/post.dart';
import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/shells/title_shell.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart';
@ -302,7 +303,7 @@ class _PostItemState extends State<PostItem> {
autoload: false,
isGrid: true,
).paddingOnly(left: 36, top: 4, bottom: 4);
} else if (attachments.length > 1) {
} else if (attachments.length > 1 || AppTheme.isLargeScreen(context)) {
return AttachmentList(
parentId: widget.item.id.toString(),
attachmentsId: attachments,
@ -497,7 +498,10 @@ class _PostItemState extends State<PostItem> {
],
).paddingOnly(
top: 10,
bottom: attachments.length == 1 ? 10 : 0,
bottom:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 10
: 0,
right: 16,
left: 16,
),
@ -514,8 +518,13 @@ class _PostItemState extends State<PostItem> {
});
},
).paddingOnly(
top: attachments.length == 1 ? 10 : 6,
left: attachments.length == 1 ? 24 : 60,
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 10
: 6,
left:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 24
: 60,
right: 16,
bottom: 10,
)

View File

@ -14,6 +14,7 @@
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <pasteboard/pasteboard_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@ -41,6 +42,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_libs_linux
media_kit_video
pasteboard
sqlite3_flutter_libs
url_launcher_linux
)

View File

@ -13,6 +13,7 @@ import firebase_analytics
import firebase_core
import firebase_crashlytics
import firebase_messaging
import flutter_local_notifications
import flutter_secure_storage_macos
import flutter_webrtc
import gal
@ -28,6 +29,7 @@ import screen_brightness_macos
import share_plus
import shared_preferences_foundation
import sqflite
import sqlite3_flutter_libs
import url_launcher_macos
import wakelock_plus
@ -40,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
@ -55,6 +58,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}

View File

@ -21,19 +21,19 @@ PODS:
- Firebase/Messaging (11.0.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.0.0)
- firebase_analytics (11.3.0):
- firebase_analytics (11.3.1):
- Firebase/Analytics (= 11.0.0)
- firebase_core
- FlutterMacOS
- firebase_core (3.4.0):
- firebase_core (3.4.1):
- Firebase/CoreOnly (~> 11.0.0)
- FlutterMacOS
- firebase_crashlytics (4.1.0):
- firebase_crashlytics (4.1.1):
- Firebase/CoreOnly (~> 11.0.0)
- Firebase/Crashlytics (~> 11.0.0)
- firebase_core
- FlutterMacOS
- firebase_messaging (15.1.0):
- firebase_messaging (15.1.1):
- Firebase/CoreOnly (~> 11.0.0)
- Firebase/Messaging (~> 11.0.0)
- firebase_core
@ -97,6 +97,8 @@ PODS:
- GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1)
- flutter_local_notifications (0.0.1):
- FlutterMacOS
- flutter_secure_storage_macos (6.1.1):
- FlutterMacOS
- flutter_webrtc (0.11.3):
@ -156,7 +158,7 @@ PODS:
- GoogleUtilities/UserDefaults (8.0.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- livekit_client (2.2.4):
- livekit_client (2.2.5):
- FlutterMacOS
- WebRTC-SDK (= 125.6422.04)
- macos_window_utils (1.0.0):
@ -194,6 +196,24 @@ PODS:
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
- "sqlite3 (3.46.1+1)":
- "sqlite3/common (= 3.46.1+1)"
- "sqlite3/common (3.46.1+1)"
- "sqlite3/dbstatvtab (3.46.1+1)":
- sqlite3/common
- "sqlite3/fts5 (3.46.1+1)":
- sqlite3/common
- "sqlite3/perf-threadsafe (3.46.1+1)":
- sqlite3/common
- "sqlite3/rtree (3.46.1+1)":
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- FlutterMacOS
- "sqlite3 (~> 3.46.0+1)"
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- url_launcher_macos (0.0.1):
- FlutterMacOS
- wakelock_plus (0.0.1):
@ -209,6 +229,7 @@ DEPENDENCIES:
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
- firebase_crashlytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos`)
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
@ -226,6 +247,7 @@ DEPENDENCIES:
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
@ -247,6 +269,7 @@ SPEC REPOS:
- nanopb
- PromisesObjC
- PromisesSwift
- sqlite3
- WebRTC-SDK
EXTERNAL SOURCES:
@ -266,6 +289,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos
firebase_messaging:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
flutter_local_notifications:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
flutter_secure_storage_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
flutter_webrtc:
@ -300,6 +325,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
sqlite3_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
wakelock_plus:
@ -311,10 +338,10 @@ SPEC CHECKSUMS:
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
firebase_analytics: e9ae2af44afdaecf076797dcf9f3cd9c71ad8ac6
firebase_core: ca9bcfb9835e1bcd5ff205e24a541f6bc04e7a35
firebase_crashlytics: 87ec8afd46aeceddb565f7469df437c83fb0e01f
firebase_messaging: c834894f659ce965ba4f93fe5d0dc5d705a3cc88
firebase_analytics: 2169e28bb3ee1f765efe0fd4f5b5f625d92fda13
firebase_core: 3f80bec72646b26618f0497e74ce8bcd608f03ca
firebase_crashlytics: 6b1e511297406a6efd4405dc6301da8843b9dfe1
firebase_messaging: ce70e6615f0cd906d80b7a651b960d76dad6de56
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa
@ -324,6 +351,7 @@ SPEC CHECKSUMS:
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87
FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
@ -331,7 +359,7 @@ SPEC CHECKSUMS:
GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
livekit_client: 95f3b71e6545845aa658a6df0a3a62dcc3471d7c
livekit_client: be04a950a4b84b9dbc87507ffad5154fe75fa067
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
@ -347,6 +375,8 @@ SPEC CHECKSUMS:
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "9371d13b8ee442e3bfc08a24e3a1b3742c839abbfaf5eef11b79c4b862c89bf7"
sha256: ddc6f775260b89176d329dee26f88b9469ef46aa3228ff6a0b91caf2b2989692
url: "https://pub.dev"
source: hosted
version: "1.3.41"
version: "1.3.42"
_macros:
dependency: transitive
description: dart
@ -30,6 +30,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.7.0"
analyzer_plugin:
dependency: transitive
description:
name: analyzer_plugin
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
url: "https://pub.dev"
source: hosted
version: "0.11.3"
animations:
dependency: "direct main"
description:
@ -162,10 +170,10 @@ packages:
dependency: "direct main"
description:
name: cached_network_image
sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
description:
@ -178,10 +186,10 @@ packages:
dependency: transitive
description:
name: cached_network_image_web
sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.3.1"
carousel_slider:
dependency: "direct main"
description:
@ -298,18 +306,18 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
url: "https://pub.dev"
source: hosted
version: "2.3.6"
version: "2.3.7"
dart_webrtc:
dependency: transitive
description:
name: dart_webrtc
sha256: ac7ef077084b3e54004716f1d736fcd839e1b60bc3f21f4122a35a9bb5ca2e47
sha256: c664ad88d5646735753add421ee2118486c100febef5e92b7f59cdbabf6a51f6
url: "https://pub.dev"
source: hosted
version: "1.4.8"
version: "1.4.9"
dbus:
dependency: transitive
description:
@ -326,14 +334,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.4"
dev_build:
dependency: transitive
description:
name: dev_build
sha256: f526d1fbe68875f6119ffc333f114dfe6aa93ad04439276d53968f7977cc410e
url: "https://pub.dev"
source: hosted
version: "1.0.0+11"
device_info_plus:
dependency: "direct main"
description:
@ -374,6 +374,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
drift:
dependency: "direct main"
description:
name: drift
sha256: "5b561ec76fff260e1e0593a29ca0d058a140a4b4dfb11dcc0c3813820cd20200"
url: "https://pub.dev"
source: hosted
version: "2.20.2"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "3ee987578ca2281b5ff91eadd757cd6dd36001458d6e33784f990d67ff38f756"
url: "https://pub.dev"
source: hosted
version: "2.20.3"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: c670c947fe17ad149678a43fdbbfdb69321f0c83d315043e34e8ad2729e11f49
url: "https://pub.dev"
source: hosted
version: "0.2.0"
dropdown_button2:
dependency: "direct main"
description:
@ -426,10 +450,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3"
sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12"
url: "https://pub.dev"
source: hosted
version: "8.0.7"
version: "8.1.2"
file_selector_linux:
dependency: transitive
description:
@ -466,34 +490,34 @@ packages:
dependency: "direct main"
description:
name: firebase_analytics
sha256: "7e032ade38dec2a92f543ba02c5f72f54ffaa095c60d2132b867eab56de3bc73"
sha256: "7b5ae39d853ead76f9d030dc23389bfec4ea826d7cccb4eea4873dcb0cdd172b"
url: "https://pub.dev"
source: hosted
version: "11.3.0"
version: "11.3.1"
firebase_analytics_platform_interface:
dependency: transitive
description:
name: firebase_analytics_platform_interface
sha256: b62a2444767d95067a7e36b1d6e335e0b877968574bbbfb656168c46f2e95a13
sha256: "0205e05bb37abd29d5dec5cd89aeb04f3f58bf849aad21dd938be0507d52a40c"
url: "https://pub.dev"
source: hosted
version: "4.2.2"
version: "4.2.3"
firebase_analytics_web:
dependency: transitive
description:
name: firebase_analytics_web
sha256: bad44f71f96cfca6c16c9dd4f70b85f123ddca7d5dd698977449fadf298b1782
sha256: "434807f8b30526e21cc062410c28ee5c6680a13626c4443b5ffede29f84b0c74"
url: "https://pub.dev"
source: hosted
version: "0.5.9+2"
version: "0.5.10"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "06537da27db981947fa535bb91ca120b4e9cb59cb87278dbdde718558cafc9ff"
sha256: "40921de9795fbf5887ed5c0adfdf4972d5a8d7ae7e1b2bb98dea39bc02626a88"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.4.1"
firebase_core_platform_interface:
dependency: transitive
description:
@ -506,74 +530,74 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88"
sha256: f4ee170441ca141c5f9ee5ad8737daba3ee9c8e7efb6902aee90b4fbd178ce25
url: "https://pub.dev"
source: hosted
version: "2.17.5"
version: "2.18.0"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "4c9872020c0d97a161362ee6af7000cfdb8666234ddc290a15252ad379bb235a"
sha256: c4fdbb14ba6f36794f89dc27fb5c759c9cc67ecbaeb079edc4dba515bbf9f555
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "4.1.1"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: ede8a199ff03378857d3c8cbb7fa58d37c27bb5a6b75faf8415ff6925dcaae2a
sha256: "891d6f7ba4b93672d0e1265f27b6a9dccd56ba2cc30ce6496586b32d1d8770ac"
url: "https://pub.dev"
source: hosted
version: "3.6.41"
version: "3.6.42"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "29941ba5a3204d80656c0e52103369aa9a53edfd9ceae05a2bb3376f24fda453"
sha256: cc02c4afd6510cd84586020670140c4a23fbe52af16cd260ccf8ede101bb8d1b
url: "https://pub.dev"
source: hosted
version: "15.1.0"
version: "15.1.1"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "26c5370d3a79b15c8032724a68a4741e28f63e1f1a45699c4f0a8ae740aadd72"
sha256: d8a4984635f09213302243ea670fe5c42f3261d7d8c7c0a5f7dcd5d6c84be459
url: "https://pub.dev"
source: hosted
version: "4.5.43"
version: "4.5.44"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "58276cd5d9e22a9320ef9e5bc358628920f770f93c91221f8b638e8346ed5df4"
sha256: "258b9d637965db7855299b123533609ed95e52350746a723dfd1d8d6f3fac678"
url: "https://pub.dev"
source: hosted
version: "3.8.13"
version: "3.9.0"
firebase_performance:
dependency: "direct main"
description:
name: firebase_performance
sha256: "66666f697ecdcca2616af99f8ccfa74d795e5819c598227f2784fc00b1c6e421"
sha256: "879ce4d83242cb7d1ec67a8b45daa351f230211778e34eeea979894839c4832a"
url: "https://pub.dev"
source: hosted
version: "0.10.0+5"
version: "0.10.0+6"
firebase_performance_platform_interface:
dependency: transitive
description:
name: firebase_performance_platform_interface
sha256: ceaa026d067347cc6ea11113ba926ae450f56e305c186d1edce78f05983b481a
sha256: ac68eba644f593903a931ba7f26f0677b725d5a60f8f7bc0fed01d88a11d1463
url: "https://pub.dev"
source: hosted
version: "0.1.4+41"
version: "0.1.4+42"
firebase_performance_web:
dependency: transitive
description:
name: firebase_performance_web
sha256: "6d121cd7e27b63995998dc4039caf0cbf304c2eee6fc6ed9ac7f80860cc0e51c"
sha256: ff53b9c5d8601fc983d0173b88fdfd8abcc74948f0a3753f3c1ec276b680f23c
url: "https://pub.dev"
source: hosted
version: "0.1.6+13"
version: "0.1.7"
fixnum:
dependency: transitive
description:
@ -590,38 +614,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.69.0"
floor:
dependency: "direct main"
description:
name: floor
sha256: c1b06023912b5b8e49deb6a9d867863c535ae1a232d991c3582bba3ee8687867
url: "https://pub.dev"
source: hosted
version: "1.5.0"
floor_annotation:
dependency: transitive
description:
name: floor_annotation
sha256: a40949580a7ab0eee572686e2d3b1638fd6bd6a753e661d792ab4236b365b23b
url: "https://pub.dev"
source: hosted
version: "1.5.0"
floor_common:
dependency: transitive
description:
name: floor_common
sha256: "41c9914862f83a821815e1b1ffd47a1e1ae2130c35ff882ba2d000a67713ba64"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
floor_generator:
dependency: "direct dev"
description:
name: floor_generator
sha256: "1499b3ab878a807e6fbe6f140dc014124845cd1df3090a113aae5fa7577a1e77"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
flutter:
dependency: "direct main"
description: flutter
@ -643,6 +635,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.0"
flutter_background_service:
dependency: "direct main"
description:
name: flutter_background_service
sha256: d32f078ec57647c9cfd6e1a8da9297f7d8f021d4dcc204a35aaad2cdbfe255f0
url: "https://pub.dev"
source: hosted
version: "5.0.10"
flutter_background_service_android:
dependency: transitive
description:
name: flutter_background_service_android
sha256: "39da42dddf877beeef82bc2583130d8bedb4d0765e99ca9e7b4a32e8c6abd239"
url: "https://pub.dev"
source: hosted
version: "6.2.7"
flutter_background_service_ios:
dependency: transitive
description:
name: flutter_background_service_ios
sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
flutter_background_service_platform_interface:
dependency: transitive
description:
name: flutter_background_service_platform_interface
sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41
url: "https://pub.dev"
source: hosted
version: "5.1.2"
flutter_cache_manager:
dependency: "direct main"
description:
@ -723,6 +747,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f
url: "https://pub.dev"
source: hosted
version: "17.2.2"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af
url: "https://pub.dev"
source: hosted
version: "4.0.1"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66"
url: "https://pub.dev"
source: hosted
version: "7.2.0"
flutter_markdown:
dependency: "direct main"
description:
@ -849,10 +897,10 @@ packages:
dependency: "direct main"
description:
name: flutter_webrtc
sha256: "67faa07cf49392b50b1aa14590a83caa64d2109345fabd29899dcd8da8538348"
sha256: f6800cc2af79018c12e955ddf8ad007891fdfbb8199b0ce3dccd0977ed2add9c
url: "https://pub.dev"
source: hosted
version: "0.11.6+hotfix.1"
version: "0.11.7"
font_awesome_flutter:
dependency: "direct main"
description:
@ -861,14 +909,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "10.7.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e"
url: "https://pub.dev"
source: hosted
version: "2.5.7"
freezed_annotation:
dependency: "direct main"
description:
@ -1157,22 +1197,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
lists:
dependency: transitive
description:
name: lists
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
livekit_client:
dependency: "direct main"
description:
name: livekit_client
sha256: fc86a8b65b74b41faef646cc671c1892a457c24fd69910e25f7a50dc8cdd3155
sha256: "5df9b6f153b5f2c59fbf116b41e54597dfe8b2340b6630f7d8869887a9e58f44"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
version: "2.2.5"
logging:
dependency: transitive
description:
@ -1369,10 +1401,10 @@ packages:
dependency: "direct main"
description:
name: pasteboard
sha256: "1c8b6a8b3f1d12e55d4e9404433cda1b4abe66db6b17bc2d2fb5965772c04674"
sha256: "7bf733f3a00c7188ec1f2c6f0612854248b302cf91ef3611a2b7bb141c0f9d55"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.3.0"
path:
dependency: "direct main"
description:
@ -1390,7 +1422,7 @@ packages:
source: hosted
version: "1.0.1"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
@ -1473,10 +1505,10 @@ packages:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: fe0ffe274d665be8e34f9c59705441a7d248edebbe5d9e3ec2665f88b79358ea
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
url: "https://pub.dev"
source: hosted
version: "4.2.2"
version: "4.2.3"
permission_handler_windows:
dependency: transitive
description:
@ -1557,14 +1589,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
process_run:
dependency: transitive
description:
name: process_run
sha256: "112a77da35be50617ed9e2230df68d0817972f225e7f97ce8336f76b4e601606"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
protobuf:
dependency: transitive
description:
@ -1645,6 +1669,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rxdart:
dependency: transitive
description:
@ -1721,10 +1753,10 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: "59dfd53f497340a0c3a81909b220cfdb9b8973a91055c4e5ab9b9b9ad7c513c0"
sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
version: "10.0.2"
share_plus_platform_interface:
dependency: transitive
description:
@ -1851,7 +1883,7 @@ packages:
source: hosted
version: "7.0.0"
sqflite:
dependency: "direct main"
dependency: transitive
description:
name: sqflite
sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
@ -1866,38 +1898,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.5.4+2"
sqflite_common_ffi:
dependency: "direct dev"
description:
name: sqflite_common_ffi
sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
sqflite_common_ffi_web:
dependency: "direct dev"
description:
name: sqflite_common_ffi_web
sha256: cfc9d1c61a3e06e5b2e96994a44b11125b4f451fee95b9fad8bd473b4613d592
url: "https://pub.dev"
source: hosted
version: "0.4.3+1"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb
sha256: "45f168ae2213201b54e09429ed0c593dc2c88c924a1488d6f9c523a255d567cb"
url: "https://pub.dev"
source: hosted
version: "2.4.5"
version: "2.4.6"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1"
url: "https://pub.dev"
source: hosted
version: "0.5.24"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "7b20045d1ccfb7bc1df7e8f9fee5ae58673fce6ff62cefbb0e0fd7214e90e5a0"
sha256: "852cf80f9e974ac8e1b613758a8aa640215f7701352b66a7f468e95711eb570b"
url: "https://pub.dev"
source: hosted
version: "0.34.1"
version: "0.38.1"
stack_trace:
dependency: transitive
description:
@ -1930,14 +1954,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
strings:
dependency: transitive
description:
name: strings
sha256: "052836499f03897d3860a603b330c1ea3c8a14177b21f34b15a1295f36024aae"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
synchronized:
dependency: transitive
description:
@ -1970,6 +1986,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.7.0"
timezone:
dependency: transitive
description:
name: timezone
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
timing:
dependency: transitive
description:
@ -1986,14 +2010,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
unicode:
dependency: transitive
description:
name: unicode
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
universal_io:
dependency: transitive
description:
@ -2122,6 +2138,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
very_good_infinite_list:
dependency: "direct main"
description:
name: very_good_infinite_list
sha256: "03445e302f9e0878b6b429c096825463e0990dd38fa69a3c5c74c646afd0e485"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
vm_service:
dependency: transitive
description:
@ -2166,10 +2190,10 @@ packages:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "1.0.0"
web_socket:
dependency: transitive
description:
@ -2206,10 +2230,10 @@ packages:
dependency: transitive
description:
name: win32_registry
sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6"
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
url: "https://pub.dev"
source: hosted
version: "1.1.4"
version: "1.1.5"
xdg_directories:
dependency: transitive
description:

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App"
publish_to: "none"
version: 1.2.1+35
version: 1.2.1+37
environment:
sdk: ">=3.3.4 <4.0.0"
@ -40,11 +40,9 @@ dependencies:
package_info_plus: ^8.0.0
device_info_plus: ^10.1.0
flutter_acrylic: ^1.1.4
floor: ^1.5.0
sqflite: ^2.3.3+1
protocol_handler: ^0.2.0
markdown: ^7.2.2
pasteboard: ^0.2.0
pasteboard: ^0.3.0
desktop_drop: ^0.4.4
badges: ^3.1.2
flutter_card_swiper: ^7.0.1
@ -77,6 +75,12 @@ dependencies:
media_kit: ^1.1.11
media_kit_video: ^1.2.5
media_kit_libs_video: ^1.0.5
drift: ^2.20.2
drift_flutter: ^0.2.0
very_good_infinite_list: ^0.8.0
path_provider: ^2.1.4
flutter_background_service: ^5.0.10
flutter_local_notifications: ^17.2.2
dev_dependencies:
flutter_test:
@ -85,13 +89,10 @@ dev_dependencies:
flutter_lints: ^4.0.0
flutter_launcher_icons: ^0.13.1
floor_generator: ^1.4.0
build_runner: ^2.4.12
sqflite_common_ffi: ^2.3.3
sqflite_common_ffi_web: ^0.4.3+1
flutter_native_splash: ^2.4.1
freezed: ^2.5.7
json_serializable: ^6.8.0
drift_dev: ^2.20.3
flutter:
uses-material-design: true
@ -126,9 +127,7 @@ flutter:
weight: 700
flutter_launcher_icons:
android:
generate: "launcher_icon"
image_path: "assets/icon.png"
android: false
ios: true
image_path: "assets/icon.png"
min_sdk_android: 21

View File

@ -35,7 +35,8 @@
<link rel="manifest" href="manifest.json">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<style id="splash-screen-style">
html {
height: 100%
@ -107,6 +108,7 @@
document.body.style.background = "transparent";
}
</script>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
</head>
<body>
@ -116,6 +118,7 @@
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
</picture>
<script src="flutter_bootstrap.js" async=""></script>

View File

@ -22,6 +22,7 @@
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -57,6 +58,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -19,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
protocol_handler_windows
screen_brightness_windows
share_plus
sqlite3_flutter_libs
url_launcher_windows
)