Compare commits
28 Commits
0424f98eb5
...
2.3.2+75
| Author | SHA1 | Date | |
|---|---|---|---|
| acbc125dec | |||
| ad0ee971c1 | |||
| 52d6bb083e | |||
| 2027eab49b | |||
| 566ebde1dd | |||
| 9e039cc532 | |||
| c4b95d7084 | |||
| a66129a9ba | |||
| 44e1a8bf67 | |||
| efcfd3f57d | |||
| 84759715a4 | |||
| fda09382dd | |||
| 2c5dd0563a | |||
| 5bdd8e94fa | |||
| 2a53031c9a | |||
| e8bc7261f3 | |||
| 997934f680 | |||
| 26e69d6264 | |||
| 153eabcbf2 | |||
| 6d0145c335 | |||
| 81a79f9476 | |||
| 537f404fe0 | |||
| eb29f76b9a | |||
| 56816dc060 | |||
| 899d5f3e5e | |||
| c8c455bb57 | |||
| 5468fc0748 | |||
| 78516abf2e |
1
.github/workflows/nightly.yml
vendored
1
.github/workflows/nightly.yml
vendored
@@ -55,6 +55,7 @@ jobs:
|
|||||||
sudo apt-get install libmpv-dev mpv
|
sudo apt-get install libmpv-dev mpv
|
||||||
sudo apt-get install libayatana-appindicator3-dev
|
sudo apt-get install libayatana-appindicator3-dev
|
||||||
sudo apt-get install keybinder-3.0
|
sudo apt-get install keybinder-3.0
|
||||||
|
sudo apt-get install libnotify-dev
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
- run: flutter build linux
|
- run: flutter build linux
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
|
|||||||
@@ -689,5 +689,38 @@
|
|||||||
"databaseDeleteDescription": "Remove the database on your local disk, the content will be fetched from server again.",
|
"databaseDeleteDescription": "Remove the database on your local disk, the content will be fetched from server again.",
|
||||||
"databaseDeleted": "The local database has been deleted.",
|
"databaseDeleted": "The local database has been deleted.",
|
||||||
"settingsEnablePushNotifications": "Enable Push Notifications",
|
"settingsEnablePushNotifications": "Enable Push Notifications",
|
||||||
"settingsEnablePushNotificationsDescription": "Re-enable and request permission to receive push notifications. Just in case it didn't run automatically."
|
"settingsEnablePushNotificationsDescription": "Re-enable and request permission to receive push notifications. Just in case it didn't run automatically.",
|
||||||
|
"settingsEnabledPushNotifications": "Push notification has been enabled.",
|
||||||
|
"screenStickers": "Stickers",
|
||||||
|
"stickersDiscovery": "Discovery",
|
||||||
|
"stickersOwned": "Owned",
|
||||||
|
"stickersCreated": "Created",
|
||||||
|
"stickersAdd": "Add Sticker Pack",
|
||||||
|
"stickersAdded": "Sticker pack has been added.",
|
||||||
|
"add": "Add",
|
||||||
|
"stickersRemoved": "Sticker pack has been removed, you can add it again anytime.",
|
||||||
|
"stickersReload": "Reload Stickers",
|
||||||
|
"stickersReloadDescription": "Reload stickers from the server, update the sticker picker.",
|
||||||
|
"stickersReloaded": "Sticker packs has been reloaded.",
|
||||||
|
"stickersPackDelete": "Delete Pack {}",
|
||||||
|
"stickersPackDeleteDescription": "Are you sure you want to delete this sticker pack? This operation is irreversible.",
|
||||||
|
"stickersPackDeleted": "Sticker pack has been deleted.",
|
||||||
|
"stickersDelete": "Delete Sticker {}",
|
||||||
|
"stickersDeleteDescription": "Are you sure you want to delete this sticker? This operation is irreversible.",
|
||||||
|
"stickersDeleted": "Sticker has been deleted.",
|
||||||
|
"fieldStickerName": "Sticker Name",
|
||||||
|
"fieldStickerAlias": "Sticker Alias",
|
||||||
|
"fieldStickerAliasHint": "The unique sticker placeholder with the pack prefix.",
|
||||||
|
"fieldStickerPackName": "Name",
|
||||||
|
"fieldStickerPackDescription": "Description",
|
||||||
|
"fieldStickerPackPrefix": "Prefix",
|
||||||
|
"fieldStickerAttachment": "Attachment",
|
||||||
|
"stickersNew": "New Sticker",
|
||||||
|
"stickersNewDescription": "Create a new sticker belongs to this pack.",
|
||||||
|
"stickersPackNew": "New Sticker Pack",
|
||||||
|
"trayMenuShow": "Show",
|
||||||
|
"trayMenuMuteNotification": "Do Not Disturb",
|
||||||
|
"update": "Update",
|
||||||
|
"forceUpdate": "Force Update",
|
||||||
|
"forceUpdateDescription": "Force to show the application update popup, even the new version is not available."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -687,5 +687,38 @@
|
|||||||
"databaseDeleteDescription": "删除本地数据库,内容将从服务器重新获取。",
|
"databaseDeleteDescription": "删除本地数据库,内容将从服务器重新获取。",
|
||||||
"databaseDeleted": "本地数据库已被删除。",
|
"databaseDeleted": "本地数据库已被删除。",
|
||||||
"settingsEnablePushNotifications": "启用推送数据",
|
"settingsEnablePushNotifications": "启用推送数据",
|
||||||
"settingsEnablePushNotificationsDescription": "重新启用并请求推送权限,以防自动激活失败。"
|
"settingsEnablePushNotificationsDescription": "重新启用并请求推送权限,以防自动激活失败。",
|
||||||
|
"settingsEnabledPushNotifications": "推送通知已经注册。",
|
||||||
|
"screenStickers": "贴图",
|
||||||
|
"stickersDiscovery": "发现",
|
||||||
|
"stickersOwned": "由我拥有",
|
||||||
|
"stickersCreated": "由我发布",
|
||||||
|
"stickersAdd": "添加贴图包",
|
||||||
|
"stickersAdded": "贴图包已添加。",
|
||||||
|
"add": "添加",
|
||||||
|
"stickersRemoved": "贴图包已被移除,你可以随时再次添加回来。",
|
||||||
|
"stickersReload": "重载贴图包",
|
||||||
|
"stickersReloadDescription": "从服务器重新加载添加过的贴图,更新贴图选择器。",
|
||||||
|
"stickersReloaded": "贴图包已重载。",
|
||||||
|
"stickersPackDelete": "删除贴图包 {}",
|
||||||
|
"stickersPackDeleteDescription": "你确定要删除这个贴图包吗?这个操作不可撤销。",
|
||||||
|
"stickersPackDeleted": "贴图包已被删除。",
|
||||||
|
"stickersDelete": "删除贴图 {}",
|
||||||
|
"stickersDeleteDescription": "你确定要删除这个贴图吗?这个操作不可撤销。",
|
||||||
|
"stickersDeleted": "贴图已被删除。",
|
||||||
|
"fieldStickerName": "贴图名称",
|
||||||
|
"fieldStickerAlias": "贴图别名",
|
||||||
|
"fieldStickerAliasHint": "和贴图包前缀组合成为本贴图的唯一占位符。",
|
||||||
|
"fieldStickerPackName": "名称",
|
||||||
|
"fieldStickerPackDescription": "描述",
|
||||||
|
"fieldStickerPackPrefix": "贴图包前缀",
|
||||||
|
"fieldStickerAttachment": "附件",
|
||||||
|
"stickersNew": "新建贴图",
|
||||||
|
"stickersNewDescription": "创建一个新的贴图。",
|
||||||
|
"stickersPackNew": "新建贴图包",
|
||||||
|
"trayMenuShow": "显示",
|
||||||
|
"trayMenuMuteNotification": "静音通知",
|
||||||
|
"update": "更新",
|
||||||
|
"forceUpdate": "强制更新",
|
||||||
|
"forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -685,5 +685,40 @@
|
|||||||
"databaseSize": "數據庫大小",
|
"databaseSize": "數據庫大小",
|
||||||
"databaseDelete": "刪除數據庫",
|
"databaseDelete": "刪除數據庫",
|
||||||
"databaseDeleteDescription": "刪除本地數據庫,內容將從服務器重新獲取。",
|
"databaseDeleteDescription": "刪除本地數據庫,內容將從服務器重新獲取。",
|
||||||
"databaseDeleted": "本地數據庫已被刪除。"
|
"databaseDeleted": "本地數據庫已被刪除。",
|
||||||
|
"settingsEnablePushNotifications": "啓用推送數據",
|
||||||
|
"settingsEnablePushNotificationsDescription": "重新啓用並請求推送權限,以防自動激活失敗。",
|
||||||
|
"settingsEnabledPushNotifications": "推送通知已經註冊。",
|
||||||
|
"screenStickers": "貼圖",
|
||||||
|
"stickersDiscovery": "發現",
|
||||||
|
"stickersOwned": "由我擁有",
|
||||||
|
"stickersCreated": "由我發佈",
|
||||||
|
"stickersAdd": "添加貼圖包",
|
||||||
|
"stickersAdded": "貼圖包已添加。",
|
||||||
|
"add": "添加",
|
||||||
|
"stickersRemoved": "貼圖包已被移除,你可以隨時再次添加回來。",
|
||||||
|
"stickersReload": "重載貼圖包",
|
||||||
|
"stickersReloadDescription": "從服務器重新加載添加過的貼圖,更新貼圖選擇器。",
|
||||||
|
"stickersReloaded": "貼圖包已重載。",
|
||||||
|
"stickersPackDelete": "刪除貼圖包 {}",
|
||||||
|
"stickersPackDeleteDescription": "你確定要刪除這個貼圖包嗎?這個操作不可撤銷。",
|
||||||
|
"stickersPackDeleted": "貼圖包已被刪除。",
|
||||||
|
"stickersDelete": "刪除貼圖 {}",
|
||||||
|
"stickersDeleteDescription": "你確定要刪除這個貼圖嗎?這個操作不可撤銷。",
|
||||||
|
"stickersDeleted": "貼圖已被刪除。",
|
||||||
|
"fieldStickerName": "貼圖名稱",
|
||||||
|
"fieldStickerAlias": "貼圖別名",
|
||||||
|
"fieldStickerAliasHint": "和貼圖包前綴組合成為本貼圖的唯一佔位符。",
|
||||||
|
"fieldStickerPackName": "名稱",
|
||||||
|
"fieldStickerPackDescription": "描述",
|
||||||
|
"fieldStickerPackPrefix": "貼圖包前綴",
|
||||||
|
"fieldStickerAttachment": "附件",
|
||||||
|
"stickersNew": "新建貼圖",
|
||||||
|
"stickersNewDescription": "創建一個新的貼圖。",
|
||||||
|
"stickersPackNew": "新建貼圖包",
|
||||||
|
"trayMenuShow": "顯示",
|
||||||
|
"trayMenuMuteNotification": "靜音通知",
|
||||||
|
"update": "更新",
|
||||||
|
"forceUpdate": "強制更新",
|
||||||
|
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -685,5 +685,40 @@
|
|||||||
"databaseSize": "數據庫大小",
|
"databaseSize": "數據庫大小",
|
||||||
"databaseDelete": "刪除數據庫",
|
"databaseDelete": "刪除數據庫",
|
||||||
"databaseDeleteDescription": "刪除本地數據庫,內容將從服務器重新獲取。",
|
"databaseDeleteDescription": "刪除本地數據庫,內容將從服務器重新獲取。",
|
||||||
"databaseDeleted": "本地數據庫已被刪除。"
|
"databaseDeleted": "本地數據庫已被刪除。",
|
||||||
|
"settingsEnablePushNotifications": "啟用推送數據",
|
||||||
|
"settingsEnablePushNotificationsDescription": "重新啟用並請求推送權限,以防自動激活失敗。",
|
||||||
|
"settingsEnabledPushNotifications": "推送通知已經註冊。",
|
||||||
|
"screenStickers": "貼圖",
|
||||||
|
"stickersDiscovery": "發現",
|
||||||
|
"stickersOwned": "由我擁有",
|
||||||
|
"stickersCreated": "由我發佈",
|
||||||
|
"stickersAdd": "添加貼圖包",
|
||||||
|
"stickersAdded": "貼圖包已添加。",
|
||||||
|
"add": "添加",
|
||||||
|
"stickersRemoved": "貼圖包已被移除,你可以隨時再次添加回來。",
|
||||||
|
"stickersReload": "重載貼圖包",
|
||||||
|
"stickersReloadDescription": "從服務器重新加載添加過的貼圖,更新貼圖選擇器。",
|
||||||
|
"stickersReloaded": "貼圖包已重載。",
|
||||||
|
"stickersPackDelete": "刪除貼圖包 {}",
|
||||||
|
"stickersPackDeleteDescription": "你確定要刪除這個貼圖包嗎?這個操作不可撤銷。",
|
||||||
|
"stickersPackDeleted": "貼圖包已被刪除。",
|
||||||
|
"stickersDelete": "刪除貼圖 {}",
|
||||||
|
"stickersDeleteDescription": "你確定要刪除這個貼圖嗎?這個操作不可撤銷。",
|
||||||
|
"stickersDeleted": "貼圖已被刪除。",
|
||||||
|
"fieldStickerName": "貼圖名稱",
|
||||||
|
"fieldStickerAlias": "貼圖別名",
|
||||||
|
"fieldStickerAliasHint": "和貼圖包前綴組合成為本貼圖的唯一佔位符。",
|
||||||
|
"fieldStickerPackName": "名稱",
|
||||||
|
"fieldStickerPackDescription": "描述",
|
||||||
|
"fieldStickerPackPrefix": "貼圖包前綴",
|
||||||
|
"fieldStickerAttachment": "附件",
|
||||||
|
"stickersNew": "新建貼圖",
|
||||||
|
"stickersNewDescription": "創建一個新的貼圖。",
|
||||||
|
"stickersPackNew": "新建貼圖包",
|
||||||
|
"trayMenuShow": "顯示",
|
||||||
|
"trayMenuMuteNotification": "靜音通知",
|
||||||
|
"update": "更新",
|
||||||
|
"forceUpdate": "強制更新",
|
||||||
|
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ PODS:
|
|||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.2.0)
|
- Kingfisher (8.2.0)
|
||||||
- livekit_client (2.3.6):
|
- livekit_client (2.4.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
@@ -426,7 +426,7 @@ SPEC CHECKSUMS:
|
|||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||||
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
||||||
livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
|
livekit_client: 9819ebc8be8ef00ed0fae7d806bf8938ec689573
|
||||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||||
|
|||||||
@@ -194,9 +194,11 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
channelId: channel!.id,
|
channelId: channel!.id,
|
||||||
createdAt: Value(message.createdAt),
|
createdAt: Value(message.createdAt),
|
||||||
),
|
),
|
||||||
onConflict: DoUpdate((_) => SnLocalChatMessageCompanion.custom(
|
onConflict: DoUpdate(
|
||||||
|
(_) => SnLocalChatMessageCompanion.custom(
|
||||||
content: Constant(jsonEncode(message.toJson())),
|
content: Constant(jsonEncode(message.toJson())),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
incomeStrandedQueue.add(message);
|
incomeStrandedQueue.add(message);
|
||||||
@@ -212,12 +214,13 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
final idx =
|
final idx =
|
||||||
messages.indexWhere((x) => x.id == message.relatedEventId);
|
messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
final newBody = message.body;
|
final newBody = Map<String, dynamic>.from(message.body);
|
||||||
newBody.remove('related_event');
|
newBody.remove('related_event');
|
||||||
messages[idx] = messages[idx].copyWith(
|
messages[idx] = messages[idx].copyWith(
|
||||||
body: newBody,
|
body: newBody,
|
||||||
updatedAt: message.updatedAt,
|
updatedAt: message.updatedAt,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
if (message.relatedEventId != null) {
|
if (message.relatedEventId != null) {
|
||||||
await (_dt.db.snLocalChatMessage.update()
|
await (_dt.db.snLocalChatMessage.update()
|
||||||
..where((e) => e.id.equals(message.relatedEventId!)))
|
..where((e) => e.id.equals(message.relatedEventId!)))
|
||||||
@@ -228,7 +231,6 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case 'messages.delete':
|
case 'messages.delete':
|
||||||
if (message.relatedEventId != null) {
|
if (message.relatedEventId != null) {
|
||||||
messages.removeWhere((x) => x.id == message.relatedEventId);
|
messages.removeWhere((x) => x.id == message.relatedEventId);
|
||||||
@@ -322,6 +324,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
final mostRecentMessage = await (_dt.db.snLocalChatMessage.select()
|
final mostRecentMessage = await (_dt.db.snLocalChatMessage.select()
|
||||||
|
..where((e) => e.channelId.equals(channel!.id))
|
||||||
..limit(1)
|
..limit(1)
|
||||||
..orderBy([
|
..orderBy([
|
||||||
(e) =>
|
(e) =>
|
||||||
@@ -514,6 +517,11 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_readEventDebounce = Timer(const Duration(milliseconds: 500), () {
|
_readEventDebounce = Timer(const Duration(milliseconds: 500), () {
|
||||||
|
_sendReadEvent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendReadEvent() {
|
||||||
_ws.conn?.sink.add(jsonEncode(
|
_ws.conn?.sink.add(jsonEncode(
|
||||||
WebSocketPackage(
|
WebSocketPackage(
|
||||||
method: 'events.read',
|
method: 'events.read',
|
||||||
@@ -525,12 +533,14 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
).toJson(),
|
).toJson(),
|
||||||
));
|
));
|
||||||
log('[Messaging] Send read event request: $_readEventAnchor');
|
log('[Messaging] Send read event request: $_readEventAnchor');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_wsSubscription?.cancel();
|
_wsSubscription?.cancel();
|
||||||
|
if (_readEventDebounce?.isActive ?? false) {
|
||||||
|
_sendReadEvent();
|
||||||
|
}
|
||||||
_readEventDebounce?.cancel();
|
_readEventDebounce?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import 'package:workmanager/workmanager.dart';
|
|||||||
import 'package:in_app_review/in_app_review.dart';
|
import 'package:in_app_review/in_app_review.dart';
|
||||||
import 'package:image_picker_android/image_picker_android.dart';
|
import 'package:image_picker_android/image_picker_android.dart';
|
||||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||||
|
import 'package:local_notifier/local_notifier.dart';
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void appBackgroundDispatcher() {
|
void appBackgroundDispatcher() {
|
||||||
@@ -253,10 +254,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
receiveTimeout: const Duration(seconds: 60),
|
receiveTimeout: const Duration(seconds: 60),
|
||||||
),
|
),
|
||||||
).get(
|
).get(
|
||||||
'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1',
|
'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest',
|
||||||
);
|
);
|
||||||
final remoteVersionString =
|
final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
|
||||||
(resp.data as List).firstOrNull?['name'] ?? '0.0.0+0';
|
|
||||||
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
|
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
|
||||||
final localVersion = Version.parse(localVersionString.split('+').first);
|
final localVersion = Version.parse(localVersionString.split('+').first);
|
||||||
final remoteBuildNumber =
|
final remoteBuildNumber =
|
||||||
@@ -268,10 +268,12 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
remoteBuildNumber > localBuildNumber) &&
|
remoteBuildNumber > localBuildNumber) &&
|
||||||
mounted) {
|
mounted) {
|
||||||
final config = context.read<ConfigProvider>();
|
final config = context.read<ConfigProvider>();
|
||||||
config.setUpdate(remoteVersionString);
|
config.setUpdate(
|
||||||
|
remoteVersionString, resp.data?['body'] ?? 'No changelog');
|
||||||
log("[Update] Update available: $remoteVersionString");
|
log("[Update] Update available: $remoteVersionString");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
log('[Error] Unable to check update: $e');
|
||||||
if (mounted) context.showErrorDialog('Unable to check update: $e');
|
if (mounted) context.showErrorDialog('Unable to check update: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,7 +305,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
await notify.registerPushNotifications();
|
await notify.registerPushNotifications();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final sticker = context.read<SnStickerProvider>();
|
final sticker = context.read<SnStickerProvider>();
|
||||||
await sticker.listStickerEagerly();
|
await sticker.listSticker();
|
||||||
|
log('[Bootstrap] Everything initialized!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await context.showErrorDialog(err);
|
await context.showErrorDialog(err);
|
||||||
@@ -330,6 +333,31 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Menu _appTrayMenu = Menu(
|
||||||
|
items: [
|
||||||
|
MenuItem(
|
||||||
|
key: 'version_label',
|
||||||
|
label: 'Solian',
|
||||||
|
disabled: true,
|
||||||
|
),
|
||||||
|
MenuItem.separator(),
|
||||||
|
MenuItem.checkbox(
|
||||||
|
checked: false,
|
||||||
|
key: 'mute_notification',
|
||||||
|
label: 'trayMenuMuteNotification'.tr(),
|
||||||
|
),
|
||||||
|
MenuItem.separator(),
|
||||||
|
MenuItem(
|
||||||
|
key: 'window_show',
|
||||||
|
label: 'trayMenuShow'.tr(),
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
key: 'exit',
|
||||||
|
label: 'trayMenuExit'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> _trayInitialization() async {
|
Future<void> _trayInitialization() async {
|
||||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||||
|
|
||||||
@@ -341,21 +369,22 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
trayManager.addListener(this);
|
trayManager.addListener(this);
|
||||||
await trayManager.setIcon(icon);
|
await trayManager.setIcon(icon);
|
||||||
|
|
||||||
Menu menu = Menu(
|
_appTrayMenu.items![0] = MenuItem(
|
||||||
items: [
|
|
||||||
MenuItem(
|
|
||||||
key: 'version_label',
|
key: 'version_label',
|
||||||
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
|
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
),
|
|
||||||
MenuItem.separator(),
|
|
||||||
MenuItem(
|
|
||||||
key: 'exit',
|
|
||||||
label: 'trayMenuExit'.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
await trayManager.setContextMenu(menu);
|
|
||||||
|
await trayManager.setContextMenu(_appTrayMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _notifyInitialization() async {
|
||||||
|
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||||
|
|
||||||
|
await localNotifier.setup(
|
||||||
|
appName: 'Solian',
|
||||||
|
shortcutPolicy: ShortcutPolicy.requireCreate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppLifecycleListener? _appLifecycleListener;
|
AppLifecycleListener? _appLifecycleListener;
|
||||||
@@ -372,6 +401,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
|
|
||||||
_trayInitialization();
|
_trayInitialization();
|
||||||
_hotkeyInitialization();
|
_hotkeyInitialization();
|
||||||
|
_notifyInitialization();
|
||||||
_initialize().then((_) {
|
_initialize().then((_) {
|
||||||
_postInitialization();
|
_postInitialization();
|
||||||
_tryRequestRating();
|
_tryRequestRating();
|
||||||
@@ -407,9 +437,23 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
@override
|
@override
|
||||||
void onTrayMenuItemClick(MenuItem menuItem) {
|
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||||
switch (menuItem.key) {
|
switch (menuItem.key) {
|
||||||
|
case 'mute_notification':
|
||||||
|
final nty = context.read<NotificationProvider>();
|
||||||
|
nty.isMuted = !nty.isMuted;
|
||||||
|
_appTrayMenu.items![2].checked = nty.isMuted;
|
||||||
|
trayManager.setContextMenu(_appTrayMenu);
|
||||||
|
break;
|
||||||
|
case 'window_show':
|
||||||
|
// To prevent the window from being hide after just show on macOS
|
||||||
|
Timer(const Duration(milliseconds: 100), () => appWindow.show());
|
||||||
|
break;
|
||||||
case 'exit':
|
case 'exit':
|
||||||
_appLifecycleListener?.dispose();
|
_appLifecycleListener?.dispose();
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
appWindow.close();
|
||||||
|
} else {
|
||||||
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:surface/database/database.dart';
|
import 'package:surface/database/database.dart';
|
||||||
import 'package:surface/providers/database.dart';
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/sn_realm.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
|
||||||
|
|
||||||
class ChatChannelProvider extends ChangeNotifier {
|
class ChatChannelProvider extends ChangeNotifier {
|
||||||
static const kChatChannelBoxName = 'nex_chat_channels';
|
static const kChatChannelBoxName = 'nex_chat_channels';
|
||||||
@@ -16,11 +16,13 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final UserDirectoryProvider _ud;
|
late final UserDirectoryProvider _ud;
|
||||||
late final DatabaseProvider _dt;
|
late final DatabaseProvider _dt;
|
||||||
|
late final SnRealmProvider _rels;
|
||||||
|
|
||||||
ChatChannelProvider(BuildContext context) {
|
ChatChannelProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_ud = context.read<UserDirectoryProvider>();
|
_ud = context.read<UserDirectoryProvider>();
|
||||||
_dt = context.read<DatabaseProvider>();
|
_dt = context.read<DatabaseProvider>();
|
||||||
|
_rels = context.read<SnRealmProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
||||||
@@ -44,16 +46,9 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SnChannel>> _fetchChannelsFromServer({
|
Future<List<SnChannel>> _fetchChannelsFromServer({
|
||||||
String scope = 'global',
|
|
||||||
bool direct = false,
|
|
||||||
bool doNotSave = false,
|
bool doNotSave = false,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client.get(
|
final resp = await _sn.client.get('/cgi/im/channels/me/available');
|
||||||
'/cgi/im/channels/$scope/me/available',
|
|
||||||
queryParameters: {
|
|
||||||
'direct': direct,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final out = List<SnChannel>.from(
|
final out = List<SnChannel>.from(
|
||||||
resp.data?.map((e) => SnChannel.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnChannel.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
@@ -68,7 +63,14 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
final local = await (_dt.db.snLocalChatChannel.select()
|
final local = await (_dt.db.snLocalChatChannel.select()
|
||||||
..where((e) => e.alias.equals(key)))
|
..where((e) => e.alias.equals(key)))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
if (local != null) return local.content;
|
if (local != null) {
|
||||||
|
final out = local.content;
|
||||||
|
if (out.realmId != null) {
|
||||||
|
return out.copyWith(realm: await _rels.getRealm(out.realmId!));
|
||||||
|
} else {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var resp =
|
var resp =
|
||||||
await _sn.client.get('/cgi/im/channels/${key.replaceAll(':', '/')}');
|
await _sn.client.get('/cgi/im/channels/${key.replaceAll(':', '/')}');
|
||||||
@@ -76,8 +78,7 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
// Preload realm of the channel
|
// Preload realm of the channel
|
||||||
if (out.realmId != null) {
|
if (out.realmId != null) {
|
||||||
resp = await _sn.client.get('/cgi/id/realms/${out.realmId}');
|
out = out.copyWith(realm: await _rels.getRealm(out.realmId!));
|
||||||
out = out.copyWith(realm: SnRealm.fromJson(resp.data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_saveChannelToLocal([out]);
|
_saveChannelToLocal([out]);
|
||||||
@@ -98,44 +99,30 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
|
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
|
||||||
]))
|
]))
|
||||||
.get();
|
.get();
|
||||||
yield local.map((e) => e.content).toList();
|
final out = local.map((e) => e.content).toList();
|
||||||
|
for (var idx = 0; idx < out.length; idx++) {
|
||||||
|
final channel = out[idx];
|
||||||
|
if (channel.realmId != null) {
|
||||||
|
out[idx] = out[idx].copyWith(
|
||||||
|
realm: await _rels.getRealm(channel.realmId!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield out;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noRemote) return;
|
if (noRemote) return;
|
||||||
|
|
||||||
var resp = await _sn.client.get('/cgi/id/realms/me/available');
|
|
||||||
final realms = List<SnRealm>.from(
|
|
||||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
|
||||||
);
|
|
||||||
final realmMap = {
|
|
||||||
for (final realm in realms) realm.alias: realm,
|
|
||||||
};
|
|
||||||
|
|
||||||
final scopeToFetch = {'global', ...realms.map((e) => e.alias)};
|
|
||||||
|
|
||||||
final List<SnChannel> result = List.empty(growable: true);
|
final List<SnChannel> result = List.empty(growable: true);
|
||||||
final directMessages = await _fetchChannelsFromServer(
|
final channels = await _fetchChannelsFromServer();
|
||||||
scope: scopeToFetch.first,
|
for (var idx = 0; idx < channels.length; idx++) {
|
||||||
direct: true,
|
final channel = channels[idx];
|
||||||
|
if (channel.realmId != null) {
|
||||||
|
channels[idx] = channels[idx].copyWith(
|
||||||
|
realm: await _rels.getRealm(channel.realmId!),
|
||||||
);
|
);
|
||||||
result.addAll(directMessages);
|
|
||||||
|
|
||||||
final nonBelongsChannels = await _fetchChannelsFromServer(
|
|
||||||
scope: scopeToFetch.first,
|
|
||||||
direct: false,
|
|
||||||
);
|
|
||||||
result.addAll(nonBelongsChannels);
|
|
||||||
|
|
||||||
for (final scope in scopeToFetch.skip(1)) {
|
|
||||||
final channel = await _fetchChannelsFromServer(
|
|
||||||
scope: scope,
|
|
||||||
direct: false,
|
|
||||||
doNotSave: true,
|
|
||||||
);
|
|
||||||
final out = channel.map((ele) => ele.copyWith(realm: realmMap[scope]));
|
|
||||||
_saveChannelToLocal(out);
|
|
||||||
result.addAll(out);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
result.addAll(channels);
|
||||||
|
|
||||||
yield result;
|
yield result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
: false;
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
|
if (newDrawerIsExpanded != drawerIsExpanded ||
|
||||||
|
newDrawerIsCollapsed != drawerIsCollapsed) {
|
||||||
drawerIsExpanded = newDrawerIsExpanded;
|
drawerIsExpanded = newDrawerIsExpanded;
|
||||||
drawerIsCollapsed = newDrawerIsCollapsed;
|
drawerIsCollapsed = newDrawerIsCollapsed;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -66,7 +67,9 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FilterQuality get imageQuality {
|
FilterQuality get imageQuality {
|
||||||
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
|
return kImageQualityLevel.values
|
||||||
|
.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ??
|
||||||
|
FilterQuality.high;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get serverUrl {
|
String get serverUrl {
|
||||||
@@ -76,6 +79,7 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
bool get realmCompactView {
|
bool get realmCompactView {
|
||||||
return prefs.getBool(kAppRealmCompactView) ?? false;
|
return prefs.getBool(kAppRealmCompactView) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
set realmCompactView(bool value) {
|
set realmCompactView(bool value) {
|
||||||
prefs.setBool(kAppRealmCompactView, value);
|
prefs.setBool(kAppRealmCompactView, value);
|
||||||
}
|
}
|
||||||
@@ -86,9 +90,11 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? updatableVersion;
|
String? updatableVersion;
|
||||||
|
String? updatableChangelog;
|
||||||
|
|
||||||
void setUpdate(String newVersion) {
|
void setUpdate(String newVersion, String newChangelog) {
|
||||||
updatableVersion = newVersion;
|
updatableVersion = newVersion;
|
||||||
|
updatableChangelog = newChangelog;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
screen: 'news',
|
screen: 'news',
|
||||||
label: 'screenNews',
|
label: 'screenNews',
|
||||||
),
|
),
|
||||||
|
AppNavDestination(
|
||||||
|
icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20),
|
||||||
|
screen: 'stickers',
|
||||||
|
label: 'screenStickers',
|
||||||
|
),
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
||||||
screen: 'album',
|
screen: 'album',
|
||||||
@@ -88,7 +93,8 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
List<AppNavDestination> destinations = [];
|
List<AppNavDestination> destinations = [];
|
||||||
|
|
||||||
int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
|
int get pinnedDestinationCount =>
|
||||||
|
destinations.where((ele) => ele.isPinned).length;
|
||||||
|
|
||||||
NavigationProvider() {
|
NavigationProvider() {
|
||||||
buildDestinations(kDefaultPinnedDestination);
|
buildDestinations(kDefaultPinnedDestination);
|
||||||
@@ -117,13 +123,17 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isIndexInRange(int min, int max) {
|
bool isIndexInRange(int min, int max) {
|
||||||
return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
|
return _currentIndex != null &&
|
||||||
|
_currentIndex! >= min &&
|
||||||
|
_currentIndex! < max;
|
||||||
}
|
}
|
||||||
|
|
||||||
void autoDetectIndex(GoRouter? state) {
|
void autoDetectIndex(GoRouter? state) {
|
||||||
if (state == null) return;
|
if (state == null) return;
|
||||||
final idx = destinations.indexWhere(
|
final idx = destinations.indexWhere(
|
||||||
(ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
|
(ele) =>
|
||||||
|
ele.screen ==
|
||||||
|
state.routerDelegate.currentConfiguration.last.route.name,
|
||||||
);
|
);
|
||||||
_currentIndex = idx == -1 ? null : idx;
|
_currentIndex = idx == -1 ? null : idx;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_udid/flutter_udid.dart';
|
import 'package:flutter_udid/flutter_udid.dart';
|
||||||
|
import 'package:local_notifier/local_notifier.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
@@ -76,10 +78,25 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
int showingTrayCount = 0;
|
int showingTrayCount = 0;
|
||||||
List<SnNotification> notifications = List.empty(growable: true);
|
List<SnNotification> notifications = List.empty(growable: true);
|
||||||
|
|
||||||
|
int? skippableNotifyChannel;
|
||||||
|
bool isMuted = false;
|
||||||
|
|
||||||
void listen() {
|
void listen() {
|
||||||
_ws.pk.stream.listen((event) {
|
_ws.pk.stream.listen((event) {
|
||||||
if (event.method == 'notifications.new') {
|
if (event.method == 'notifications.new') {
|
||||||
final notification = SnNotification.fromJson(event.payload!);
|
final notification = SnNotification.fromJson(event.payload!);
|
||||||
|
|
||||||
|
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||||
|
if (doHaptic) HapticFeedback.mediumImpact();
|
||||||
|
|
||||||
|
if (notification.topic == 'messaging.message' &&
|
||||||
|
skippableNotifyChannel != null) {
|
||||||
|
if (notification.metadata['channel_id'] != null &&
|
||||||
|
notification.metadata['channel_id'] == skippableNotifyChannel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showingCount < 0) showingCount = 0;
|
if (showingCount < 0) showingCount = 0;
|
||||||
showingCount++;
|
showingCount++;
|
||||||
showingTrayCount++;
|
showingTrayCount++;
|
||||||
@@ -90,8 +107,20 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
updateTray();
|
updateTray();
|
||||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
|
||||||
if (doHaptic) HapticFeedback.mediumImpact();
|
if (!kIsWeb && !isMuted) {
|
||||||
|
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||||
|
LocalNotification notify = LocalNotification(
|
||||||
|
title: notification.title,
|
||||||
|
subtitle: notification.subtitle,
|
||||||
|
body: notification.body,
|
||||||
|
);
|
||||||
|
notify.onClick = () {
|
||||||
|
appWindow.show();
|
||||||
|
};
|
||||||
|
notify.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ class SnAttachmentProvider {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SnAttachment?>> getMultiple(List<String> rids, {noCache = false}) async {
|
Future<List<SnAttachment?>> getMultiple(List<String> rids,
|
||||||
|
{noCache = false}) async {
|
||||||
final result = List<SnAttachment?>.filled(rids.length, null);
|
final result = List<SnAttachment?>.filled(rids.length, null);
|
||||||
final Map<String, int> randomMapping = {};
|
final Map<String, int> randomMapping = {};
|
||||||
for (int i = 0; i < rids.length; i++) {
|
for (int i = 0; i < rids.length; i++) {
|
||||||
@@ -62,8 +63,10 @@ class SnAttachmentProvider {
|
|||||||
'id': pendingFetch.join(','),
|
'id': pendingFetch.join(','),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
final List<SnAttachment?> out =
|
final List<SnAttachment?> out = resp.data['data']
|
||||||
resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).cast<SnAttachment?>().toList();
|
.map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e))
|
||||||
|
.cast<SnAttachment?>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
for (final item in out) {
|
for (final item in out) {
|
||||||
if (item == null) continue;
|
if (item == null) continue;
|
||||||
@@ -77,7 +80,13 @@ class SnAttachmentProvider {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, String> mimetypeOverrides = {'mov': 'video/quicktime', 'mp4': 'video/mp4'};
|
static Map<String, String> mimetypeOverrides = {
|
||||||
|
'mov': 'video/quicktime',
|
||||||
|
'mp4': 'video/mp4',
|
||||||
|
'm4a': 'audio/mp4',
|
||||||
|
'apng': 'image/apng',
|
||||||
|
'webp': 'image/webp',
|
||||||
|
};
|
||||||
|
|
||||||
Future<SnAttachment> directUploadOne(
|
Future<SnAttachment> directUploadOne(
|
||||||
Uint8List data,
|
Uint8List data,
|
||||||
@@ -89,8 +98,11 @@ class SnAttachmentProvider {
|
|||||||
bool analyzeNow = false,
|
bool analyzeNow = false,
|
||||||
}) async {
|
}) async {
|
||||||
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
||||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
final fileAlt = filename.contains('.')
|
||||||
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
? filename.substring(0, filename.lastIndexOf('.'))
|
||||||
|
: filename;
|
||||||
|
final fileExt =
|
||||||
|
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
|
||||||
String? mimetypeOverride;
|
String? mimetypeOverride;
|
||||||
if (mimetype != null) {
|
if (mimetype != null) {
|
||||||
@@ -127,8 +139,11 @@ class SnAttachmentProvider {
|
|||||||
Map<String, dynamic>? metadata, {
|
Map<String, dynamic>? metadata, {
|
||||||
String? mimetype,
|
String? mimetype,
|
||||||
}) async {
|
}) async {
|
||||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
final fileAlt = filename.contains('.')
|
||||||
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
? filename.substring(0, filename.lastIndexOf('.'))
|
||||||
|
: filename;
|
||||||
|
final fileExt =
|
||||||
|
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
|
||||||
String? mimetypeOverride;
|
String? mimetypeOverride;
|
||||||
if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) {
|
if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) {
|
||||||
@@ -146,7 +161,10 @@ class SnAttachmentProvider {
|
|||||||
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (SnAttachmentFragment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int);
|
return (
|
||||||
|
SnAttachmentFragment.fromJson(resp.data['meta']),
|
||||||
|
resp.data['chunk_size'] as int
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> _chunkedUploadOnePart(
|
Future<dynamic> _chunkedUploadOnePart(
|
||||||
@@ -197,7 +215,10 @@ class SnAttachmentProvider {
|
|||||||
(entry.value + 1) * chunkSize,
|
(entry.value + 1) * chunkSize,
|
||||||
await file.length(),
|
await file.length(),
|
||||||
);
|
);
|
||||||
final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList());
|
final data = Uint8List.fromList(await file
|
||||||
|
.openRead(beginCursor, endCursor)
|
||||||
|
.expand((chunk) => chunk)
|
||||||
|
.toList());
|
||||||
|
|
||||||
final result = await _chunkedUploadOnePart(
|
final result = await _chunkedUploadOnePart(
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ class SnStickerProvider {
|
|||||||
|
|
||||||
final Map<int, List<SnSticker>> stickersByPack = {};
|
final Map<int, List<SnSticker>> stickersByPack = {};
|
||||||
|
|
||||||
List<SnSticker> get stickers => _cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
|
List<SnSticker> get stickers =>
|
||||||
|
_cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
|
||||||
|
|
||||||
SnStickerProvider(BuildContext context) {
|
SnStickerProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
@@ -23,8 +24,18 @@ class SnStickerProvider {
|
|||||||
|
|
||||||
void _cacheSticker(SnSticker sticker) {
|
void _cacheSticker(SnSticker sticker) {
|
||||||
_cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker;
|
_cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker;
|
||||||
if (stickersByPack[sticker.pack.id] == null) stickersByPack[sticker.pack.id] = List.empty(growable: true);
|
if (stickersByPack[sticker.pack.id] == null) {
|
||||||
if (!stickersByPack[sticker.pack.id]!.contains(sticker)) stickersByPack[sticker.pack.id]!.add(sticker);
|
stickersByPack[sticker.pack.id] = List.empty(growable: true);
|
||||||
|
}
|
||||||
|
if (!stickersByPack[sticker.pack.id]!.contains(sticker)) {
|
||||||
|
stickersByPack[sticker.pack.id]!.add(sticker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void putSticker(Iterable<SnSticker> sticker) {
|
||||||
|
for (final ele in sticker) {
|
||||||
|
_cacheSticker(ele);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnSticker?> lookupSticker(String alias) async {
|
Future<SnSticker?> lookupSticker(String alias) async {
|
||||||
@@ -46,26 +57,14 @@ class SnStickerProvider {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> listStickerEagerly() async {
|
Future<void> listSticker() async {
|
||||||
var count = await listSticker();
|
|
||||||
for (var page = 1; count > 0; count -= 10) {
|
|
||||||
await listSticker(page: page);
|
|
||||||
page++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> listSticker({int page = 0}) async {
|
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client.get('/cgi/uc/stickers', queryParameters: {
|
final resp = await _sn.client.get('/cgi/uc/stickers');
|
||||||
'take': 10,
|
|
||||||
'offset': page * 10,
|
|
||||||
});
|
|
||||||
final data = resp.data;
|
final data = resp.data;
|
||||||
final stickers = List.from(data['data']).map((ele) => SnSticker.fromJson(ele));
|
final stickers = List.from(data).map((ele) => SnSticker.fromJson(ele));
|
||||||
for (final sticker in stickers) {
|
for (final sticker in stickers) {
|
||||||
_cacheSticker(sticker);
|
_cacheSticker(sticker);
|
||||||
}
|
}
|
||||||
return data['count'] as int;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('[Sticker] Failed to list stickers: $err');
|
log('[Sticker] Failed to list stickers: $err');
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|||||||
@@ -34,13 +34,15 @@ import 'package:surface/screens/realm/realm_detail.dart';
|
|||||||
import 'package:surface/screens/realm/realm_discovery.dart';
|
import 'package:surface/screens/realm/realm_discovery.dart';
|
||||||
import 'package:surface/screens/settings.dart';
|
import 'package:surface/screens/settings.dart';
|
||||||
import 'package:surface/screens/sharing.dart';
|
import 'package:surface/screens/sharing.dart';
|
||||||
|
import 'package:surface/screens/stickers.dart';
|
||||||
|
import 'package:surface/screens/stickers/pack_detail.dart';
|
||||||
import 'package:surface/screens/wallet.dart';
|
import 'package:surface/screens/wallet.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/about.dart';
|
import 'package:surface/widgets/about.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
Widget _fadeThroughTransition(
|
Widget _fadeThroughTransition(BuildContext context, Animation<double> animation,
|
||||||
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
Animation<double> secondaryAnimation, Widget child) {
|
||||||
return FadeThroughTransition(
|
return FadeThroughTransition(
|
||||||
animation: animation,
|
animation: animation,
|
||||||
secondaryAnimation: secondaryAnimation,
|
secondaryAnimation: secondaryAnimation,
|
||||||
@@ -82,13 +84,15 @@ final _appRoutes = [
|
|||||||
name: 'postSearch',
|
name: 'postSearch',
|
||||||
builder: (context, state) => PostSearchScreen(
|
builder: (context, state) => PostSearchScreen(
|
||||||
initialTags: state.uri.queryParameters['tags']?.split(','),
|
initialTags: state.uri.queryParameters['tags']?.split(','),
|
||||||
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
initialCategories:
|
||||||
|
state.uri.queryParameters['categories']?.split(','),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/publishers/:name',
|
path: '/publishers/:name',
|
||||||
name: 'postPublisher',
|
name: 'postPublisher',
|
||||||
builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
|
builder: (context, state) =>
|
||||||
|
PostPublisherScreen(name: state.pathParameters['name']!),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:slug',
|
path: '/:slug',
|
||||||
@@ -100,7 +104,11 @@ final _appRoutes = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
|
GoRoute(
|
||||||
|
path: '/account',
|
||||||
|
name: 'account',
|
||||||
|
builder: (context, state) => const AccountScreen(),
|
||||||
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/wallet',
|
path: '/wallet',
|
||||||
name: 'accountWallet',
|
name: 'accountWallet',
|
||||||
@@ -208,11 +216,16 @@ final _appRoutes = [
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:alias',
|
path: '/:alias',
|
||||||
name: 'realmDetail',
|
name: 'realmDetail',
|
||||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
builder: (context, state) =>
|
||||||
|
RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
|
GoRoute(
|
||||||
|
path: '/news',
|
||||||
|
name: 'news',
|
||||||
|
builder: (context, state) => const NewsScreen(),
|
||||||
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:hash',
|
path: '/:hash',
|
||||||
name: 'newsDetail',
|
name: 'newsDetail',
|
||||||
@@ -220,7 +233,22 @@ final _appRoutes = [
|
|||||||
hash: state.pathParameters['hash']!,
|
hash: state.pathParameters['hash']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/stickers',
|
||||||
|
name: 'stickers',
|
||||||
|
builder: (context, state) => const StickerScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/packs/:id',
|
||||||
|
name: 'stickerPack',
|
||||||
|
builder: (context, state) => StickerPackScreen(
|
||||||
|
id: int.tryParse(state.pathParameters['id']!)!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/album',
|
path: '/album',
|
||||||
name: 'album',
|
name: 'album',
|
||||||
|
|||||||
@@ -54,14 +54,20 @@ class AccountSettingsScreen extends StatelessWidget {
|
|||||||
child: DropdownButton2<Locale?>(
|
child: DropdownButton2<Locale?>(
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
items: [
|
items: [
|
||||||
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
...EasyLocalization.of(context)!
|
||||||
|
.supportedLocales
|
||||||
|
.mapIndexed((idx, ele) {
|
||||||
return DropdownMenuItem<Locale?>(
|
return DropdownMenuItem<Locale?>(
|
||||||
value: Locale.parse(ele.toString()),
|
value: Locale.parse(ele.toString()),
|
||||||
child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14),
|
child: Text('${ele.languageCode}-${ele.countryCode}')
|
||||||
|
.fontSize(14),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'),
|
value: ua.user?.language != null
|
||||||
|
? (Locale.tryParse(ua.user!.language) ??
|
||||||
|
Locale.parse('en-US'))
|
||||||
|
: Locale.parse('en-US'),
|
||||||
onChanged: (Locale? value) {
|
onChanged: (Locale? value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
_setAccountLanguage(context, value);
|
_setAccountLanguage(context, value);
|
||||||
|
|||||||
@@ -3,18 +3,22 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/screens/chat/room.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/account/account_select.dart';
|
import 'package:surface/widgets/account/account_select.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/loading_indicator.dart';
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_background.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -33,6 +37,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
List<SnChannel>? _channels;
|
List<SnChannel>? _channels;
|
||||||
Map<int, SnChatMessage>? _lastMessages;
|
Map<int, SnChatMessage>? _lastMessages;
|
||||||
|
Map<int, int>? _unreadCounts;
|
||||||
|
|
||||||
|
Future<void> _fetchWhatsNew() async {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/im/whats-new');
|
||||||
|
if (resp.data == null) return;
|
||||||
|
final List<dynamic> out = resp.data;
|
||||||
|
setState(() {
|
||||||
|
_unreadCounts = {for (var v in out) v['channel_id']: v['count']};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _refreshChannels({bool noRemote = false}) {
|
void _refreshChannels({bool noRemote = false}) {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
@@ -113,10 +128,35 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SnChannel? _focusChannel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_refreshChannels();
|
_refreshChannels();
|
||||||
|
_fetchWhatsNew();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapChannel(SnChannel channel) {
|
||||||
|
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
|
||||||
|
|
||||||
|
if (doExpand) {
|
||||||
|
setState(() => _focusChannel = channel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {
|
||||||
|
'scope': channel.realm?.alias ?? 'global',
|
||||||
|
'alias': channel.alias,
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
|
if (mounted) {
|
||||||
|
_unreadCounts?[channel.id] = 0;
|
||||||
|
setState(() => _unreadCounts?[channel.id] = 0);
|
||||||
|
_refreshChannels(noRemote: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -136,7 +176,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
|
||||||
|
|
||||||
|
final chatList = AppScaffold(
|
||||||
|
noBackground: doExpand,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenChat').tr(),
|
title: Text('screenChat').tr(),
|
||||||
@@ -211,7 +254,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
removeTop: true,
|
removeTop: true,
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
onRefresh: () => Future.wait([
|
||||||
|
Future.sync(() => _refreshChannels()),
|
||||||
|
_fetchWhatsNew(),
|
||||||
|
]),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: _channels?.length ?? 0,
|
itemCount: _channels?.length ?? 0,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
@@ -226,46 +272,23 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(ud
|
title: Row(
|
||||||
.getAccountFromCache(otherMember?.accountId)
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(ud
|
||||||
|
.getAccountFromCache(
|
||||||
|
otherMember?.accountId)
|
||||||
?.nick ??
|
?.nick ??
|
||||||
channel.name),
|
channel.name),
|
||||||
subtitle: lastMessage != null
|
|
||||||
? Text(
|
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
'channelDirectMessageDescription'.tr(args: [
|
|
||||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
|
||||||
]),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
contentPadding:
|
const Gap(8),
|
||||||
const EdgeInsets.symmetric(horizontal: 16),
|
if (_unreadCounts?[channel.id] != null &&
|
||||||
leading: AccountImage(
|
_unreadCounts![channel.id]! > 0)
|
||||||
content: ud
|
Badge(
|
||||||
.getAccountFromCache(otherMember?.accountId)
|
label: Text('${_unreadCounts![channel.id]}'),
|
||||||
?.avatar,
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'chatRoom',
|
|
||||||
pathParameters: {
|
|
||||||
'scope': channel.realm?.alias ?? 'global',
|
|
||||||
'alias': channel.alias,
|
|
||||||
},
|
|
||||||
).then((value) {
|
|
||||||
if (mounted) _refreshChannels(noRemote: true);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
title: Text(channel.name),
|
|
||||||
subtitle: lastMessage != null
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Text(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
@@ -280,19 +303,85 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 16),
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: null,
|
content: ud
|
||||||
|
.getAccountFromCache(otherMember?.accountId)
|
||||||
|
?.avatar,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_onTapChannel(channel);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(channel.name)),
|
||||||
|
const Gap(8),
|
||||||
|
if (_unreadCounts?[channel.id] != null &&
|
||||||
|
_unreadCounts![channel.id]! > 0)
|
||||||
|
Badge(
|
||||||
|
label: Text('${_unreadCounts![channel.id]}'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: lastMessage != null
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
Badge(
|
||||||
|
label: Text(ud
|
||||||
|
.getAccountFromCache(
|
||||||
|
lastMessage.sender.accountId)
|
||||||
|
?.nick ??
|
||||||
|
'unknown'.tr()),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
lastMessage.body['text'] ??
|
||||||
|
'Unable preview',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
DateFormat(
|
||||||
|
lastMessage.createdAt.toLocal().day ==
|
||||||
|
DateTime.now().day
|
||||||
|
? 'HH:mm'
|
||||||
|
: lastMessage.createdAt
|
||||||
|
.toLocal()
|
||||||
|
.year ==
|
||||||
|
DateTime.now().year
|
||||||
|
? 'MM/dd'
|
||||||
|
: 'yy/MM/dd',
|
||||||
|
).format(lastMessage.createdAt.toLocal()),
|
||||||
|
style: GoogleFonts.robotoMono(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
channel.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
leading: AccountImage(
|
||||||
|
content: channel.realm?.avatar,
|
||||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
if (doExpand) {
|
||||||
'chatRoom',
|
_unreadCounts?[channel.id] = 0;
|
||||||
pathParameters: {
|
setState(() => _focusChannel = channel);
|
||||||
'scope': channel.realm?.alias ?? 'global',
|
return;
|
||||||
'alias': channel.alias,
|
}
|
||||||
},
|
_onTapChannel(channel);
|
||||||
).then((value) {
|
|
||||||
if (value == true) _refreshChannels();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -303,5 +392,27 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (doExpand) {
|
||||||
|
return AppBackground(
|
||||||
|
isRoot: true,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(width: 340, child: chatList),
|
||||||
|
const VerticalDivider(width: 1),
|
||||||
|
if (_focusChannel != null)
|
||||||
|
Expanded(
|
||||||
|
child: ChatRoomScreen(
|
||||||
|
key: ValueKey(_focusChannel!.id),
|
||||||
|
scope: _focusChannel!.realm?.alias ?? 'global',
|
||||||
|
alias: _focusChannel!.alias,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/members/me');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/me');
|
||||||
_profile = SnChannelMember.fromJson(resp.data);
|
_profile = SnChannelMember.fromJson(resp.data);
|
||||||
_notifyLevel = _profile!.notify;
|
_notifyLevel = _profile!.notify;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -245,7 +246,11 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('channelDetailPersonalRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
Text('channelDetailPersonalRegion')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.notifications),
|
leading: const Icon(Symbols.notifications),
|
||||||
trailing: DropdownButtonHideUnderline(
|
trailing: DropdownButtonHideUnderline(
|
||||||
@@ -284,7 +289,8 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
content:
|
||||||
|
ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
||||||
radius: 18,
|
radius: 18,
|
||||||
),
|
),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -303,7 +309,8 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
title: Text('channelActionLeave').tr(),
|
title: Text('channelActionLeave').tr(),
|
||||||
subtitle: Text('channelActionLeaveDescription').tr(),
|
subtitle: Text('channelActionLeaveDescription').tr(),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
onTap: _leaveChannel,
|
onTap: _leaveChannel,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -311,7 +318,11 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('channelDetailMemberRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
Text('channelDetailMemberRegion')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.group),
|
leading: const Icon(Symbols.group),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -333,7 +344,11 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('channelDetailAdminRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
Text('channelDetailAdminRegion')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.edit),
|
leading: const Icon(Symbols.edit),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
@@ -379,10 +394,12 @@ class _ChannelProfileDetailDialog extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ChannelProfileDetailDialog> createState() => _ChannelProfileDetailDialogState();
|
State<_ChannelProfileDetailDialog> createState() =>
|
||||||
|
_ChannelProfileDetailDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelProfileDetailDialogState extends State<_ChannelProfileDetailDialog> {
|
class _ChannelProfileDetailDialogState
|
||||||
|
extends State<_ChannelProfileDetailDialog> {
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
|
|
||||||
final TextEditingController _nickController = TextEditingController();
|
final TextEditingController _nickController = TextEditingController();
|
||||||
@@ -457,7 +474,8 @@ class _ChannelMemberListWidget extends StatefulWidget {
|
|||||||
const _ChannelMemberListWidget({required this.channel});
|
const _ChannelMemberListWidget({required this.channel});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ChannelMemberListWidget> createState() => _ChannelMemberListWidgetState();
|
State<_ChannelMemberListWidget> createState() =>
|
||||||
|
_ChannelMemberListWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
||||||
@@ -472,7 +490,9 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
try {
|
try {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: {
|
final resp = await sn.client.get(
|
||||||
|
'/cgi/im/channels/${widget.channel.keyPath}/members',
|
||||||
|
queryParameters: {
|
||||||
'take': 10,
|
'take': 10,
|
||||||
'offset': _members.length,
|
'offset': _members.length,
|
||||||
});
|
});
|
||||||
@@ -533,7 +553,9 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.group, size: 24),
|
const Icon(Symbols.group, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('channelMemberManage').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
Text('channelMemberManage')
|
||||||
|
.tr()
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -544,7 +566,8 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
},
|
},
|
||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
itemCount: _members.length,
|
itemCount: _members.length,
|
||||||
hasReachedMax: _totalCount != null && _members.length >= _totalCount!,
|
hasReachedMax:
|
||||||
|
_totalCount != null && _members.length >= _totalCount!,
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
onFetchData: _fetchMembers,
|
onFetchData: _fetchMembers,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -555,7 +578,8 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
ud.getAccountFromCache(member.accountId)?.name ??
|
||||||
|
'unknown'.tr(),
|
||||||
),
|
),
|
||||||
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
@@ -565,7 +589,8 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _isUpdating ? null : () => _deleteMember(member),
|
onPressed:
|
||||||
|
_isUpdating ? null : () => _deleteMember(member),
|
||||||
icon: const Icon(Symbols.person_remove),
|
icon: const Icon(Symbols.person_remove),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:surface/controllers/chat_message_controller.dart';
|
|||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.dart';
|
import 'package:surface/providers/chat_call.dart';
|
||||||
|
import 'package:surface/providers/notification.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
@@ -84,6 +85,10 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
final nty = context.read<NotificationProvider>();
|
||||||
|
nty.skippableNotifyChannel = _channel!.id;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -232,6 +237,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_wsSubscription?.cancel();
|
_wsSubscription?.cancel();
|
||||||
_messageController.dispose();
|
_messageController.dispose();
|
||||||
|
final nty = context.read<NotificationProvider>();
|
||||||
|
nty.skippableNotifyChannel = null;
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_app_update/flutter_app_update.dart';
|
|
||||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -29,6 +26,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
|
|||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
|
import 'package:surface/widgets/updater.dart';
|
||||||
|
|
||||||
class HomeScreenDashEntry {
|
class HomeScreenDashEntry {
|
||||||
final String name;
|
final String name;
|
||||||
@@ -83,14 +81,24 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Align(
|
return Align(
|
||||||
alignment: constraints.maxWidth > 640 ? Alignment.center : Alignment.topCenter,
|
alignment: constraints.maxWidth > 640
|
||||||
|
? Alignment.center
|
||||||
|
: Alignment.topCenter,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start,
|
mainAxisAlignment: constraints.maxWidth > 640
|
||||||
|
? MainAxisAlignment.center
|
||||||
|
: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)),
|
_HomeDashUpdateWidget(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: 8,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
_HomeDashSpecialDayWidget().padding(horizontal: 8),
|
_HomeDashSpecialDayWidget().padding(horizontal: 8),
|
||||||
StaggeredGrid.extent(
|
StaggeredGrid.extent(
|
||||||
maxCrossAxisExtent: 280,
|
maxCrossAxisExtent: 280,
|
||||||
@@ -136,19 +144,13 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
|||||||
leading: Icon(Symbols.update),
|
leading: Icon(Symbols.update),
|
||||||
title: Text('updateAvailable').tr(),
|
title: Text('updateAvailable').tr(),
|
||||||
subtitle: Text(config.updatableVersion!),
|
subtitle: Text(config.updatableVersion!),
|
||||||
trailing: (kIsWeb || Platform.isWindows || Platform.isLinux)
|
trailing: IconButton(
|
||||||
? null
|
|
||||||
: IconButton(
|
|
||||||
icon: const Icon(Symbols.arrow_right_alt),
|
icon: const Icon(Symbols.arrow_right_alt),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final model = UpdateModel(
|
showModalBottomSheet(
|
||||||
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
|
context: context,
|
||||||
'solian-app-release-${config.updatableVersion!}.apk',
|
builder: (context) => VersionUpdatePopup(),
|
||||||
'ic_launcher',
|
|
||||||
'https://apps.apple.com/us/app/solian/id6499032345',
|
|
||||||
);
|
);
|
||||||
AzhonAppUpdate.update(model);
|
|
||||||
context.showSnackbar('updateOngoing'.tr());
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -166,7 +168,8 @@ class _HomeDashSpecialDayWidget extends StatefulWidget {
|
|||||||
const _HomeDashSpecialDayWidget();
|
const _HomeDashSpecialDayWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_HomeDashSpecialDayWidget> createState() => _HomeDashSpecialDayWidgetState();
|
State<_HomeDashSpecialDayWidget> createState() =>
|
||||||
|
_HomeDashSpecialDayWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||||
@@ -208,7 +211,9 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
|||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
||||||
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
|
title: Text('pending$name').tr(args: [
|
||||||
|
RelativeTime(context).format(date).replaceFirst('in', '').trim()
|
||||||
|
]),
|
||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -297,12 +302,19 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_article!.title,
|
_article!.title,
|
||||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
|
style: Theme.of(context)
|
||||||
maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1,
|
.textTheme
|
||||||
|
.titleMedium!
|
||||||
|
.copyWith(fontSize: 18),
|
||||||
|
maxLines:
|
||||||
|
MediaQuery.of(context).size.width >= 640 ? 2 : 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
parse(_article!.description).children.map((e) => e.text.trim()).join(),
|
parse(_article!.description)
|
||||||
|
.children
|
||||||
|
.map((e) => e.text.trim())
|
||||||
|
.join(),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
@@ -313,9 +325,13 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
spacing: 2,
|
spacing: 2,
|
||||||
children: [
|
children: [
|
||||||
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
Text(DateFormat().format(date)).textStyle(
|
||||||
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
|
Theme.of(context).textTheme.bodySmall!),
|
||||||
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
|
Text(' · ')
|
||||||
|
.textStyle(Theme.of(context).textTheme.bodySmall!)
|
||||||
|
.bold(),
|
||||||
|
Text(RelativeTime(context).format(date)).textStyle(
|
||||||
|
Theme.of(context).textTheme.bodySmall!),
|
||||||
],
|
],
|
||||||
).opacity(0.75);
|
).opacity(0.75);
|
||||||
}),
|
}),
|
||||||
@@ -386,15 +402,20 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetailChunk(int index, bool positive) {
|
Widget _buildDetailChunk(int index, bool positive) {
|
||||||
final prefix = positive ? 'dailyCheckPositiveHint' : 'dailyCheckNegativeHint';
|
final prefix =
|
||||||
final mod = positive ? kSuggestionPositiveHintCount : kSuggestionNegativeHintCount;
|
positive ? 'dailyCheckPositiveHint' : 'dailyCheckNegativeHint';
|
||||||
|
final mod =
|
||||||
|
positive ? kSuggestionPositiveHintCount : kSuggestionNegativeHintCount;
|
||||||
final pos = math.max(1, _todayRecord!.resultModifiers[index] % mod);
|
final pos = math.max(1, _todayRecord!.resultModifiers[index] % mod);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
prefix.tr(args: ['$prefix$pos'.tr()]),
|
prefix.tr(args: ['$prefix$pos'.tr()]),
|
||||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium!
|
||||||
|
.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'$prefix${pos}Description',
|
'$prefix${pos}Description',
|
||||||
@@ -429,7 +450,10 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
else
|
else
|
||||||
Text(
|
Text(
|
||||||
'dailyCheckEverythingIsNegative',
|
'dailyCheckEverythingIsNegative',
|
||||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium!
|
||||||
|
.copyWith(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
).tr(),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (_todayRecord?.resultTier != 4)
|
if (_todayRecord?.resultTier != 4)
|
||||||
@@ -445,7 +469,10 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
else
|
else
|
||||||
Text(
|
Text(
|
||||||
'dailyCheckEverythingIsPositive',
|
'dailyCheckEverythingIsPositive',
|
||||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium!
|
||||||
|
.copyWith(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -571,10 +598,12 @@ class _HomeDashNotificationWidget extends StatefulWidget {
|
|||||||
const _HomeDashNotificationWidget();
|
const _HomeDashNotificationWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_HomeDashNotificationWidget> createState() => _HomeDashNotificationWidgetState();
|
State<_HomeDashNotificationWidget> createState() =>
|
||||||
|
_HomeDashNotificationWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget> {
|
class _HomeDashNotificationWidgetState
|
||||||
|
extends State<_HomeDashNotificationWidget> {
|
||||||
int? _count;
|
int? _count;
|
||||||
|
|
||||||
Future<void> _fetchNotificationCount() async {
|
Future<void> _fetchNotificationCount() async {
|
||||||
@@ -612,7 +641,9 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget
|
|||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
).tr(),
|
).tr(),
|
||||||
Text(
|
Text(
|
||||||
_count == null ? 'loading'.tr() : 'notificationUnreadCount'.plural(_count ?? 0),
|
_count == null
|
||||||
|
? 'loading'.tr()
|
||||||
|
: 'notificationUnreadCount'.plural(_count ?? 0),
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -643,10 +674,12 @@ class _HomeDashRecommendationPostWidget extends StatefulWidget {
|
|||||||
const _HomeDashRecommendationPostWidget();
|
const _HomeDashRecommendationPostWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_HomeDashRecommendationPostWidget> createState() => _HomeDashRecommendationPostWidgetState();
|
State<_HomeDashRecommendationPostWidget> createState() =>
|
||||||
|
_HomeDashRecommendationPostWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendationPostWidget> {
|
class _HomeDashRecommendationPostWidgetState
|
||||||
|
extends State<_HomeDashRecommendationPostWidget> {
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
List<SnPost>? _posts;
|
List<SnPost>? _posts;
|
||||||
|
|
||||||
@@ -710,13 +743,15 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
|
|||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text('${_currentPage + 1}/${_posts?.length ?? 0}', style: GoogleFonts.robotoMono())
|
Text('${_currentPage + 1}/${_posts?.length ?? 0}',
|
||||||
|
style: GoogleFonts.robotoMono())
|
||||||
],
|
],
|
||||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
|
scrollBehavior:
|
||||||
|
ScrollConfiguration.of(context).copyWith(dragDevices: {
|
||||||
PointerDeviceKind.mouse,
|
PointerDeviceKind.mouse,
|
||||||
PointerDeviceKind.touch,
|
PointerDeviceKind.touch,
|
||||||
}),
|
}),
|
||||||
@@ -729,7 +764,8 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
|
|||||||
showMenu: false,
|
showMenu: false,
|
||||||
).padding(bottom: 8),
|
).padding(bottom: 8),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed('postDetail', pathParameters: {
|
GoRouter.of(context)
|
||||||
|
.pushNamed('postDetail', pathParameters: {
|
||||||
'slug': _posts![index].id.toString(),
|
'slug': _posts![index].id.toString(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ import 'package:surface/providers/config.dart';
|
|||||||
import 'package:surface/providers/database.dart';
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/notification.dart';
|
import 'package:surface/providers/notification.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/sn_sticker.dart';
|
||||||
import 'package:surface/providers/theme.dart';
|
import 'package:surface/providers/theme.dart';
|
||||||
import 'package:surface/theme.dart';
|
import 'package:surface/theme.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:surface/widgets/updater.dart';
|
||||||
|
|
||||||
const Map<String, Color> kColorSchemes = {
|
const Map<String, Color> kColorSchemes = {
|
||||||
'colorSchemeIndigo': Colors.indigo,
|
'colorSchemeIndigo': Colors.indigo,
|
||||||
@@ -570,9 +572,50 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Text('settingsEnablePushNotificationsDescription').tr(),
|
Text('settingsEnablePushNotificationsDescription').tr(),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
final nty = context.read<NotificationProvider>();
|
final nty = context.read<NotificationProvider>();
|
||||||
nty.registerPushNotifications();
|
try {
|
||||||
|
await nty.registerPushNotifications();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
context.showSnackbar(
|
||||||
|
'settingsEnabledPushNotifications'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.refresh),
|
||||||
|
title: Text('stickersReload').tr(),
|
||||||
|
subtitle: Text('stickersReloadDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
final stickers = context.read<SnStickerProvider>();
|
||||||
|
try {
|
||||||
|
await stickers.listSticker();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
context.showSnackbar('stickersReloaded'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('forceUpdate').tr(),
|
||||||
|
subtitle: Text('forceUpdateDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.update),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => VersionUpdatePopup(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
464
lib/screens/stickers.dart
Normal file
464
lib/screens/stickers.dart
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/sn_sticker.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
|
class StickerScreen extends StatefulWidget {
|
||||||
|
const StickerScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StickerScreen> createState() => _StickerScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StickerScreenState extends State<StickerScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final TabController _tabController =
|
||||||
|
TabController(length: 3, vsync: this);
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
int? _totalCount;
|
||||||
|
final List<SnStickerPack> _packs = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> _fetchPacks() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
final resp = await sn.client.get(
|
||||||
|
_tabController.index == 1
|
||||||
|
? '/cgi/uc/stickers/packs/own'
|
||||||
|
: '/cgi/uc/stickers/packs',
|
||||||
|
queryParameters: {
|
||||||
|
'take': 10,
|
||||||
|
'offset': _packs.length,
|
||||||
|
if (_tabController.index == 2) 'author': ua.user?.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (resp.data is Map<String, dynamic>) {
|
||||||
|
_totalCount = resp.data['count'] as int?;
|
||||||
|
final out = List<SnStickerPack>.from(
|
||||||
|
resp.data['data'].map((ele) => SnStickerPack.fromJson(ele)),
|
||||||
|
);
|
||||||
|
_packs.addAll(out);
|
||||||
|
} else {
|
||||||
|
_totalCount = 0;
|
||||||
|
final out = List<SnStickerPack>.from(
|
||||||
|
resp.data.map((ele) => SnStickerPack.fromJson(ele)),
|
||||||
|
);
|
||||||
|
_packs.addAll(out);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _removePack(SnStickerPack pack) async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.delete('/cgi/uc/stickers/packs/${pack.id}/own');
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('stickersRemoved'.tr());
|
||||||
|
_refreshPacks();
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deletePack(SnStickerPack pack) async {
|
||||||
|
final confirm = await context.showConfirmDialog(
|
||||||
|
'stickersPackDelete'.tr(args: [pack.name]),
|
||||||
|
'stickersPackDeleteDescription'.tr(),
|
||||||
|
);
|
||||||
|
if (!confirm) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.delete('/cgi/uc/stickers/packs/${pack.id}');
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('stickersDeleted'.tr());
|
||||||
|
_refreshPacks();
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshPacks() async {
|
||||||
|
_packs.clear();
|
||||||
|
_totalCount = null;
|
||||||
|
await _fetchPacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchPacks();
|
||||||
|
_tabController.addListener(() {
|
||||||
|
if (_tabController.indexIsChanging) {
|
||||||
|
_refreshPacks();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AutoAppBarLeading(),
|
||||||
|
title: Text('screenStickers').tr(),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.add_circle),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _StickerPackCreateDialog(),
|
||||||
|
).then((value) {
|
||||||
|
if (value == true) _refreshPacks();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Text('stickersDiscovery'.tr()).textColor(
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text('stickersOwned'.tr()).textColor(
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text('stickersCreated'.tr()).textColor(
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: _refreshPacks,
|
||||||
|
child: InfiniteList(
|
||||||
|
itemCount: _packs.length,
|
||||||
|
onFetchData: _fetchPacks,
|
||||||
|
hasReachedMax: _totalCount != null && _packs.length >= _totalCount!,
|
||||||
|
isLoading: _isBusy,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final pack = _packs[idx];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(pack.name),
|
||||||
|
subtitle: Text(
|
||||||
|
pack.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
trailing: _tabController.index == 1
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_removePack(pack);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.remove),
|
||||||
|
)
|
||||||
|
: _tabController.index == 2
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_deletePack(pack);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.delete),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: () {
|
||||||
|
if (_tabController.index == 0) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _StickerPackAddPopup(pack: pack),
|
||||||
|
).then((value) {
|
||||||
|
if (value == true && _tabController.index == 1) {
|
||||||
|
_refreshPacks();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'stickerPack',
|
||||||
|
pathParameters: {
|
||||||
|
'id': pack.id.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StickerPackAddPopup extends StatefulWidget {
|
||||||
|
final SnStickerPack pack;
|
||||||
|
const _StickerPackAddPopup({required this.pack});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_StickerPackAddPopup> createState() => _StickerPackAddPopupState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StickerPackAddPopupState extends State<_StickerPackAddPopup> {
|
||||||
|
SnStickerPack? _pack;
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _fetchPack() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp =
|
||||||
|
await sn.client.get('/cgi/uc/stickers/packs/${widget.pack.id}');
|
||||||
|
_pack = SnStickerPack.fromJson(resp.data);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchPack();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isAdding = false;
|
||||||
|
|
||||||
|
Future<void> _addPack() async {
|
||||||
|
if (_pack == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() => _isAdding = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final stickers = context.read<SnStickerProvider>();
|
||||||
|
await sn.client.post(
|
||||||
|
'/cgi/uc/stickers/packs/${widget.pack.id}/own',
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('stickersAdded'.tr());
|
||||||
|
if (_pack?.stickers != null) stickers.putSticker(_pack!.stickers!);
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isAdding = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.add, size: 24),
|
||||||
|
const Gap(16),
|
||||||
|
Text('stickersAdd', style: Theme.of(context).textTheme.titleLarge)
|
||||||
|
.tr(),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.pack.name).bold(),
|
||||||
|
Text(
|
||||||
|
widget.pack.description,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isAdding ? null : _addPack,
|
||||||
|
child: Text('add').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24),
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
if (_pack?.stickers != null)
|
||||||
|
Expanded(
|
||||||
|
child: GridView.extent(
|
||||||
|
padding: EdgeInsets.only(left: 20, right: 20, top: 8),
|
||||||
|
maxCrossAxisExtent: 48,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
children: _pack!.stickers!
|
||||||
|
.map(
|
||||||
|
(ele) => ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: ele.attachment,
|
||||||
|
heroTag: 'sticker-pack-${ele.attachment.rid}',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StickerPackCreateDialog extends StatefulWidget {
|
||||||
|
const _StickerPackCreateDialog();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_StickerPackCreateDialog> createState() =>
|
||||||
|
_StickerPackCreateDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StickerPackCreateDialogState extends State<_StickerPackCreateDialog> {
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
final TextEditingController _prefixController = TextEditingController();
|
||||||
|
final TextEditingController _descriptionController = TextEditingController();
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _createPack() async {
|
||||||
|
if (_nameController.text.isEmpty ||
|
||||||
|
_prefixController.text.isEmpty ||
|
||||||
|
_descriptionController.text.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post(
|
||||||
|
'/cgi/uc/stickers/packs',
|
||||||
|
data: {
|
||||||
|
'name': _nameController.text,
|
||||||
|
'prefix': _prefixController.text,
|
||||||
|
'description': _descriptionController.text,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_prefixController.dispose();
|
||||||
|
_descriptionController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('stickersPackNew').tr(),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldStickerPackName'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextField(
|
||||||
|
controller: _prefixController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldStickerPackPrefix'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextField(
|
||||||
|
controller: _descriptionController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldStickerPackDescription'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('dialogDismiss').tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _createPack(),
|
||||||
|
child: Text('dialogConfirm').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
266
lib/screens/stickers/pack_detail.dart
Normal file
266
lib/screens/stickers/pack_detail.dart
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
|
import 'package:surface/widgets/attachment/attachment_input.dart';
|
||||||
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
|
class StickerPackScreen extends StatefulWidget {
|
||||||
|
final int id;
|
||||||
|
const StickerPackScreen({super.key, required this.id});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StickerPackScreen> createState() => _StickerPackScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StickerPackScreenState extends State<StickerPackScreen> {
|
||||||
|
SnStickerPack? _pack;
|
||||||
|
|
||||||
|
Future<void> _fetchPack() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/uc/stickers/packs/${widget.id}');
|
||||||
|
_pack = SnStickerPack.fromJson(resp.data);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _deleteSticker(SnSticker sticker) async {
|
||||||
|
final confirm = await context.showConfirmDialog(
|
||||||
|
'stickersDelete'.tr(args: [sticker.name]),
|
||||||
|
'stickersDeleteDescription'.tr(),
|
||||||
|
);
|
||||||
|
if (!confirm) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.delete('/cgi/uc/stickers/${sticker.id}');
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('stickersDeleted'.tr());
|
||||||
|
_fetchPack();
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchPack();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_pack?.name ?? 'loading'.tr()),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
if (_pack != null)
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(_pack!.name).bold(),
|
||||||
|
Text(
|
||||||
|
_pack!.description,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24, vertical: 16),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.add),
|
||||||
|
title: Text('stickersNew').tr(),
|
||||||
|
subtitle: Text('stickersNewDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _StickerCreateDialog(pack: _pack!),
|
||||||
|
).then((value) {
|
||||||
|
if (value) _fetchPack();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
if (_pack?.stickers != null)
|
||||||
|
Expanded(
|
||||||
|
child: GridView.extent(
|
||||||
|
padding: EdgeInsets.only(left: 20, right: 20, top: 16),
|
||||||
|
maxCrossAxisExtent: 48,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
children: _pack!.stickers!
|
||||||
|
.map(
|
||||||
|
(ele) => GestureDetector(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHigh,
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: ele.attachment,
|
||||||
|
heroTag: 'sticker-pack-${ele.attachment.rid}',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_deleteSticker(ele);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StickerCreateDialog extends StatefulWidget {
|
||||||
|
final SnStickerPack pack;
|
||||||
|
const _StickerCreateDialog({required this.pack});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_StickerCreateDialog> createState() => _StickerCreateDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StickerCreateDialogState extends State<_StickerCreateDialog> {
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
final TextEditingController _aliasController = TextEditingController();
|
||||||
|
final TextEditingController _attachmentController = TextEditingController();
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_aliasController.dispose();
|
||||||
|
_attachmentController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createSticker() async {
|
||||||
|
if (_nameController.text.isEmpty ||
|
||||||
|
_aliasController.text.isEmpty ||
|
||||||
|
_attachmentController.text.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post(
|
||||||
|
'/cgi/uc/stickers',
|
||||||
|
data: {
|
||||||
|
'name': _nameController.text,
|
||||||
|
'alias': _aliasController.text,
|
||||||
|
'attachment_id': _attachmentController.text,
|
||||||
|
'pack_id': widget.pack.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('stickersNew'.tr()),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldStickerName'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextField(
|
||||||
|
controller: _aliasController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldStickerAlias'.tr(),
|
||||||
|
helperText: 'fieldStickerAliasHint'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextField(
|
||||||
|
controller: _attachmentController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldStickerAttachment'.tr(),
|
||||||
|
),
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () async {
|
||||||
|
final attachment = await showDialog<SnAttachment?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AttachmentInputDialog(
|
||||||
|
title: 'fieldStickerAttachment'.tr(),
|
||||||
|
pool: 'sticker',
|
||||||
|
mediaType: SnMediaType.image,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (attachment != null) {
|
||||||
|
setState(() {
|
||||||
|
_attachmentController.text = attachment.rid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('dialogDismiss').tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _createSticker(),
|
||||||
|
child: Text('dialogConfirm').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,11 +109,13 @@ class ChatMessage extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
showPopover(
|
showPopover(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surface,
|
||||||
context: context,
|
context: context,
|
||||||
transition: PopoverTransition.other,
|
transition: PopoverTransition.other,
|
||||||
bodyBuilder: (context) => SizedBox(
|
bodyBuilder: (context) => SizedBox(
|
||||||
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
width: math.min(
|
||||||
|
400, MediaQuery.of(context).size.width - 10),
|
||||||
child: AccountPopoverCard(
|
child: AccountPopoverCard(
|
||||||
data: user,
|
data: user,
|
||||||
),
|
),
|
||||||
@@ -144,11 +146,14 @@ class ChatMessage extends StatelessWidget {
|
|||||||
radius: 12,
|
radius: 12,
|
||||||
).padding(right: 8),
|
).padding(right: 8),
|
||||||
Text(
|
Text(
|
||||||
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
(data.sender.nick?.isNotEmpty ?? false)
|
||||||
|
? data.sender.nick!
|
||||||
|
: user?.nick ?? 'unknown',
|
||||||
).bold(),
|
).bold(),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(
|
Text(
|
||||||
dateFormatter.format(data.createdAt.toLocal()),
|
dateFormatter
|
||||||
|
.format(data.createdAt.toLocal()),
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
],
|
],
|
||||||
).height(21),
|
).height(21),
|
||||||
@@ -156,10 +161,11 @@ class ChatMessage extends StatelessWidget {
|
|||||||
if (data.preload?.quoteEvent != null)
|
if (data.preload?.quoteEvent != null)
|
||||||
StyledWidget(Container(
|
StyledWidget(Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: 480,
|
maxWidth: 360,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(8)),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1,
|
width: 1,
|
||||||
@@ -204,12 +210,14 @@ class ChatMessage extends StatelessWidget {
|
|||||||
AttachmentList(
|
AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
maxHeight: 560,
|
maxHeight: 360,
|
||||||
maxWidth: 480,
|
maxWidth: 480 - 48 - padding.left,
|
||||||
minWidth: 480,
|
padding: padding.copyWith(top: 8, left: 48 + padding.left),
|
||||||
padding: padding.copyWith(top: 8),
|
|
||||||
),
|
),
|
||||||
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8),
|
if (!hasMerged && !isCompact)
|
||||||
|
const Gap(12)
|
||||||
|
else if (!isCompact)
|
||||||
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -223,7 +231,8 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
final Function(SnChatMessage)? onEdit;
|
final Function(SnChatMessage)? onEdit;
|
||||||
final Function(SnChatMessage)? onDelete;
|
final Function(SnChatMessage)? onDelete;
|
||||||
|
|
||||||
const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete});
|
const _ChatMessageText(
|
||||||
|
{required this.data, this.onReply, this.onEdit, this.onDelete});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -237,7 +246,8 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
SelectionArea(
|
SelectionArea(
|
||||||
contextMenuBuilder: (context, editableTextState) {
|
contextMenuBuilder: (context, editableTextState) {
|
||||||
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
|
final List<ContextMenuButtonItem> items =
|
||||||
|
editableTextState.contextMenuButtonItems;
|
||||||
|
|
||||||
if (onReply != null) {
|
if (onReply != null) {
|
||||||
items.insert(
|
items.insert(
|
||||||
@@ -281,12 +291,11 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
buttonItems: items,
|
buttonItems: items,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
|
||||||
child: MarkdownTextContent(
|
child: MarkdownTextContent(
|
||||||
content: data.body['text'],
|
content: data.body['text'],
|
||||||
isAutoWarp: true,
|
isAutoWarp: true,
|
||||||
),
|
isEnlargeSticker:
|
||||||
|
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (data.updatedAt != data.createdAt)
|
if (data.updatedAt != data.createdAt)
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
Theme.of(context),
|
Theme.of(context),
|
||||||
).copyWith(
|
).copyWith(
|
||||||
textScaler: textScaler,
|
textScaler: textScaler,
|
||||||
p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null,
|
p: textColor != null
|
||||||
|
? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor)
|
||||||
|
: null,
|
||||||
blockquote: TextStyle(
|
blockquote: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -115,7 +117,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
final alias = segments[1];
|
final alias = segments[1];
|
||||||
final st = context.read<SnStickerProvider>();
|
final st = context.read<SnStickerProvider>();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final double size = isEnlargeSticker ? 128 : 32;
|
final double size = isEnlargeSticker ? 96 : 32;
|
||||||
return Container(
|
return Container(
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
@@ -131,7 +133,8 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
|
sn.getAttachmentUrl(
|
||||||
|
snapshot.data!.attachment.rid),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
@@ -177,7 +180,9 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: attachment.metadata['ratio'] ??
|
aspectRatio: attachment.metadata['ratio'] ??
|
||||||
switch (attachment.mimetype.split('/').firstOrNull) {
|
switch (attachment.mimetype
|
||||||
|
.split('/')
|
||||||
|
.firstOrNull) {
|
||||||
'audio' => 16 / 9,
|
'audio' => 16 / 9,
|
||||||
'video' => 16 / 9,
|
'video' => 16 / 9,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class AppScaffold extends StatelessWidget {
|
|||||||
final AppBar? appBar;
|
final AppBar? appBar;
|
||||||
final DrawerCallback? onDrawerChanged;
|
final DrawerCallback? onDrawerChanged;
|
||||||
final DrawerCallback? onEndDrawerChanged;
|
final DrawerCallback? onEndDrawerChanged;
|
||||||
|
final bool noBackground;
|
||||||
|
|
||||||
const AppScaffold({
|
const AppScaffold({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -45,6 +46,7 @@ class AppScaffold extends StatelessWidget {
|
|||||||
this.endDrawer,
|
this.endDrawer,
|
||||||
this.onDrawerChanged,
|
this.onDrawerChanged,
|
||||||
this.onEndDrawerChanged,
|
this.onEndDrawerChanged,
|
||||||
|
this.noBackground = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -52,22 +54,23 @@ class AppScaffold extends StatelessWidget {
|
|||||||
final appBarHeight = appBar?.preferredSize.height ?? 0;
|
final appBarHeight = appBar?.preferredSize.height ?? 0;
|
||||||
final safeTop = MediaQuery.of(context).padding.top;
|
final safeTop = MediaQuery.of(context).padding.top;
|
||||||
|
|
||||||
|
final content = Column(
|
||||||
|
children: [
|
||||||
|
IgnorePointer(
|
||||||
|
child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0),
|
||||||
|
),
|
||||||
|
if (body != null) Expanded(child: body!),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: SizedBox.expand(
|
body: SizedBox.expand(
|
||||||
child: AppBackground(
|
child: noBackground
|
||||||
isRoot: true,
|
? content
|
||||||
child: Column(
|
: AppBackground(isRoot: true, child: content),
|
||||||
children: [
|
|
||||||
IgnorePointer(
|
|
||||||
child: SizedBox(
|
|
||||||
height: appBar != null ? appBarHeight + safeTop : 0)),
|
|
||||||
if (body != null) Expanded(child: body!),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
bottomNavigationBar: bottomNavigationBar,
|
bottomNavigationBar: bottomNavigationBar,
|
||||||
@@ -185,29 +188,19 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'Solar Network',
|
'Solar Network',
|
||||||
style: GoogleFonts.spaceGrotesk(),
|
style: GoogleFonts.spaceGrotesk(),
|
||||||
textAlign: !kIsWeb
|
textAlign: Platform.isMacOS
|
||||||
? Platform.isMacOS
|
|
||||||
? TextAlign.center
|
? TextAlign.center
|
||||||
: null
|
: TextAlign.start,
|
||||||
: null,
|
|
||||||
).padding(horizontal: 12, vertical: 5),
|
).padding(horizontal: 12, vertical: 5),
|
||||||
),
|
),
|
||||||
if (!Platform.isMacOS)
|
if (!Platform.isMacOS)
|
||||||
Row(
|
MinimizeWindowButton(colors: windowButtonColor),
|
||||||
mainAxisSize: MainAxisSize.min,
|
if (!Platform.isMacOS)
|
||||||
children: [
|
MaximizeWindowButton(colors: windowButtonColor),
|
||||||
Expanded(child: MoveWindow()),
|
if (!Platform.isMacOS)
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
MinimizeWindowButton(
|
|
||||||
colors: windowButtonColor),
|
|
||||||
MaximizeWindowButton(
|
|
||||||
colors: windowButtonColor),
|
|
||||||
CloseWindowButton(
|
CloseWindowButton(
|
||||||
colors: windowButtonColor),
|
colors: windowButtonColor,
|
||||||
],
|
onPressed: () => appWindow.hide(),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -226,13 +219,15 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
bottom: safeBottom > 0 ? safeBottom : 16,
|
bottom: safeBottom > 0 ? safeBottom : 16,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: ConnectionIndicator())
|
child: ConnectionIndicator(),
|
||||||
|
)
|
||||||
else
|
else
|
||||||
Positioned(
|
Positioned(
|
||||||
top: safeTop > 0 ? safeTop : 16,
|
top: safeTop > 0 ? safeTop : 16,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: ConnectionIndicator()),
|
child: ConnectionIndicator(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ class OpenablePostItem extends StatelessWidget {
|
|||||||
openColor: Colors.transparent,
|
openColor: Colors.transparent,
|
||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
transitionType: ContainerTransitionType.fade,
|
transitionType: ContainerTransitionType.fade,
|
||||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
closedColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
||||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
||||||
),
|
),
|
||||||
closedShape: const RoundedRectangleBorder(
|
closedShape: const RoundedRectangleBorder(
|
||||||
@@ -135,9 +136,11 @@ class PostItem extends StatelessWidget {
|
|||||||
final box = context.findRenderObject() as RenderBox?;
|
final box = context.findRenderObject() as RenderBox?;
|
||||||
final url = 'https://solsynth.dev/posts/${data.id}';
|
final url = 'https://solsynth.dev/posts/${data.id}';
|
||||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||||
Share.shareUri(Uri.parse(url), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
Share.shareUri(Uri.parse(url),
|
||||||
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
||||||
} else {
|
} else {
|
||||||
Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
Share.share(url,
|
||||||
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +158,8 @@ class PostItem extends StatelessWidget {
|
|||||||
child: MultiProvider(
|
child: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
||||||
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
|
ChangeNotifierProvider<ConfigProvider>(
|
||||||
|
create: (_) => context.read()),
|
||||||
],
|
],
|
||||||
child: ResponsiveBreakpoints.builder(
|
child: ResponsiveBreakpoints.builder(
|
||||||
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
|
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
|
||||||
@@ -183,7 +187,8 @@ class PostItem extends StatelessWidget {
|
|||||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile);
|
await FileSaver.instance.saveFile(
|
||||||
|
name: 'Solar Network Post #${data.id}.png', file: imageFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
await imageFile.delete();
|
await imageFile.delete();
|
||||||
@@ -197,7 +202,9 @@ class PostItem extends StatelessWidget {
|
|||||||
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
||||||
|
|
||||||
// Video full view
|
// Video full view
|
||||||
if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
|
if (showFullPost &&
|
||||||
|
data.type == 'video' &&
|
||||||
|
ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -217,7 +224,8 @@ class PostItem extends StatelessWidget {
|
|||||||
if (onDeleted != null) {}
|
if (onDeleted != null) {}
|
||||||
},
|
},
|
||||||
).padding(bottom: 8),
|
).padding(bottom: 8),
|
||||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8),
|
if (data.preload?.video != null)
|
||||||
|
_PostVideoPlayer(data: data).padding(bottom: 8),
|
||||||
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
||||||
_PostFeaturedComment(data: data),
|
_PostFeaturedComment(data: data),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
@@ -265,7 +273,8 @@ class PostItem extends StatelessWidget {
|
|||||||
if (onDeleted != null) {}
|
if (onDeleted != null) {}
|
||||||
},
|
},
|
||||||
).padding(horizontal: 12, top: 8, bottom: 8),
|
).padding(horizontal: 12, top: 8, bottom: 8),
|
||||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
if (data.preload?.video != null)
|
||||||
|
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
|
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
|
||||||
@@ -308,8 +317,13 @@ class PostItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
|
Text('postArticle')
|
||||||
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
.tr()
|
||||||
|
.fontSize(13)
|
||||||
|
.opacity(0.75)
|
||||||
|
.padding(horizontal: 24, bottom: 8),
|
||||||
|
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
||||||
|
.padding(horizontal: 12),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
data: data,
|
data: data,
|
||||||
showComments: showComments,
|
showComments: showComments,
|
||||||
@@ -324,7 +338,8 @@ class PostItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final displayableAttachments = data.preload?.attachments
|
final displayableAttachments = data.preload?.attachments
|
||||||
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
|
?.where((ele) =>
|
||||||
|
ele?.mediaType != SnMediaType.image || data.type != 'article')
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final cfg = context.read<ConfigProvider>();
|
final cfg = context.read<ConfigProvider>();
|
||||||
@@ -349,9 +364,13 @@ class PostItem extends StatelessWidget {
|
|||||||
if (onDeleted != null) onDeleted!();
|
if (onDeleted != null) onDeleted!();
|
||||||
},
|
},
|
||||||
).padding(horizontal: 12, vertical: 8),
|
).padding(horizontal: 12, vertical: 8),
|
||||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
if (data.preload?.video != null)
|
||||||
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||||
if (data.body['title'] != null || data.body['description'] != null)
|
if (data.type == 'question')
|
||||||
|
_PostQuestionHint(data: data)
|
||||||
|
.padding(horizontal: 16, bottom: 8),
|
||||||
|
if (data.body['title'] != null ||
|
||||||
|
data.body['description'] != null)
|
||||||
_PostHeadline(
|
_PostHeadline(
|
||||||
data: data,
|
data: data,
|
||||||
isEnlarge: data.type == 'article' && showFullPost,
|
isEnlarge: data.type == 'article' && showFullPost,
|
||||||
@@ -365,7 +384,8 @@ class PostItem extends StatelessWidget {
|
|||||||
if (data.repostTo != null)
|
if (data.repostTo != null)
|
||||||
_PostQuoteContent(child: data.repostTo!).padding(
|
_PostQuoteContent(child: data.repostTo!).padding(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
bottom: data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
|
bottom:
|
||||||
|
data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
|
||||||
),
|
),
|
||||||
if (data.visibility > 0)
|
if (data.visibility > 0)
|
||||||
_PostVisibilityHint(data: data).padding(
|
_PostVisibilityHint(data: data).padding(
|
||||||
@@ -377,7 +397,9 @@ class PostItem extends StatelessWidget {
|
|||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6),
|
if (data.tags.isNotEmpty)
|
||||||
|
_PostTagsList(data: data)
|
||||||
|
.padding(horizontal: 16, top: 4, bottom: 6),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -390,12 +412,16 @@ class PostItem extends StatelessWidget {
|
|||||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4),
|
if (data.preload?.poll != null)
|
||||||
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
PostPoll(poll: data.preload!.poll!)
|
||||||
|
.padding(horizontal: 12, vertical: 4),
|
||||||
|
if (data.body['content'] != null &&
|
||||||
|
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||||
LinkPreviewWidget(
|
LinkPreviewWidget(
|
||||||
text: data.body['content'],
|
text: data.body['content'],
|
||||||
).padding(horizontal: 4),
|
).padding(horizontal: 4),
|
||||||
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
||||||
|
.padding(horizontal: 12),
|
||||||
Container(
|
Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -457,7 +483,8 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
showMenu: false,
|
showMenu: false,
|
||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
if (data.type == 'question')
|
||||||
|
_PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
||||||
_PostHeadline(
|
_PostHeadline(
|
||||||
data: data,
|
data: data,
|
||||||
isEnlarge: data.type == 'article',
|
isEnlarge: data.type == 'article',
|
||||||
@@ -472,7 +499,8 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
child: data.repostTo!,
|
child: data.repostTo!,
|
||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
|
if (data.type != 'article' &&
|
||||||
|
(data.preload?.attachments?.isNotEmpty ?? false))
|
||||||
StyledWidget(AttachmentList(
|
StyledWidget(AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
columned: true,
|
columned: true,
|
||||||
@@ -481,7 +509,8 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (data.visibility > 0) _PostVisibilityHint(data: data),
|
if (data.visibility > 0) _PostVisibilityHint(data: data),
|
||||||
if (data.body['content_truncated'] == true) _PostTruncatedHint(data: data),
|
if (data.body['content_truncated'] == true)
|
||||||
|
_PostTruncatedHint(data: data),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
@@ -541,7 +570,8 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
version: QrVersions.auto,
|
version: QrVersions.auto,
|
||||||
size: 100,
|
size: 100,
|
||||||
gapless: true,
|
gapless: true,
|
||||||
embeddedImage: AssetImage('assets/icon/icon-light-radius.png'),
|
embeddedImage:
|
||||||
|
AssetImage('assets/icon/icon-light-radius.png'),
|
||||||
embeddedImageStyle: QrEmbeddedImageStyle(
|
embeddedImageStyle: QrEmbeddedImageStyle(
|
||||||
size: Size(28, 28),
|
size: Size(28, 28),
|
||||||
),
|
),
|
||||||
@@ -572,9 +602,11 @@ class _PostQuestionHint extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20),
|
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle,
|
||||||
|
size: 20),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
if (data.body['answer'] == null && data.body['reward']?.toDouble() != null)
|
if (data.body['answer'] == null &&
|
||||||
|
data.body['reward']?.toDouble() != null)
|
||||||
Text('postQuestionUnansweredWithReward'.tr(args: [
|
Text('postQuestionUnansweredWithReward'.tr(args: [
|
||||||
'${data.body['reward']}',
|
'${data.body['reward']}',
|
||||||
])).opacity(0.75)
|
])).opacity(0.75)
|
||||||
@@ -610,7 +642,9 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
|
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
|
||||||
? data.metric.reactionList.entries.reduce((a, b) => a.value > b.value ? a : b).key
|
? data.metric.reactionList.entries
|
||||||
|
.reduce((a, b) => a.value > b.value ? a : b)
|
||||||
|
.key
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
@@ -624,7 +658,8 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
InkWell(
|
InkWell(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (mostTypicalReaction == null || kTemplateReactions[mostTypicalReaction] == null)
|
if (mostTypicalReaction == null ||
|
||||||
|
kTemplateReactions[mostTypicalReaction] == null)
|
||||||
Icon(Symbols.add_reaction, size: 20, color: iconColor)
|
Icon(Symbols.add_reaction, size: 20, color: iconColor)
|
||||||
else
|
else
|
||||||
Text(
|
Text(
|
||||||
@@ -636,7 +671,8 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (data.totalUpvote > 0 && data.totalUpvote >= data.totalDownvote)
|
if (data.totalUpvote > 0 &&
|
||||||
|
data.totalUpvote >= data.totalDownvote)
|
||||||
Text('postReactionUpvote').plural(
|
Text('postReactionUpvote').plural(
|
||||||
data.totalUpvote,
|
data.totalUpvote,
|
||||||
)
|
)
|
||||||
@@ -655,8 +691,12 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
data: data,
|
data: data,
|
||||||
onChanged: (value, attr, delta) {
|
onChanged: (value, attr, delta) {
|
||||||
onChanged(data.copyWith(
|
onChanged(data.copyWith(
|
||||||
totalUpvote: attr == 1 ? data.totalUpvote + delta : data.totalUpvote,
|
totalUpvote: attr == 1
|
||||||
totalDownvote: attr == 2 ? data.totalDownvote + delta : data.totalDownvote,
|
? data.totalUpvote + delta
|
||||||
|
: data.totalUpvote,
|
||||||
|
totalDownvote: attr == 2
|
||||||
|
? data.totalDownvote + delta
|
||||||
|
: data.totalDownvote,
|
||||||
metric: data.metric.copyWith(reactionList: value),
|
metric: data.metric.copyWith(reactionList: value),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@@ -904,8 +944,10 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
isRelativeDate
|
isRelativeDate
|
||||||
? RelativeTime(context).format(data.publishedAt ?? data.createdAt)
|
? RelativeTime(context)
|
||||||
: DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt),
|
.format(data.publishedAt ?? data.createdAt)
|
||||||
|
: DateFormat('y/M/d HH:mm')
|
||||||
|
.format(data.publishedAt ?? data.createdAt),
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
],
|
],
|
||||||
).opacity(0.8),
|
).opacity(0.8),
|
||||||
@@ -923,8 +965,10 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
isRelativeDate
|
isRelativeDate
|
||||||
? RelativeTime(context).format(data.publishedAt ?? data.createdAt)
|
? RelativeTime(context)
|
||||||
: DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt),
|
.format(data.publishedAt ?? data.createdAt)
|
||||||
|
: DateFormat('y/M/d HH:mm')
|
||||||
|
.format(data.publishedAt ?? data.createdAt),
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
],
|
],
|
||||||
).opacity(0.8),
|
).opacity(0.8),
|
||||||
@@ -1107,7 +1151,8 @@ class _PostContentBody extends StatelessWidget {
|
|||||||
if (data.body['content'] == null) return const SizedBox.shrink();
|
if (data.body['content'] == null) return const SizedBox.shrink();
|
||||||
final content = MarkdownTextContent(
|
final content = MarkdownTextContent(
|
||||||
isAutoWarp: data.type == 'story',
|
isAutoWarp: data.type == 'story',
|
||||||
isEnlargeSticker: true,
|
isEnlargeSticker:
|
||||||
|
RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
|
||||||
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
||||||
content: data.body['content'],
|
content: data.body['content'],
|
||||||
attachments: data.preload?.attachments,
|
attachments: data.preload?.attachments,
|
||||||
@@ -1156,10 +1201,12 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
onDeleted: () {},
|
onDeleted: () {},
|
||||||
).padding(bottom: 4),
|
).padding(bottom: 4),
|
||||||
_PostContentBody(data: child),
|
_PostContentBody(data: child),
|
||||||
if (child.visibility > 0) _PostVisibilityHint(data: child).padding(top: 4),
|
if (child.visibility > 0)
|
||||||
|
_PostVisibilityHint(data: child).padding(top: 4),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
if (child.type != 'article' && (child.preload?.attachments?.isNotEmpty ?? false))
|
if (child.type != 'article' &&
|
||||||
|
(child.preload?.attachments?.isNotEmpty ?? false))
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(8),
|
bottomLeft: Radius.circular(8),
|
||||||
@@ -1310,7 +1357,9 @@ class _PostTruncatedHint extends StatelessWidget {
|
|||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text('postReadEstimate').tr(args: [
|
Text('postReadEstimate').tr(args: [
|
||||||
'${Duration(
|
'${Duration(
|
||||||
seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed,
|
seconds: (data.body['content_length'] as num).toDouble() *
|
||||||
|
60 ~/
|
||||||
|
kHumanReadSpeed,
|
||||||
).inSeconds}s',
|
).inSeconds}s',
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
@@ -1349,7 +1398,8 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
// If this is a answered question, fetch the answer instead
|
// If this is a answered question, fetch the answer instead
|
||||||
if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
|
if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
|
||||||
_isAnswer = true;
|
_isAnswer = true;
|
||||||
setState(() => _featuredComment = SnPost.fromJson(resp.data));
|
setState(() => _featuredComment = SnPost.fromJson(resp.data));
|
||||||
return;
|
return;
|
||||||
@@ -1357,7 +1407,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
|
final resp = await sn.client.get(
|
||||||
|
'/cgi/co/posts/${widget.data.id}/replies/featured',
|
||||||
|
queryParameters: {
|
||||||
'take': 1,
|
'take': 1,
|
||||||
});
|
});
|
||||||
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
|
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
|
||||||
@@ -1388,7 +1440,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Material(
|
child: Material(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
color: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: _isAnswer
|
||||||
|
? Colors.green.withOpacity(0.5)
|
||||||
|
: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -1408,11 +1462,17 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Gap(2),
|
const Gap(2),
|
||||||
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20),
|
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion,
|
||||||
|
size: 20),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text(
|
Text(
|
||||||
_isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment',
|
_isAnswer
|
||||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15),
|
? 'postQuestionAnswerTitle'
|
||||||
|
: 'postFeaturedComment',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium!
|
||||||
|
.copyWith(fontSize: 15),
|
||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1550,7 +1610,8 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
|
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
|
||||||
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
|
setState(
|
||||||
|
() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -1573,11 +1634,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.book_4_spark, size: 24),
|
const Icon(Symbols.book_4_spark, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(),
|
Text('postGetInsightTitle',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge)
|
||||||
|
.tr(),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20),
|
Text('postGetInsightDescription',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
if (_response == null)
|
if (_response == null)
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -1595,12 +1661,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
|||||||
leading: const Icon(Symbols.info),
|
leading: const Icon(Symbols.info),
|
||||||
title: Text('aiThinkingProcess'.tr()),
|
title: Text('aiThinkingProcess'.tr()),
|
||||||
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
|
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
collapsedBackgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
minTileHeight: 32,
|
minTileHeight: 32,
|
||||||
children: [
|
children: [
|
||||||
SelectableText(
|
SelectableText(
|
||||||
_thinkingProcess!,
|
_thinkingProcess!,
|
||||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic),
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!
|
||||||
|
.copyWith(fontStyle: FontStyle.italic),
|
||||||
).padding(horizontal: 20, vertical: 8),
|
).padding(horizontal: 20, vertical: 8),
|
||||||
],
|
],
|
||||||
).padding(vertical: 8),
|
).padding(vertical: 8),
|
||||||
@@ -1637,7 +1707,8 @@ class _PostVideoPlayer extends StatelessWidget {
|
|||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
|
child: AttachmentItem(
|
||||||
|
data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
96
lib/widgets/updater.dart
Normal file
96
lib/widgets/updater.dart
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_app_update/azhon_app_update.dart';
|
||||||
|
import 'package:flutter_app_update/update_model.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/markdown_content.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class VersionUpdatePopup extends StatelessWidget {
|
||||||
|
const VersionUpdatePopup({super.key});
|
||||||
|
|
||||||
|
void _update(BuildContext context) async {
|
||||||
|
if (kIsWeb) return;
|
||||||
|
|
||||||
|
final config = context.read<ConfigProvider>();
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final model = UpdateModel(
|
||||||
|
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
|
||||||
|
'solian-app-release-${config.updatableVersion!}.apk',
|
||||||
|
'ic_launcher',
|
||||||
|
'https://apps.apple.com/us/app/solian/id6499032345',
|
||||||
|
);
|
||||||
|
AzhonAppUpdate.update(model);
|
||||||
|
context.showSnackbar('updateOngoing'.tr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final resp = await Dio(
|
||||||
|
BaseOptions(
|
||||||
|
sendTimeout: const Duration(seconds: 60),
|
||||||
|
receiveTimeout: const Duration(seconds: 60),
|
||||||
|
),
|
||||||
|
).get(
|
||||||
|
'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest',
|
||||||
|
);
|
||||||
|
|
||||||
|
launchUrlString(resp.data?['html_url']);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final config = context.watch<ConfigProvider>();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.update),
|
||||||
|
const Gap(16),
|
||||||
|
Text('update')
|
||||||
|
.tr()
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
config.updatableVersion ?? 'unknown'.tr(),
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () => _update(context),
|
||||||
|
child: Text('update').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20),
|
||||||
|
const Divider(height: 1).padding(vertical: 8),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: MarkdownTextContent(
|
||||||
|
content: config.updatableChangelog ?? 'No changelog',
|
||||||
|
).padding(horizontal: 20),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <flutter_udid/flutter_udid_plugin.h>
|
#include <flutter_udid/flutter_udid_plugin.h>
|
||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
|
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
|
||||||
|
#include <local_notifier/local_notifier_plugin.h>
|
||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
#include <media_kit_video/media_kit_video_plugin.h>
|
#include <media_kit_video/media_kit_video_plugin.h>
|
||||||
#include <pasteboard/pasteboard_plugin.h>
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
@@ -38,6 +39,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
|
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
|
||||||
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
|
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) local_notifier_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
|
||||||
|
local_notifier_plugin_register_with_registrar(local_notifier_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
|
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
|
||||||
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
|
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
flutter_udid
|
flutter_udid
|
||||||
flutter_webrtc
|
flutter_webrtc
|
||||||
hotkey_manager_linux
|
hotkey_manager_linux
|
||||||
|
local_notifier
|
||||||
media_kit_libs_linux
|
media_kit_libs_linux
|
||||||
media_kit_video
|
media_kit_video
|
||||||
pasteboard
|
pasteboard
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import gal
|
|||||||
import hotkey_manager_macos
|
import hotkey_manager_macos
|
||||||
import in_app_review
|
import in_app_review
|
||||||
import livekit_client
|
import livekit_client
|
||||||
|
import local_notifier
|
||||||
import media_kit_libs_macos_video
|
import media_kit_libs_macos_video
|
||||||
import media_kit_video
|
import media_kit_video
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
@@ -53,6 +54,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
||||||
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||||
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
||||||
|
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
||||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
|
|||||||
@@ -142,10 +142,12 @@ PODS:
|
|||||||
- HotKey
|
- HotKey
|
||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- livekit_client (2.3.6):
|
- livekit_client (2.4.0):
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
|
- local_notifier (0.1.0):
|
||||||
|
- FlutterMacOS
|
||||||
- media_kit_libs_macos_video (1.0.4):
|
- media_kit_libs_macos_video (1.0.4):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- media_kit_native_event_loop (1.0.0):
|
- media_kit_native_event_loop (1.0.0):
|
||||||
@@ -225,6 +227,7 @@ DEPENDENCIES:
|
|||||||
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
|
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
|
||||||
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||||
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
||||||
|
- local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`)
|
||||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||||
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
|
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
|
||||||
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
|
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
|
||||||
@@ -297,6 +300,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
|
||||||
livekit_client:
|
livekit_client:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
|
||||||
|
local_notifier:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos
|
||||||
media_kit_libs_macos_video:
|
media_kit_libs_macos_video:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
|
||||||
media_kit_native_event_loop:
|
media_kit_native_event_loop:
|
||||||
@@ -356,7 +361,8 @@ SPEC CHECKSUMS:
|
|||||||
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
||||||
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
|
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
|
||||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||||
livekit_client: 0ad107154753a5a76802d2222c040223ad049499
|
livekit_client: 2e766be2c3ee6274a8e2633b356b98b5eb842987
|
||||||
|
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
|
||||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||||
|
|||||||
41
pubspec.lock
41
pubspec.lock
@@ -357,10 +357,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dart_webrtc
|
name: dart_webrtc
|
||||||
sha256: "03df5b41b23bc185ebcf4b0ffc92d002e295bf56287fb5f9d2c321ddaf7760cc"
|
sha256: b34e90bc82f33c1023cf98661369c37bccd648c8a4cf882a875d9f5d8bbef694
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.1"
|
version: "1.5.2+hotfix.1"
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -865,10 +865,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_webrtc
|
name: flutter_webrtc
|
||||||
sha256: "9c4ca34ced1d1b780baf3776557f9edd0af18ce030969346f752e8df455faaab"
|
sha256: "6ea3a86d95b61cfe42d5715426d355b3cece6c88d0119de428d56f6c653811ce"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.10"
|
version: "0.12.11"
|
||||||
freezed:
|
freezed:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -1201,10 +1201,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: livekit_client
|
name: livekit_client
|
||||||
sha256: "0cfb2f48eff7a93ea8927696dc6f218aebd2fcd1fcc1b1a7b2f53ff3597fdb52"
|
sha256: "753bbf484c6b70f10f3dc1dc808dfe3755f472d80eb9682323cff07ad8e2609d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.6"
|
version: "2.4.0"
|
||||||
|
local_notifier:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: local_notifier
|
||||||
|
sha256: f6cfc933c6fbc961f4e52b5c880f68e41b2d3cd29aad557cc654fd211093a025
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.6"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1549,14 +1557,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.6"
|
version: "3.1.6"
|
||||||
platform_detect:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: platform_detect
|
|
||||||
sha256: "7394dc1d884e652785a37c3ff25c54e503c6d9fa2f35b55d5efc0a133dec122c"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.5"
|
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2286,10 +2286,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webrtc_interface
|
name: webrtc_interface
|
||||||
sha256: "10fc6dc0ac16f909f5e434c18902415211d759313c87261f1e4ec5b4f6a04c26"
|
sha256: e05f00091c9c70a15bab4ccb1b6c46d9a16a6075002f02cfac3641eccb05e25d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.1+hotfix.1"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2309,10 +2309,11 @@ packages:
|
|||||||
workmanager:
|
workmanager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: workmanager
|
path: workmanager
|
||||||
sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00
|
ref: main
|
||||||
url: "https://pub.dev"
|
resolved-ref: "4ce065135dc1b91fee918f81596b42a56850391d"
|
||||||
source: hosted
|
url: "https://github.com/fluttercommunity/flutter_workmanager.git"
|
||||||
|
source: git
|
||||||
version: "0.5.2"
|
version: "0.5.2"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2.3.2+70
|
version: 2.3.2+75
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@@ -103,7 +103,11 @@ dependencies:
|
|||||||
flutter_svg: ^2.0.16
|
flutter_svg: ^2.0.16
|
||||||
home_widget: ^0.7.0
|
home_widget: ^0.7.0
|
||||||
receive_sharing_intent: ^1.8.1
|
receive_sharing_intent: ^1.8.1
|
||||||
workmanager: ^0.5.2
|
workmanager:
|
||||||
|
git:
|
||||||
|
url: https://github.com/fluttercommunity/flutter_workmanager.git
|
||||||
|
path: workmanager
|
||||||
|
ref: main
|
||||||
flutter_app_update: ^3.2.2
|
flutter_app_update: ^3.2.2
|
||||||
in_app_review: ^2.0.10
|
in_app_review: ^2.0.10
|
||||||
version: ^3.0.2
|
version: ^3.0.2
|
||||||
@@ -123,6 +127,7 @@ dependencies:
|
|||||||
image_picker_platform_interface: ^2.10.1
|
image_picker_platform_interface: ^2.10.1
|
||||||
drift: ^2.25.1
|
drift: ^2.25.1
|
||||||
drift_flutter: ^0.2.4
|
drift_flutter: ^0.2.4
|
||||||
|
local_notifier: ^0.1.6
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include <gal/gal_plugin_c_api.h>
|
#include <gal/gal_plugin_c_api.h>
|
||||||
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
|
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
|
||||||
#include <livekit_client/live_kit_plugin.h>
|
#include <livekit_client/live_kit_plugin.h>
|
||||||
|
#include <local_notifier/local_notifier_plugin.h>
|
||||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||||
#include <pasteboard/pasteboard_plugin.h>
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
@@ -50,6 +51,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
|
||||||
LiveKitPluginRegisterWithRegistrar(
|
LiveKitPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("LiveKitPlugin"));
|
registry->GetRegistrarForPlugin("LiveKitPlugin"));
|
||||||
|
LocalNotifierPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
||||||
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
||||||
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
gal
|
gal
|
||||||
hotkey_manager_windows
|
hotkey_manager_windows
|
||||||
livekit_client
|
livekit_client
|
||||||
|
local_notifier
|
||||||
media_kit_libs_windows_video
|
media_kit_libs_windows_video
|
||||||
media_kit_video
|
media_kit_video
|
||||||
pasteboard
|
pasteboard
|
||||||
|
|||||||
Reference in New Issue
Block a user