Compare commits
32 Commits
2.3.2+71
...
289aa17a7a
| Author | SHA1 | Date | |
|---|---|---|---|
| 289aa17a7a | |||
| 93f41bb523 | |||
| 09ec9d4a0c | |||
| 1153fbdeee | |||
| e933058338 | |||
| ae9743c84f | |||
| 32bf834108 | |||
| 1b41c847a6 | |||
| b1af6c2c97 | |||
| 8e76ff3f84 | |||
| bd26602299 | |||
| 52ab1d0d10 | |||
| f746e06f65 | |||
| d11069a2be | |||
| d6dc487d9e | |||
| a07c7cdede | |||
| acbc125dec | |||
| ad0ee971c1 | |||
| 52d6bb083e | |||
| 2027eab49b | |||
| 566ebde1dd | |||
| 9e039cc532 | |||
| c4b95d7084 | |||
| a66129a9ba | |||
| 44e1a8bf67 | |||
| efcfd3f57d | |||
| 84759715a4 | |||
| fda09382dd | |||
| 2c5dd0563a | |||
| 5bdd8e94fa | |||
| 2a53031c9a | |||
| e8bc7261f3 |
18
api/Passport/Deal Abuse Report.bru
Normal file
18
api/Passport/Deal Abuse Report.bru
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
meta {
|
||||||
|
name: Deal Abuse Report
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
put {
|
||||||
|
url: {{endpoint}}/cgi/id/reports/abuse/3/status
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"status": "processed",
|
||||||
|
"message": "相关附件已经进行评级处理,未来会将该项权限下放到帖主以及社区成员。"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -203,6 +203,11 @@
|
|||||||
"other": "{} comments"
|
"other": "{} comments"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
|
"settingsCustomFonts": "Custom Fonts",
|
||||||
|
"settingsCustomFontsDescription": "Set custom fonts for the application.",
|
||||||
|
"settingsCustomFontFamily": "Custom Font Family",
|
||||||
|
"settingsCustomFontFamilyHint": "Use comma to separate fonts, higher priority comes first",
|
||||||
|
"settingsCustomFontApplied": "Custom font has been applied.",
|
||||||
"settingsDisplayLanguage": "Display Language",
|
"settingsDisplayLanguage": "Display Language",
|
||||||
"settingsDisplayLanguageDescription": "Set the application language.",
|
"settingsDisplayLanguageDescription": "Set the application language.",
|
||||||
"settingsDisplayLanguageSystem": "Follow System",
|
"settingsDisplayLanguageSystem": "Follow System",
|
||||||
@@ -718,5 +723,17 @@
|
|||||||
"stickersNew": "New Sticker",
|
"stickersNew": "New Sticker",
|
||||||
"stickersNewDescription": "Create a new sticker belongs to this pack.",
|
"stickersNewDescription": "Create a new sticker belongs to this pack.",
|
||||||
"stickersPackNew": "New Sticker Pack",
|
"stickersPackNew": "New Sticker Pack",
|
||||||
"trayMenuShow": "Show"
|
"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.",
|
||||||
|
"debugLogging": "Runtime Logs",
|
||||||
|
"runtimeLogsOpen": "Open Logs",
|
||||||
|
"runtimeLogsDescription": "Show the runtime logs to help debugging.",
|
||||||
|
"signinResetPasswordHint": "Please enter the username / email address to help us to find your account and reset your password.",
|
||||||
|
"cacheSize": "Cache Size",
|
||||||
|
"cacheDelete": "Clean Cache",
|
||||||
|
"cacheDeleteDescription": "Remove the cached images and other resources from your disk, the content will be downloaded from server again.",
|
||||||
|
"cacheDeleted": "All cache has been cleaned up."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,11 @@
|
|||||||
"other": "{} 条评论"
|
"other": "{} 条评论"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "外观",
|
"settingsAppearance": "外观",
|
||||||
|
"settingsCustomFonts": "自定义字体",
|
||||||
|
"settingsCustomFontsDescription": "设置应用程序使用的字体。",
|
||||||
|
"settingsCustomFontFamily": "应用字体",
|
||||||
|
"settingsCustomFontFamilyHint": "使用英文逗号分割每一种字体,越前优先级越高",
|
||||||
|
"settingsCustomFontApplied": "自定义字体已经应用。",
|
||||||
"settingsDisplayLanguage": "显示语言",
|
"settingsDisplayLanguage": "显示语言",
|
||||||
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
||||||
"settingsDisplayLanguageSystem": "跟随系统",
|
"settingsDisplayLanguageSystem": "跟随系统",
|
||||||
@@ -716,5 +721,17 @@
|
|||||||
"stickersNew": "新建贴图",
|
"stickersNew": "新建贴图",
|
||||||
"stickersNewDescription": "创建一个新的贴图。",
|
"stickersNewDescription": "创建一个新的贴图。",
|
||||||
"stickersPackNew": "新建贴图包",
|
"stickersPackNew": "新建贴图包",
|
||||||
"trayMenuShow": "显示"
|
"trayMenuShow": "显示",
|
||||||
|
"trayMenuMuteNotification": "静音通知",
|
||||||
|
"update": "更新",
|
||||||
|
"forceUpdate": "强制更新",
|
||||||
|
"forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。",
|
||||||
|
"runtimeLogs": "运行时日志",
|
||||||
|
"runtimeLogsOpen": "打开日志文件",
|
||||||
|
"runtimeLogsDescription": "显示运行时的日志记录。",
|
||||||
|
"signinResetPasswordHint": "请输入用户名/电子邮箱地址以帮助我们找到您的帐户并重置密码。",
|
||||||
|
"cacheSize": "缓存资源大小",
|
||||||
|
"cacheDelete": "清除缓存",
|
||||||
|
"cacheDeleteDescription": "从磁盘中移除缓存的图片和其他资源,内容将从服务器重新下载。",
|
||||||
|
"cacheDeleted": "所有缓存已被清除。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,11 @@
|
|||||||
"other": "{} 條評論"
|
"other": "{} 條評論"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "外觀",
|
"settingsAppearance": "外觀",
|
||||||
|
"settingsCustomFonts": "自定義字體",
|
||||||
|
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
|
||||||
|
"settingsCustomFontFamily": "應用字體",
|
||||||
|
"settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
|
||||||
|
"settingsCustomFontApplied": "自定義字體已經應用。",
|
||||||
"settingsDisplayLanguage": "顯示語言",
|
"settingsDisplayLanguage": "顯示語言",
|
||||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||||
@@ -715,5 +720,18 @@
|
|||||||
"fieldStickerAttachment": "附件",
|
"fieldStickerAttachment": "附件",
|
||||||
"stickersNew": "新建貼圖",
|
"stickersNew": "新建貼圖",
|
||||||
"stickersNewDescription": "創建一個新的貼圖。",
|
"stickersNewDescription": "創建一個新的貼圖。",
|
||||||
"stickersPackNew": "新建貼圖包"
|
"stickersPackNew": "新建貼圖包",
|
||||||
|
"trayMenuShow": "顯示",
|
||||||
|
"trayMenuMuteNotification": "靜音通知",
|
||||||
|
"update": "更新",
|
||||||
|
"forceUpdate": "強制更新",
|
||||||
|
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
|
||||||
|
"runtimeLogs": "運行時日誌",
|
||||||
|
"runtimeLogsOpen": "打開日誌文件",
|
||||||
|
"runtimeLogsDescription": "顯示運行時的日誌記錄。",
|
||||||
|
"signinResetPasswordHint": "請輸入用户名/電子郵箱地址以幫助我們找到您的帳户並重置密碼。",
|
||||||
|
"cacheSize": "緩存資源大小",
|
||||||
|
"cacheDelete": "清除緩存",
|
||||||
|
"cacheDeleteDescription": "從磁盤中移除緩存的圖片和其他資源,內容將從服務器重新下載。",
|
||||||
|
"cacheDeleted": "所有緩存已被清除。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,11 @@
|
|||||||
"other": "{} 條評論"
|
"other": "{} 條評論"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "外觀",
|
"settingsAppearance": "外觀",
|
||||||
|
"settingsCustomFonts": "自定義字體",
|
||||||
|
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
|
||||||
|
"settingsCustomFontFamily": "應用字體",
|
||||||
|
"settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
|
||||||
|
"settingsCustomFontApplied": "自定義字體已經應用。",
|
||||||
"settingsDisplayLanguage": "顯示語言",
|
"settingsDisplayLanguage": "顯示語言",
|
||||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||||
@@ -715,5 +720,18 @@
|
|||||||
"fieldStickerAttachment": "附件",
|
"fieldStickerAttachment": "附件",
|
||||||
"stickersNew": "新建貼圖",
|
"stickersNew": "新建貼圖",
|
||||||
"stickersNewDescription": "創建一個新的貼圖。",
|
"stickersNewDescription": "創建一個新的貼圖。",
|
||||||
"stickersPackNew": "新建貼圖包"
|
"stickersPackNew": "新建貼圖包",
|
||||||
|
"trayMenuShow": "顯示",
|
||||||
|
"trayMenuMuteNotification": "靜音通知",
|
||||||
|
"update": "更新",
|
||||||
|
"forceUpdate": "強制更新",
|
||||||
|
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
|
||||||
|
"runtimeLogs": "運行時日誌",
|
||||||
|
"runtimeLogsOpen": "打開日誌文件",
|
||||||
|
"runtimeLogsDescription": "顯示運行時的日誌記錄。",
|
||||||
|
"signinResetPasswordHint": "請輸入用戶名/電子郵箱地址以幫助我們找到您的帳戶並重置密碼。",
|
||||||
|
"cacheSize": "緩存資源大小",
|
||||||
|
"cacheDelete": "清除緩存",
|
||||||
|
"cacheDeleteDescription": "從磁盤中移除緩存的圖片和其他資源,內容將從服務器重新下載。",
|
||||||
|
"cacheDeleted": "所有緩存已被清除。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -235,7 +235,7 @@ PODS:
|
|||||||
- sqlite3_flutter_libs (0.0.1):
|
- sqlite3_flutter_libs (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqlite3 (~> 3.49.0)
|
- sqlite3 (~> 3.49.1)
|
||||||
- sqlite3/dbstatvtab
|
- sqlite3/dbstatvtab
|
||||||
- sqlite3/fts5
|
- sqlite3/fts5
|
||||||
- sqlite3/perf-threadsafe
|
- sqlite3/perf-threadsafe
|
||||||
@@ -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
|
||||||
@@ -445,7 +445,7 @@ SPEC CHECKSUMS:
|
|||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||||
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
|
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||||
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -8,6 +7,7 @@ import 'package:drift/drift.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:surface/database/database.dart';
|
import 'package:surface/database/database.dart';
|
||||||
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/providers/database.dart';
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
@@ -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(
|
||||||
content: Constant(jsonEncode(message.toJson())),
|
(_) => SnLocalChatMessageCompanion.custom(
|
||||||
)),
|
content: Constant(jsonEncode(message.toJson())),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
incomeStrandedQueue.add(message);
|
incomeStrandedQueue.add(message);
|
||||||
@@ -212,21 +214,21 @@ 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) {
|
}
|
||||||
await (_dt.db.snLocalChatMessage.update()
|
if (message.relatedEventId != null) {
|
||||||
..where((e) => e.id.equals(message.relatedEventId!)))
|
await (_dt.db.snLocalChatMessage.update()
|
||||||
.write(
|
..where((e) => e.id.equals(message.relatedEventId!)))
|
||||||
SnLocalChatMessageCompanion.custom(
|
.write(
|
||||||
content: Constant(jsonEncode(messages[idx].toJson())),
|
SnLocalChatMessageCompanion.custom(
|
||||||
),
|
content: Constant(jsonEncode(messages[idx].toJson())),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'messages.delete':
|
case 'messages.delete':
|
||||||
@@ -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) =>
|
||||||
@@ -529,7 +532,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
},
|
},
|
||||||
).toJson(),
|
).toJson(),
|
||||||
));
|
));
|
||||||
log('[Messaging] Send read event request: $_readEventAnchor');
|
logging.debug('[Messaging] Send read event request: $_readEventAnchor');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
10
lib/logger.dart
Normal file
10
lib/logger.dart
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import 'package:talker/talker.dart';
|
||||||
|
|
||||||
|
final logging = Talker(
|
||||||
|
settings: TalkerSettings(
|
||||||
|
enabled: true,
|
||||||
|
useHistory: true,
|
||||||
|
maxHistoryItems: 1000,
|
||||||
|
useConsoleLogs: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -20,6 +20,7 @@ import 'package:relative_time/relative_time.dart';
|
|||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:surface/firebase_options.dart';
|
import 'package:surface/firebase_options.dart';
|
||||||
|
import 'package:surface/logger.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/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
@@ -235,7 +236,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
await inAppReview.requestReview();
|
await inAppReview.requestReview();
|
||||||
prefs.setBool('rating_requested', true);
|
prefs.setBool('rating_requested', true);
|
||||||
} else {
|
} else {
|
||||||
log('Unable request app review, unavailable');
|
logging.error('Unable request app review, unavailable');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -254,25 +255,27 @@ 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 =
|
||||||
int.tryParse(remoteVersionString.split('+').last) ?? 0;
|
int.tryParse(remoteVersionString.split('+').last) ?? 0;
|
||||||
final localBuildNumber =
|
final localBuildNumber =
|
||||||
int.tryParse(localVersionString.split('+').last) ?? 0;
|
int.tryParse(localVersionString.split('+').last) ?? 0;
|
||||||
log("[Update] Local: $localVersionString, Remote: $remoteVersionString");
|
logging.info(
|
||||||
|
"[Update] Local: $localVersionString, Remote: $remoteVersionString");
|
||||||
if ((remoteVersion > localVersion ||
|
if ((remoteVersion > localVersion ||
|
||||||
remoteBuildNumber > localBuildNumber) &&
|
remoteBuildNumber > localBuildNumber) &&
|
||||||
mounted) {
|
mounted) {
|
||||||
final config = context.read<ConfigProvider>();
|
final config = context.read<ConfigProvider>();
|
||||||
config.setUpdate(remoteVersionString);
|
config.setUpdate(
|
||||||
log("[Update] Update available: $remoteVersionString");
|
remoteVersionString, resp.data?['body'] ?? 'No changelog');
|
||||||
|
logging.info("[Update] Update available: $remoteVersionString");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logging.error('[Error] Unable to check update...', e);
|
||||||
if (mounted) context.showErrorDialog('Unable to check update: $e');
|
if (mounted) context.showErrorDialog('Unable to check update: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,7 +308,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final sticker = context.read<SnStickerProvider>();
|
final sticker = context.read<SnStickerProvider>();
|
||||||
await sticker.listSticker();
|
await sticker.listSticker();
|
||||||
log('[Bootstrap] Everything initialized!');
|
logging.info('[Bootstrap] Everything initialized!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await context.showErrorDialog(err);
|
await context.showErrorDialog(err);
|
||||||
@@ -332,6 +335,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;
|
||||||
|
|
||||||
@@ -343,32 +371,20 @@ 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: [
|
key: 'version_label',
|
||||||
MenuItem(
|
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
|
||||||
key: 'version_label',
|
disabled: true,
|
||||||
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
|
|
||||||
disabled: true,
|
|
||||||
),
|
|
||||||
MenuItem.separator(),
|
|
||||||
MenuItem(
|
|
||||||
key: 'window_show',
|
|
||||||
label: 'trayMenuShow'.tr(),
|
|
||||||
),
|
|
||||||
MenuItem(
|
|
||||||
key: 'exit',
|
|
||||||
label: 'trayMenuExit'.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
await trayManager.setContextMenu(menu);
|
|
||||||
|
await trayManager.setContextMenu(_appTrayMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _notifyInitialization() async {
|
Future<void> _notifyInitialization() async {
|
||||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||||
|
|
||||||
await localNotifier.setup(
|
await localNotifier.setup(
|
||||||
appName: 'solian',
|
appName: 'Solian',
|
||||||
shortcutPolicy: ShortcutPolicy.requireCreate,
|
shortcutPolicy: ShortcutPolicy.requireCreate,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -423,12 +439,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':
|
case 'window_show':
|
||||||
appWindow.show();
|
// To prevent the window from being hide after just show on macOS
|
||||||
|
Timer(const Duration(milliseconds: 100), () => appWindow.show());
|
||||||
break;
|
break;
|
||||||
case 'exit':
|
case 'exit':
|
||||||
_appLifecycleListener?.dispose();
|
_appLifecycleListener?.dispose();
|
||||||
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
if (Platform.isWindows) {
|
||||||
|
appWindow.close();
|
||||||
|
} else {
|
||||||
|
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
if (local != null) {
|
if (local != null) {
|
||||||
final out = local.content;
|
final out = local.content;
|
||||||
return out.copyWith(realm: await _rels.getRealm(out.realmId!));
|
if (out.realmId != null) {
|
||||||
|
return out.copyWith(realm: await _rels.getRealm(out.realmId!));
|
||||||
|
} else {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp =
|
var resp =
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
|||||||
const kAppExpandPostLink = 'app_expand_post_link';
|
const kAppExpandPostLink = 'app_expand_post_link';
|
||||||
const kAppExpandChatLink = 'app_expand_chat_link';
|
const kAppExpandChatLink = 'app_expand_chat_link';
|
||||||
const kAppRealmCompactView = 'app_realm_compact_view';
|
const kAppRealmCompactView = 'app_realm_compact_view';
|
||||||
|
const kAppCustomFonts = 'app_custom_fonts';
|
||||||
|
|
||||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||||
'settingsImageQualityLowest': FilterQuality.none,
|
'settingsImageQualityLowest': FilterQuality.none,
|
||||||
@@ -58,7 +59,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 +68,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 +80,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 +91,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/link.dart';
|
import 'package:surface/types/link.dart';
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class SnLinkPreviewProvider {
|
|||||||
final target = b64.encode(url);
|
final target = b64.encode(url);
|
||||||
if (_cache.containsKey(target)) return _cache[target];
|
if (_cache.containsKey(target)) return _cache[target];
|
||||||
|
|
||||||
log('[LinkPreview] Fetching $url ($target)');
|
logging.debug('[LinkPreview] Fetching $url ($target)');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client.get('/cgi/re/link/$target');
|
final resp = await _sn.client.get('/cgi/re/link/$target');
|
||||||
@@ -28,7 +28,7 @@ class SnLinkPreviewProvider {
|
|||||||
_cache[url] = meta;
|
_cache[url] = meta;
|
||||||
return meta;
|
return meta;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('[LinkPreview] Failed to fetch $url ($target)...');
|
logging.warning('[LinkPreview] Failed to fetch $url ($target)...', err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:developer';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
@@ -9,6 +8,7 @@ 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:local_notifier/local_notifier.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/logger.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';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
@@ -48,11 +48,13 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
var deviceUuid = await FlutterUdid.consistentUdid;
|
var deviceUuid = await FlutterUdid.consistentUdid;
|
||||||
|
|
||||||
if (deviceUuid.isEmpty) {
|
if (deviceUuid.isEmpty) {
|
||||||
log("Unable to active push notifications, couldn't get device uuid");
|
logging.warning(
|
||||||
|
'[Push Notification] Unable to active push notifications, couldn\'t get device uuid');
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
log('Device UUID is $deviceUuid');
|
logging.info('[Push Notification] Device UUID is $deviceUuid');
|
||||||
log('Registering device push notifications...');
|
logging
|
||||||
|
.info('[Push Notification] Registering device push notifications...');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.isIOS || Platform.isMacOS) {
|
if (Platform.isIOS || Platform.isMacOS) {
|
||||||
@@ -62,7 +64,7 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
provider = 'fcm';
|
provider = 'fcm';
|
||||||
token = await FirebaseMessaging.instance.getToken();
|
token = await FirebaseMessaging.instance.getToken();
|
||||||
}
|
}
|
||||||
log('Device Push Token is $token');
|
logging.info('[Push Notification] Device Push Token is $token');
|
||||||
|
|
||||||
await _sn.client.post(
|
await _sn.client.post(
|
||||||
'/cgi/id/notifications/subscription',
|
'/cgi/id/notifications/subscription',
|
||||||
@@ -78,10 +80,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++;
|
||||||
@@ -92,10 +109,8 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
updateTray();
|
updateTray();
|
||||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
|
||||||
if (doHaptic) HapticFeedback.mediumImpact();
|
|
||||||
|
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb && !isMuted) {
|
||||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||||
LocalNotification notify = LocalNotification(
|
LocalNotification notify = LocalNotification(
|
||||||
title: notification.title,
|
title: notification.title,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -11,9 +10,12 @@ import 'package:package_info_plus/package_info_plus.dart';
|
|||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/widget.dart';
|
import 'package:surface/providers/widget.dart';
|
||||||
import 'package:synchronized/synchronized.dart';
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart';
|
||||||
|
import 'package:talker_dio_logger/talker_dio_logger_settings.dart';
|
||||||
|
|
||||||
const kNetworkServerDirectory = [
|
const kNetworkServerDirectory = [
|
||||||
('Solar Network', 'https://api.sn.solsynth.dev'),
|
('Solar Network', 'https://api.sn.solsynth.dev'),
|
||||||
@@ -36,6 +38,19 @@ class SnNetworkProvider {
|
|||||||
|
|
||||||
client = Dio();
|
client = Dio();
|
||||||
|
|
||||||
|
client.interceptors.add(
|
||||||
|
TalkerDioLogger(
|
||||||
|
talker: logging,
|
||||||
|
settings: const TalkerDioLoggerSettings(
|
||||||
|
printRequestHeaders: false,
|
||||||
|
printResponseHeaders: false,
|
||||||
|
printResponseMessage: false,
|
||||||
|
printResponseData: false,
|
||||||
|
printRequestData: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
client.interceptors.add(RetryInterceptor(
|
client.interceptors.add(RetryInterceptor(
|
||||||
dio: client,
|
dio: client,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
@@ -69,7 +84,6 @@ class SnNetworkProvider {
|
|||||||
_prefs = _config.prefs;
|
_prefs = _config.prefs;
|
||||||
client.options.baseUrl = _config.serverUrl;
|
client.options.baseUrl = _config.serverUrl;
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Dio> createOffContextClient() async {
|
static Future<Dio> createOffContextClient() async {
|
||||||
@@ -91,7 +105,8 @@ class SnNetworkProvider {
|
|||||||
RequestOptions options,
|
RequestOptions options,
|
||||||
RequestInterceptorHandler handler,
|
RequestInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey), prefs.getString(kRtkStoreKey), (atk, rtk) {
|
final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey),
|
||||||
|
prefs.getString(kRtkStoreKey), (atk, rtk) {
|
||||||
prefs.setString(kAtkStoreKey, atk);
|
prefs.setString(kAtkStoreKey, atk);
|
||||||
prefs.setString(kRtkStoreKey, rtk);
|
prefs.setString(kRtkStoreKey, rtk);
|
||||||
});
|
});
|
||||||
@@ -103,7 +118,8 @@ class SnNetworkProvider {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
client.options.baseUrl = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
client.options.baseUrl =
|
||||||
|
prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
@@ -119,7 +135,8 @@ class SnNetworkProvider {
|
|||||||
platformInfo = 'Web; ${deviceInfo.vendor}';
|
platformInfo = 'Web; ${deviceInfo.vendor}';
|
||||||
} else if (Platform.isAndroid) {
|
} else if (Platform.isAndroid) {
|
||||||
final deviceInfo = await DeviceInfoPlugin().androidInfo;
|
final deviceInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
platformInfo = 'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
|
platformInfo =
|
||||||
|
'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
final deviceInfo = await DeviceInfoPlugin().iosInfo;
|
final deviceInfo = await DeviceInfoPlugin().iosInfo;
|
||||||
platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}';
|
platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}';
|
||||||
@@ -128,7 +145,8 @@ class SnNetworkProvider {
|
|||||||
platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}';
|
platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}';
|
||||||
} else if (Platform.isWindows) {
|
} else if (Platform.isWindows) {
|
||||||
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
|
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
|
||||||
platformInfo = 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
|
platformInfo =
|
||||||
|
'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
|
||||||
} else if (Platform.isLinux) {
|
} else if (Platform.isLinux) {
|
||||||
final deviceInfo = await DeviceInfoPlugin().linuxInfo;
|
final deviceInfo = await DeviceInfoPlugin().linuxInfo;
|
||||||
platformInfo = 'Linux; ${deviceInfo.prettyName}';
|
platformInfo = 'Linux; ${deviceInfo.prettyName}';
|
||||||
@@ -148,12 +166,15 @@ class SnNetworkProvider {
|
|||||||
final tkLock = Lock();
|
final tkLock = Lock();
|
||||||
|
|
||||||
Future<String?> getFreshAtk() async {
|
Future<String?> getFreshAtk() async {
|
||||||
return await _getFreshAtk(client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey), (atk, rtk) {
|
return await _getFreshAtk(
|
||||||
|
client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey),
|
||||||
|
(atk, rtk) {
|
||||||
setTokenPair(atk, rtk);
|
setTokenPair(atk, rtk);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk, Function(String atk, String rtk)? onRefresh) async {
|
static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk,
|
||||||
|
Function(String atk, String rtk)? onRefresh) async {
|
||||||
if (_refreshCompleter != null) {
|
if (_refreshCompleter != null) {
|
||||||
return await _refreshCompleter!.future;
|
return await _refreshCompleter!.future;
|
||||||
} else {
|
} else {
|
||||||
@@ -185,7 +206,8 @@ class SnNetworkProvider {
|
|||||||
final payload = b64.decode(rawPayload);
|
final payload = b64.decode(rawPayload);
|
||||||
final exp = jsonDecode(payload)['exp'];
|
final exp = jsonDecode(payload)['exp'];
|
||||||
if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
|
if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
|
||||||
log('Access token need refresh, doing it at ${DateTime.now()}');
|
logging.debug(
|
||||||
|
'[Auth] Access token need refresh, doing it at ${DateTime.now()}');
|
||||||
final result = await _refreshToken(client.options.baseUrl, rtk);
|
final result = await _refreshToken(client.options.baseUrl, rtk);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
atk = null;
|
atk = null;
|
||||||
@@ -199,12 +221,12 @@ class SnNetworkProvider {
|
|||||||
_refreshCompleter!.complete(atk);
|
_refreshCompleter!.complete(atk);
|
||||||
return atk;
|
return atk;
|
||||||
} else {
|
} else {
|
||||||
log('Access token refresh failed...');
|
logging.error('[Auth] Access token refresh failed...');
|
||||||
_refreshCompleter!.complete(null);
|
_refreshCompleter!.complete(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('Failed to authenticate user: $err');
|
logging.error('[Auth] Failed to authenticate user...', err);
|
||||||
_refreshCompleter!.completeError(err);
|
_refreshCompleter!.completeError(err);
|
||||||
} finally {
|
} finally {
|
||||||
_refreshCompleter = null;
|
_refreshCompleter = null;
|
||||||
@@ -237,7 +259,8 @@ class SnNetworkProvider {
|
|||||||
return result.$1;
|
return result.$1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<(String, String)?> _refreshToken(String baseUrl, String? rtk) async {
|
static Future<(String, String)?> _refreshToken(
|
||||||
|
String baseUrl, String? rtk) async {
|
||||||
if (rtk == null) return null;
|
if (rtk == null) return null;
|
||||||
|
|
||||||
final dio = Dio();
|
final dio = Dio();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ class SnStickerProvider {
|
|||||||
return sticker;
|
return sticker;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_cache[alias] = null;
|
_cache[alias] = null;
|
||||||
log('[Sticker] Failed to lookup sticker $alias: $err');
|
logging.warning('[Sticker] Failed to lookup sticker $alias', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -66,7 +65,7 @@ class SnStickerProvider {
|
|||||||
_cacheSticker(sticker);
|
_cacheSticker(sticker);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('[Sticker] Failed to list stickers: $err');
|
logging.error('[Sticker] Failed to list stickers...', err);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,16 @@ class ThemeProvider extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void reloadTheme({Color? seedColorOverride, bool? useMaterial3}) {
|
void reloadTheme({
|
||||||
createAppThemeSet(seedColorOverride: seedColorOverride, useMaterial3: useMaterial3).then((value) {
|
Color? seedColorOverride,
|
||||||
|
bool? useMaterial3,
|
||||||
|
String? customFonts,
|
||||||
|
}) {
|
||||||
|
createAppThemeSet(
|
||||||
|
seedColorOverride: seedColorOverride,
|
||||||
|
useMaterial3: useMaterial3,
|
||||||
|
customFonts: customFonts,
|
||||||
|
).then((value) {
|
||||||
theme = value;
|
theme = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,8 +27,11 @@ class UserDirectoryProvider {
|
|||||||
plannedQuery.add(item);
|
plannedQuery.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final resp = await _sn.client.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
|
if (plannedQuery.isEmpty) return out;
|
||||||
final respDecoded = resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList();
|
final resp = await _sn.client
|
||||||
|
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
|
||||||
|
final respDecoded =
|
||||||
|
resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList();
|
||||||
var sideIdx = 0;
|
var sideIdx = 0;
|
||||||
for (var idx = 0; idx < out.length; idx++) {
|
for (var idx = 0; idx < out.length; idx++) {
|
||||||
if (out[idx] != null) continue;
|
if (out[idx] != null) continue;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:surface/logger.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';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
@@ -30,8 +29,8 @@ class UserProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
refreshUser().then((value) async {
|
refreshUser().then((value) async {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
log('Logged in as @${value.name}');
|
logging.info('[Auth] Logged in as @${value.name}');
|
||||||
log('Atk: ${await atk}');
|
logging.debug('[Auth] Access token: ${await atk}');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/websocket.dart';
|
import 'package:surface/types/websocket.dart';
|
||||||
@@ -30,7 +30,7 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
if (isConnected) return;
|
if (isConnected) return;
|
||||||
if (!_ua.isAuthorized) return;
|
if (!_ua.isAuthorized) return;
|
||||||
|
|
||||||
log('[WebSocket] Connecting to the server...');
|
logging.debug('[WebSocket] Connecting to the server...');
|
||||||
await connect();
|
await connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,17 +62,14 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
await conn!.ready;
|
await conn!.ready;
|
||||||
_wsStream = conn!.stream.asBroadcastStream();
|
_wsStream = conn!.stream.asBroadcastStream();
|
||||||
listen();
|
listen();
|
||||||
log('[WebSocket] Connected to server!');
|
logging.info('[WebSocket] Connected to server!');
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err is WebSocketChannelException) {
|
logging.error('[WebSocket] Failed to connect to websocket...', err);
|
||||||
log('Failed to connect to websocket: ${(err.inner as dynamic).message}');
|
|
||||||
} else {
|
|
||||||
log('Failed to connect to websocket: $err');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!noRetry) {
|
if (!noRetry) {
|
||||||
log('Retry connecting to websocket in 3 seconds...');
|
logging.warning(
|
||||||
|
'[WebSocket] Retry connecting to websocket in 3 seconds...');
|
||||||
return Future.delayed(
|
return Future.delayed(
|
||||||
const Duration(seconds: 3),
|
const Duration(seconds: 3),
|
||||||
() => connect(noRetry: true),
|
() => connect(noRetry: true),
|
||||||
@@ -100,7 +97,8 @@ class WebSocketProvider extends ChangeNotifier {
|
|||||||
_wsStream!.listen(
|
_wsStream!.listen(
|
||||||
(event) {
|
(event) {
|
||||||
final packet = WebSocketPackage.fromJson(jsonDecode(event));
|
final packet = WebSocketPackage.fromJson(jsonDecode(event));
|
||||||
log('Websocket incoming message: ${packet.method} ${packet.message}');
|
logging.debug(
|
||||||
|
'[Websocket] Incoming message: ${packet.method} ${packet.message}');
|
||||||
pk.sink.add(packet);
|
pk.sink.add(packet);
|
||||||
},
|
},
|
||||||
onDone: () {
|
onDone: () {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import 'package:surface/screens/chat/room.dart';
|
|||||||
import 'package:surface/screens/explore.dart';
|
import 'package:surface/screens/explore.dart';
|
||||||
import 'package:surface/screens/friend.dart';
|
import 'package:surface/screens/friend.dart';
|
||||||
import 'package:surface/screens/home.dart';
|
import 'package:surface/screens/home.dart';
|
||||||
|
import 'package:surface/screens/logging.dart';
|
||||||
import 'package:surface/screens/news/news_detail.dart';
|
import 'package:surface/screens/news/news_detail.dart';
|
||||||
import 'package:surface/screens/news/news_list.dart';
|
import 'package:surface/screens/news/news_list.dart';
|
||||||
import 'package:surface/screens/notification.dart';
|
import 'package:surface/screens/notification.dart';
|
||||||
@@ -249,6 +250,11 @@ final _appRoutes = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/debug/logging',
|
||||||
|
name: 'debugLogging',
|
||||||
|
builder: (context, state) => const DebugLoggingScreen(),
|
||||||
|
),
|
||||||
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);
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ class UserScreen extends StatefulWidget {
|
|||||||
State<UserScreen> createState() => _UserScreenState();
|
State<UserScreen> createState() => _UserScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
|
class _UserScreenState extends State<UserScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late final ScrollController _scrollController = ScrollController();
|
late final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
SnAccount? _account;
|
SnAccount? _account;
|
||||||
@@ -64,13 +65,18 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SnCheckInRecord>> _getCheckInRecords() async {
|
List<SnCheckInRecord>? _records;
|
||||||
|
|
||||||
|
Future<void> _getCheckInRecords() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
|
final resp =
|
||||||
return List.from(
|
await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
|
||||||
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
|
setState(() {
|
||||||
);
|
_records = List.from(
|
||||||
|
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
|
||||||
|
);
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (mounted) context.showErrorDialog(err);
|
if (mounted) context.showErrorDialog(err);
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -98,7 +104,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
Future<void> _fetchPublishers() async {
|
Future<void> _fetchPublishers() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/co/publishers?user=${widget.name}');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/co/publishers?user=${widget.name}');
|
||||||
_publishers = List<SnPublisher>.from(
|
_publishers = List<SnPublisher>.from(
|
||||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
@@ -144,7 +151,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
'related': _account!.name,
|
'related': _account!.name,
|
||||||
});
|
});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
context.showSnackbar(
|
||||||
|
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -160,9 +168,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final rel = context.read<SnRelationshipProvider>();
|
final rel = context.read<SnRelationshipProvider>();
|
||||||
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
await rel.updateRelationship(
|
||||||
|
_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
context.showSnackbar(
|
||||||
|
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -188,12 +198,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
double _appBarBlur = 0.0;
|
double _appBarBlur = 0.0;
|
||||||
|
|
||||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||||
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
|
late final _appBarHeight =
|
||||||
|
(_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||||
|
|
||||||
void _updateAppBarBlur() {
|
void _updateAppBarBlur() {
|
||||||
if (_scrollController.offset > _appBarHeight) return;
|
if (_scrollController.offset > _appBarHeight) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
_appBarBlur =
|
||||||
|
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +217,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
_fetchStatus();
|
_fetchStatus();
|
||||||
_fetchPublishers();
|
_fetchPublishers();
|
||||||
|
_getCheckInRecords();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final rel = context.read<SnRelationshipProvider>();
|
final rel = context.read<SnRelationshipProvider>();
|
||||||
@@ -260,18 +273,20 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
text: TextSpan(children: [
|
text: TextSpan(children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _account!.nick,
|
text: _account!.nick,
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style:
|
||||||
color: Colors.white,
|
Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
shadows: labelShadows,
|
color: Colors.white,
|
||||||
),
|
shadows: labelShadows,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '@${_account!.name}',
|
text: '@${_account!.name}',
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style:
|
||||||
color: Colors.white,
|
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
shadows: labelShadows,
|
color: Colors.white,
|
||||||
),
|
shadows: labelShadows,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
@@ -339,7 +354,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity:
|
||||||
|
VisualDensity(horizontal: -4, vertical: -4),
|
||||||
),
|
),
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@@ -399,7 +415,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
Symbols.circle,
|
Symbols.circle,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey,
|
color: (_status?.isOnline ?? false)
|
||||||
|
? Colors.green
|
||||||
|
: Colors.grey,
|
||||||
).padding(all: 4),
|
).padding(all: 4),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(
|
Text(
|
||||||
@@ -409,7 +427,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
: 'accountStatusOffline'.tr()
|
: 'accountStatusOffline'.tr()
|
||||||
: 'loading'.tr(),
|
: 'loading'.tr(),
|
||||||
),
|
),
|
||||||
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null)
|
if (_status != null &&
|
||||||
|
!_status!.isOnline &&
|
||||||
|
_status!.lastSeenAt != null)
|
||||||
Text(
|
Text(
|
||||||
'accountStatusLastSeen'.tr(args: [
|
'accountStatusLastSeen'.tr(args: [
|
||||||
_status!.lastSeenAt != null
|
_status!.lastSeenAt != null
|
||||||
@@ -429,11 +449,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
(ele) => Tooltip(
|
(ele) => Tooltip(
|
||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
TextSpan(
|
||||||
|
text: kBadgesMeta[ele.type]?.$1.tr() ??
|
||||||
|
'unknown'.tr()),
|
||||||
if (ele.metadata['title'] != null)
|
if (ele.metadata['title'] != null)
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '\n${ele.metadata['title']}',
|
text: '\n${ele.metadata['title']}',
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
TextSpan(text: '\n'),
|
TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
@@ -442,7 +465,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
|
kBadgesMeta[ele.type]?.$2 ??
|
||||||
|
Symbols.question_mark,
|
||||||
color: kBadgesMeta[ele.type]?.$3,
|
color: kBadgesMeta[ele.type]?.$3,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
),
|
),
|
||||||
@@ -458,7 +482,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.calendar_add_on),
|
const Icon(Symbols.calendar_add_on),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]),
|
Text('publisherJoinedAt').tr(args: [
|
||||||
|
DateFormat('y/M/d').format(_account!.createdAt)
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -491,17 +517,24 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.star),
|
const Icon(Symbols.star),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
|
Text(
|
||||||
|
'Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
|
Text(calcLevelUpProgressLevel(
|
||||||
|
_account?.profile?.experience ?? 0))
|
||||||
|
.fontSize(11)
|
||||||
|
.opacity(0.5),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
constraints: const BoxConstraints(maxWidth: 160),
|
constraints: const BoxConstraints(maxWidth: 160),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: calcLevelUpProgress(_account?.profile?.experience ?? 0),
|
value: calcLevelUpProgress(
|
||||||
|
_account?.profile?.experience ?? 0),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
backgroundColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer,
|
||||||
).alignment(Alignment.centerLeft),
|
).alignment(Alignment.centerLeft),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -514,21 +547,23 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
SliverToBoxAdapter(child: const Divider()),
|
SliverToBoxAdapter(child: const Divider()),
|
||||||
const SliverGap(12),
|
const SliverGap(12),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: FutureBuilder<List<SnCheckInRecord>>(
|
child: Builder(
|
||||||
future: _getCheckInRecords(),
|
builder: (context) {
|
||||||
builder: (context, snapshot) {
|
if (_records == null) return const SizedBox.shrink();
|
||||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
if (_records!.length <= 1) {
|
||||||
if (snapshot.data!.length <= 1) {
|
|
||||||
return Text(
|
return Text(
|
||||||
'accountCheckInNoRecords',
|
'accountCheckInNoRecords',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8);
|
)
|
||||||
|
.tr()
|
||||||
|
.fontWeight(FontWeight.bold)
|
||||||
|
.center()
|
||||||
|
.padding(horizontal: 20, vertical: 8);
|
||||||
}
|
}
|
||||||
final records = snapshot.data!;
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 240,
|
height: 240,
|
||||||
child: CheckInRecordChart(records: records),
|
child: CheckInRecordChart(records: _records!),
|
||||||
).padding(
|
).padding(
|
||||||
right: 24,
|
right: 24,
|
||||||
left: 16,
|
left: 16,
|
||||||
@@ -544,7 +579,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
Text('accountBadge')
|
||||||
|
.bold()
|
||||||
|
.fontSize(17)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20, bottom: 4),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 80,
|
height: 80,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -558,7 +597,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
child: Card(
|
child: Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
|
kBadgesMeta[badge.type]?.$2 ??
|
||||||
|
Symbols.question_mark,
|
||||||
color: kBadgesMeta[badge.type]?.$3,
|
color: kBadgesMeta[badge.type]?.$3,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
),
|
),
|
||||||
@@ -568,7 +608,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
subtitle: badge.metadata['title'] != null
|
subtitle: badge.metadata['title'] != null
|
||||||
? Text(badge.metadata['title'])
|
? Text(badge.metadata['title'])
|
||||||
: Text(
|
: Text(
|
||||||
DateFormat('y/M/d').format(badge.createdAt),
|
DateFormat('y/M/d')
|
||||||
|
.format(badge.createdAt),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -664,7 +705,8 @@ class CheckInRecordChart extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh,
|
getTooltipColor: (_) =>
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
titlesData: FlTitlesData(
|
titlesData: FlTitlesData(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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:responsive_framework/responsive_framework.dart';
|
||||||
@@ -41,6 +42,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Future<void> _fetchWhatsNew() async {
|
Future<void> _fetchWhatsNew() async {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/im/whats-new');
|
final resp = await sn.client.get('/cgi/im/whats-new');
|
||||||
|
if (resp.data == null) return;
|
||||||
final List<dynamic> out = resp.data;
|
final List<dynamic> out = resp.data;
|
||||||
setState(() {
|
setState(() {
|
||||||
_unreadCounts = {for (var v in out) v['channel_id']: v['count']};
|
_unreadCounts = {for (var v in out) v['channel_id']: v['count']};
|
||||||
@@ -72,18 +74,20 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
final idSet = <int>{};
|
||||||
for (final channel in channels) {
|
for (final channel in channels) {
|
||||||
if (channel.type == 1) {
|
if (channel.type == 1) {
|
||||||
await ud.listAccount(
|
idSet.addAll(
|
||||||
channel.members
|
channel.members
|
||||||
?.cast<SnChannelMember?>()
|
?.cast<SnChannelMember?>()
|
||||||
.map((ele) => ele?.accountId)
|
.map((ele) => ele?.accountId)
|
||||||
.where((ele) => ele != null)
|
.where((ele) => ele != null)
|
||||||
.toSet() ??
|
.cast<int>() ??
|
||||||
{},
|
[],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (idSet.isNotEmpty) await ud.listAccount(idSet);
|
||||||
|
|
||||||
if (mounted) setState(() => _channels = channels);
|
if (mounted) setState(() => _channels = channels);
|
||||||
})
|
})
|
||||||
@@ -135,6 +139,28 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_fetchWhatsNew();
|
_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
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
@@ -258,17 +284,42 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
channel.name),
|
channel.name),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (_unreadCounts?[channel.id] != null)
|
if (_unreadCounts?[channel.id] != null &&
|
||||||
|
_unreadCounts![channel.id]! > 0)
|
||||||
Badge(
|
Badge(
|
||||||
label: Text('${_unreadCounts![channel.id]}'),
|
label: Text('${_unreadCounts![channel.id]}'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: lastMessage != null
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Row(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
children: [
|
||||||
maxLines: 1,
|
Expanded(
|
||||||
overflow: TextOverflow.ellipsis,
|
child: Text(
|
||||||
|
lastMessage.body['text'] ??
|
||||||
|
'Unable preview',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
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(
|
: Text(
|
||||||
channel.description,
|
channel.description,
|
||||||
@@ -283,19 +334,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
?.avatar,
|
?.avatar,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (doExpand) {
|
_onTapChannel(channel);
|
||||||
setState(() => _focusChannel = channel);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'chatRoom',
|
|
||||||
pathParameters: {
|
|
||||||
'scope': channel.realm?.alias ?? 'global',
|
|
||||||
'alias': channel.alias,
|
|
||||||
},
|
|
||||||
).then((value) {
|
|
||||||
if (mounted) _refreshChannels(noRemote: true);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -305,17 +344,54 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(channel.name)),
|
Expanded(child: Text(channel.name)),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (_unreadCounts?[channel.id] != null)
|
if (_unreadCounts?[channel.id] != null &&
|
||||||
|
_unreadCounts![channel.id]! > 0)
|
||||||
Badge(
|
Badge(
|
||||||
label: Text('${_unreadCounts![channel.id]}'),
|
label: Text('${_unreadCounts![channel.id]}'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: lastMessage != null
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Row(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
children: [
|
||||||
maxLines: 1,
|
Badge(
|
||||||
overflow: TextOverflow.ellipsis,
|
label: Text(ud
|
||||||
|
.getAccountFromCache(
|
||||||
|
lastMessage.sender.accountId)
|
||||||
|
?.nick ??
|
||||||
|
'unknown'.tr()),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
textColor:
|
||||||
|
Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
const Gap(6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
lastMessage.body['text'] ??
|
||||||
|
'Unable preview',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
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(
|
: Text(
|
||||||
channel.description,
|
channel.description,
|
||||||
@@ -325,23 +401,16 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 16),
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: null,
|
content: channel.realm?.avatar,
|
||||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (doExpand) {
|
if (doExpand) {
|
||||||
|
_unreadCounts?[channel.id] = 0;
|
||||||
setState(() => _focusChannel = channel);
|
setState(() => _focusChannel = channel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GoRouter.of(context).pushNamed(
|
_onTapChannel(channel);
|
||||||
'chatRoom',
|
|
||||||
pathParameters: {
|
|
||||||
'scope': channel.realm?.alias ?? 'global',
|
|
||||||
'alias': channel.alias,
|
|
||||||
},
|
|
||||||
).then((value) {
|
|
||||||
if (value == true) _refreshChannels(noRemote: true);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,10 +490,12 @@ 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(
|
||||||
'take': 10,
|
'/cgi/im/channels/${widget.channel.keyPath}/members',
|
||||||
'offset': _members.length,
|
queryParameters: {
|
||||||
});
|
'take': 10,
|
||||||
|
'offset': _members.length,
|
||||||
|
});
|
||||||
final out = List<SnChannelMember>.from(
|
final out = List<SnChannelMember>.from(
|
||||||
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
|
resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
@@ -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,21 +144,15 @@ 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
|
icon: const Icon(Symbols.arrow_right_alt),
|
||||||
: IconButton(
|
onPressed: () {
|
||||||
icon: const Icon(Symbols.arrow_right_alt),
|
showModalBottomSheet(
|
||||||
onPressed: () {
|
context: context,
|
||||||
final model = UpdateModel(
|
builder: (context) => VersionUpdatePopup(),
|
||||||
'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());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -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(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
167
lib/screens/logging.dart
Normal file
167
lib/screens/logging.dart
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/logger.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
import 'package:talker_dio_logger/dio_logs.dart';
|
||||||
|
import 'package:talker_flutter/talker_flutter.dart';
|
||||||
|
|
||||||
|
final Map<LogLevel, IconData> kLogLevelIcons = {
|
||||||
|
LogLevel.error: Symbols.error,
|
||||||
|
LogLevel.critical: Symbols.error,
|
||||||
|
LogLevel.warning: Symbols.warning,
|
||||||
|
LogLevel.info: Symbols.info,
|
||||||
|
LogLevel.debug: Symbols.info_i,
|
||||||
|
LogLevel.verbose: Symbols.info_i,
|
||||||
|
};
|
||||||
|
|
||||||
|
final Map<LogLevel, bool> kLogLevelFilled = {
|
||||||
|
LogLevel.error: false,
|
||||||
|
LogLevel.critical: true,
|
||||||
|
LogLevel.warning: true,
|
||||||
|
LogLevel.info: true,
|
||||||
|
LogLevel.debug: false,
|
||||||
|
LogLevel.verbose: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
class DebugLoggingScreen extends StatelessWidget {
|
||||||
|
const DebugLoggingScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final talkerTheme = TalkerScreenTheme.fromTheme(Theme.of(context));
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('debugLogging').tr(),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
logging.cleanHistory();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.delete),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
reverse: true,
|
||||||
|
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
|
||||||
|
itemCount: logging.history.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final log = logging.history[index];
|
||||||
|
final color = log.getFlutterColor(talkerTheme);
|
||||||
|
return ListTile(
|
||||||
|
minTileHeight: 0,
|
||||||
|
tileColor: color.withOpacity(0.2),
|
||||||
|
leading: Icon(
|
||||||
|
kLogLevelIcons[log.logLevel ?? LogLevel.debug] ?? Symbols.help,
|
||||||
|
color: color,
|
||||||
|
fill: (kLogLevelFilled[log.logLevel ?? LogLevel.debug] ?? false)
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
|
),
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (log is DioRequestLog)
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${log.requestOptions.method} ${log.displayMessage}',
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
),
|
||||||
|
if (log.requestOptions.data != null)
|
||||||
|
Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: ExpansionTile(
|
||||||
|
title: Text('Payload').fontSize(13),
|
||||||
|
minTileHeight: 0,
|
||||||
|
tilePadding: EdgeInsets.zero,
|
||||||
|
expandedCrossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
log.requestOptions.data.toString(),
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else if (log is DioResponseLog)
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${log.response.statusCode} ${log.displayMessage}',
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
),
|
||||||
|
if (log.response.data != null)
|
||||||
|
Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: ExpansionTile(
|
||||||
|
title: Text('Payload').fontSize(13),
|
||||||
|
minTileHeight: 0,
|
||||||
|
tilePadding: EdgeInsets.zero,
|
||||||
|
expandedCrossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
log.response.data.toString(),
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
log.displayMessage,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
),
|
||||||
|
if (log.exception != null)
|
||||||
|
Text(
|
||||||
|
log.displayException,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
).bold(),
|
||||||
|
if (log.error != null)
|
||||||
|
Text(
|
||||||
|
log.displayException,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
).bold(),
|
||||||
|
if (log.stackTrace != null)
|
||||||
|
Text(
|
||||||
|
log.displayStackTrace,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||||
|
).padding(top: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'${(log.title?.replaceAll('-', ' ') ?? 'default').capitalizeEachWord()} · ${log.displayTime()}',
|
||||||
|
).fontSize(11),
|
||||||
|
onTap: () {
|
||||||
|
Clipboard.setData(
|
||||||
|
ClipboardData(
|
||||||
|
text: log.generateTextMessage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ const Map<String, IconData> kNotificationTopicIcons = {
|
|||||||
'passport.security.otp': Symbols.password,
|
'passport.security.otp': Symbols.password,
|
||||||
'interactive.subscription': Symbols.subscriptions,
|
'interactive.subscription': Symbols.subscriptions,
|
||||||
'interactive.feedback': Symbols.add_reaction,
|
'interactive.feedback': Symbols.add_reaction,
|
||||||
|
'interactive.reply': Symbols.reply,
|
||||||
'messaging.callStart': Symbols.call_received,
|
'messaging.callStart': Symbols.call_received,
|
||||||
'wallet.transaction.new': Symbols.receipt,
|
'wallet.transaction.new': Symbols.receipt,
|
||||||
};
|
};
|
||||||
@@ -57,10 +58,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final nty = context.read<NotificationProvider>();
|
final nty = context.read<NotificationProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/notifications?take=10');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/id/notifications', queryParameters: {
|
||||||
|
'take': 10,
|
||||||
|
'offset': _notifications.length,
|
||||||
|
});
|
||||||
_totalCount = resp.data['count'];
|
_totalCount = resp.data['count'];
|
||||||
_notifications.addAll(
|
_notifications.addAll(
|
||||||
resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? [],
|
resp.data['data']
|
||||||
|
?.map((e) => SnNotification.fromJson(e))
|
||||||
|
.cast<SnNotification>() ??
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
nty.updateTray();
|
nty.updateTray();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -186,7 +194,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
},
|
},
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
|
hasReachedMax: _totalCount != null &&
|
||||||
|
_notifications.length >= _totalCount!,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
final nty = _notifications[idx];
|
final nty = _notifications[idx];
|
||||||
return Row(
|
return Row(
|
||||||
@@ -218,13 +227,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
isAutoWarp: true,
|
isAutoWarp: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (['interactive.reply', 'interactive.feedback', 'interactive.subscription']
|
if ([
|
||||||
.contains(nty.topic) &&
|
'interactive.reply',
|
||||||
|
'interactive.feedback',
|
||||||
|
'interactive.subscription'
|
||||||
|
].contains(nty.topic) &&
|
||||||
nty.metadata['related_post'] != null)
|
nty.metadata['related_post'] != null)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: Container(
|
child: Container(
|
||||||
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,
|
||||||
@@ -243,7 +256,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postDetail',
|
'postDetail',
|
||||||
pathParameters: {
|
pathParameters: {
|
||||||
'slug': nty.metadata['related_post']!['id'].toString(),
|
'slug': nty
|
||||||
|
.metadata['related_post']!['id']
|
||||||
|
.toString(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -272,8 +287,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.check),
|
icon: const Icon(Symbols.check),
|
||||||
padding: EdgeInsets.all(0),
|
padding: EdgeInsets.all(0),
|
||||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity:
|
||||||
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
|
const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
|
onPressed:
|
||||||
|
_isSubmitting ? null : () => _markOneAsRead(nty),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16);
|
).padding(horizontal: 16);
|
||||||
|
|||||||
@@ -95,8 +95,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
final beforeId = config.prefs.getInt('int_last_publisher_id');
|
final beforeId = config.prefs.getInt('int_last_publisher_id');
|
||||||
_writeController
|
_writeController.setPublisher(
|
||||||
.setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull);
|
_publishers?.where((ele) => ele.id == beforeId).firstOrNull ??
|
||||||
|
_publishers?.firstOrNull);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -125,7 +126,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
|
|
||||||
final HotKey _pasteHotKey = HotKey(
|
final HotKey _pasteHotKey = HotKey(
|
||||||
key: PhysicalKeyboardKey.keyV,
|
key: PhysicalKeyboardKey.keyV,
|
||||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
modifiers: [
|
||||||
|
(!kIsWeb && Platform.isMacOS)
|
||||||
|
? HotKeyModifier.meta
|
||||||
|
: HotKeyModifier.control
|
||||||
|
],
|
||||||
scope: HotKeyScope.inapp,
|
scope: HotKeyScope.inapp,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -232,7 +237,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
if (widget.extraProps != null) {
|
if (widget.extraProps != null) {
|
||||||
_writeController.contentController.text = widget.extraProps!.text ?? '';
|
_writeController.contentController.text = widget.extraProps!.text ?? '';
|
||||||
_writeController.titleController.text = widget.extraProps!.title ?? '';
|
_writeController.titleController.text = widget.extraProps!.title ?? '';
|
||||||
_writeController.descriptionController.text = widget.extraProps!.description ?? '';
|
_writeController.descriptionController.text =
|
||||||
|
widget.extraProps!.description ?? '';
|
||||||
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
|
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,7 +259,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
text: TextSpan(children: [
|
text: TextSpan(children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(),
|
text: _writeController.title.isNotEmpty
|
||||||
|
? _writeController.title
|
||||||
|
: 'untitled'.tr(),
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
),
|
),
|
||||||
@@ -280,7 +288,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
children: [
|
children: [
|
||||||
if (_writeController.editingPost != null)
|
if (_writeController.editingPost != null)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
|
padding: const EdgeInsets.only(
|
||||||
|
top: 4, bottom: 4, left: 20, right: 20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
@@ -294,13 +303,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.edit, size: 16),
|
const Icon(Icons.edit, size: 16),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text('postEditingNotice').tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
Text('postEditingNotice').tr(args: [
|
||||||
|
'@${_writeController.editingPost!.publisher.name}'
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_writeController.replyingPost != null)
|
if (_writeController.replyingPost != null)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
|
padding: const EdgeInsets.only(
|
||||||
|
top: 4, bottom: 4, left: 20, right: 20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
@@ -314,7 +326,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.reply, size: 16),
|
const Icon(Symbols.reply, size: 16),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text('@${_writeController.replyingPost!.publisher.name}').bold(),
|
Text('@${_writeController.replyingPost!.publisher.name}')
|
||||||
|
.bold(),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -328,7 +341,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
),
|
),
|
||||||
if (_writeController.repostingPost != null)
|
if (_writeController.repostingPost != null)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
|
padding: const EdgeInsets.only(
|
||||||
|
top: 4, bottom: 4, left: 20, right: 20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
@@ -342,7 +356,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.forward, size: 16),
|
const Icon(Symbols.forward, size: 16),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text('@${_writeController.repostingPost!.publisher.name}').bold(),
|
Text('@${_writeController.repostingPost!.publisher.name}')
|
||||||
|
.bold(),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -384,7 +399,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
})
|
})
|
||||||
.padding(top: 8),
|
.padding(top: 8),
|
||||||
),
|
),
|
||||||
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
|
if (_writeController.attachments.isNotEmpty ||
|
||||||
|
_writeController.thumbnail != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -393,16 +409,19 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
attachments: _writeController.attachments,
|
attachments: _writeController.attachments,
|
||||||
isBusy: _writeController.isBusy,
|
isBusy: _writeController.isBusy,
|
||||||
onUpload: (int idx) async {
|
onUpload: (int idx) async {
|
||||||
await _writeController.uploadSingleAttachment(context, idx);
|
await _writeController.uploadSingleAttachment(
|
||||||
|
context, idx);
|
||||||
},
|
},
|
||||||
onInsertLink: (int idx) async {
|
onInsertLink: (int idx) async {
|
||||||
_writeController.contentController.text +=
|
_writeController.contentController.text +=
|
||||||
'\n';
|
'\n';
|
||||||
},
|
},
|
||||||
onUpdate: (int idx, PostWriteMedia updatedMedia) async {
|
onUpdate:
|
||||||
|
(int idx, PostWriteMedia updatedMedia) async {
|
||||||
_writeController.setIsBusy(true);
|
_writeController.setIsBusy(true);
|
||||||
try {
|
try {
|
||||||
_writeController.setAttachmentAt(idx, updatedMedia);
|
_writeController.setAttachmentAt(
|
||||||
|
idx, updatedMedia);
|
||||||
} finally {
|
} finally {
|
||||||
_writeController.setIsBusy(false);
|
_writeController.setIsBusy(false);
|
||||||
}
|
}
|
||||||
@@ -415,7 +434,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
_writeController.setIsBusy(false);
|
_writeController.setIsBusy(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUpdateBusy: (state) => _writeController.setIsBusy(state),
|
onUpdateBusy: (state) =>
|
||||||
|
_writeController.setIsBusy(state),
|
||||||
).padding(bottom: 8),
|
).padding(bottom: 8),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -426,11 +446,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (_writeController.isBusy && _writeController.progress != null)
|
if (_writeController.isBusy &&
|
||||||
|
_writeController.progress != null)
|
||||||
TweenAnimationBuilder<double>(
|
TweenAnimationBuilder<double>(
|
||||||
tween: Tween(begin: 0, end: _writeController.progress),
|
tween: Tween(begin: 0, end: _writeController.progress),
|
||||||
duration: Duration(milliseconds: 300),
|
duration: Duration(milliseconds: 300),
|
||||||
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
|
builder: (context, value, _) =>
|
||||||
|
LinearProgressIndicator(value: value, minHeight: 2),
|
||||||
)
|
)
|
||||||
else if (_writeController.isBusy)
|
else if (_writeController.isBusy)
|
||||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||||
@@ -439,12 +461,14 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
Container(
|
Container(
|
||||||
child: _writeController.temporaryRestored
|
child: _writeController.temporaryRestored
|
||||||
? Container(
|
? Container(
|
||||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
|
padding: const EdgeInsets.only(
|
||||||
|
top: 4, bottom: 4, left: 28, right: 22),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
width: 1 /
|
||||||
|
MediaQuery.of(context).devicePixelRatio,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -453,7 +477,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.restore, size: 20),
|
const Icon(Icons.restore, size: 20),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Expanded(child: Text('postLocalDraftRestored').tr()),
|
Expanded(
|
||||||
|
child:
|
||||||
|
Text('postLocalDraftRestored').tr()),
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Text('dialogDismiss').tr(),
|
child: Text('dialogDismiss').tr(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -464,8 +490,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
))
|
))
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
)
|
)
|
||||||
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
|
.height(_writeController.temporaryRestored ? 32 : 0,
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
|
animate: true)
|
||||||
|
.animate(const Duration(milliseconds: 300),
|
||||||
|
Curves.fastLinearToSlowEaseIn),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -485,11 +513,18 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
),
|
),
|
||||||
if (_writeController.mode == 'stories')
|
if (_writeController.mode == 'stories')
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Symbols.poll, color: Theme.of(context).colorScheme.primary),
|
icon: Icon(Symbols.poll,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary),
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: _writeController.poll == null
|
backgroundColor:
|
||||||
? null
|
_writeController.poll == null
|
||||||
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
|
? null
|
||||||
|
: WidgetStatePropertyAll(
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showPollEditorDialog();
|
_showPollEditorDialog();
|
||||||
@@ -497,14 +532,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
),
|
),
|
||||||
if (_writeController.mode == 'articles')
|
if (_writeController.mode == 'articles')
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Symbols.full_coverage, color: Theme.of(context).colorScheme.primary),
|
icon: Icon(Symbols.full_coverage,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary),
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: _writeController.thumbnail == null
|
backgroundColor:
|
||||||
? null
|
_writeController.thumbnail == null
|
||||||
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
|
? null
|
||||||
|
: WidgetStatePropertyAll(
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_writeController.thumbnail != null) {
|
if (_writeController.thumbnail !=
|
||||||
|
null) {
|
||||||
_writeController.setThumbnail(null);
|
_writeController.setThumbnail(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -517,7 +560,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: (_writeController.isBusy || _writeController.publisher == null)
|
onPressed: (_writeController.isBusy ||
|
||||||
|
_writeController.publisher == null)
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
_writeController.sendPost(context).then((_) {
|
_writeController.sendPost(context).then((_) {
|
||||||
@@ -556,7 +600,8 @@ class _PostPublisherPopup extends StatelessWidget {
|
|||||||
final List<SnPublisher>? publishers;
|
final List<SnPublisher>? publishers;
|
||||||
final Function onUpdate;
|
final Function onUpdate;
|
||||||
|
|
||||||
const _PostPublisherPopup({required this.controller, this.publishers, required this.onUpdate});
|
const _PostPublisherPopup(
|
||||||
|
{required this.controller, this.publishers, required this.onUpdate});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -568,7 +613,9 @@ class _PostPublisherPopup extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.face, size: 24),
|
const Icon(Symbols.face, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(),
|
Text('accountPublishers',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge)
|
||||||
|
.tr(),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -612,7 +659,8 @@ class _PostRealmPopup extends StatelessWidget {
|
|||||||
final List<SnRealm>? realms;
|
final List<SnRealm>? realms;
|
||||||
final Function onUpdate;
|
final Function onUpdate;
|
||||||
|
|
||||||
const _PostRealmPopup({required this.controller, this.realms, required this.onUpdate});
|
const _PostRealmPopup(
|
||||||
|
{required this.controller, this.realms, required this.onUpdate});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -624,7 +672,8 @@ class _PostRealmPopup extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.face, size: 24),
|
const Icon(Symbols.face, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('accountRealms', style: Theme.of(context).textTheme.titleLarge).tr(),
|
Text('accountRealms', style: Theme.of(context).textTheme.titleLarge)
|
||||||
|
.tr(),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -665,7 +714,8 @@ class _PostStoryEditor extends StatelessWidget {
|
|||||||
final Function? onTapPublisher;
|
final Function? onTapPublisher;
|
||||||
final Function? onTapRealm;
|
final Function? onTapRealm;
|
||||||
|
|
||||||
const _PostStoryEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
const _PostStoryEditor(
|
||||||
|
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -717,7 +767,8 @@ class _PostStoryEditor extends StatelessWidget {
|
|||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -732,8 +783,10 @@ class _PostStoryEditor extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
contentInsertionConfiguration:
|
||||||
|
controller.contentInsertionConfiguration,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -749,7 +802,8 @@ class _PostArticleEditor extends StatelessWidget {
|
|||||||
final Function? onTapPublisher;
|
final Function? onTapPublisher;
|
||||||
final Function? onTapRealm;
|
final Function? onTapRealm;
|
||||||
|
|
||||||
const _PostArticleEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
const _PostArticleEditor(
|
||||||
|
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -857,8 +911,10 @@ class _PostArticleEditor extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
contentInsertionConfiguration:
|
||||||
|
controller.contentInsertionConfiguration,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@@ -893,7 +949,8 @@ class _PostArticleEditor extends StatelessWidget {
|
|||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
contentInsertionConfiguration:
|
||||||
|
controller.contentInsertionConfiguration,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -906,7 +963,8 @@ class _PostQuestionEditor extends StatelessWidget {
|
|||||||
final Function? onTapPublisher;
|
final Function? onTapPublisher;
|
||||||
final Function? onTapRealm;
|
final Function? onTapRealm;
|
||||||
|
|
||||||
const _PostQuestionEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
const _PostQuestionEditor(
|
||||||
|
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -958,7 +1016,8 @@ class _PostQuestionEditor extends StatelessWidget {
|
|||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -969,7 +1028,8 @@ class _PostQuestionEditor extends StatelessWidget {
|
|||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -984,8 +1044,10 @@ class _PostQuestionEditor extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
contentInsertionConfiguration:
|
||||||
|
controller.contentInsertionConfiguration,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1001,7 +1063,8 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
final Function? onTapPublisher;
|
final Function? onTapPublisher;
|
||||||
final Function? onTapRealm;
|
final Function? onTapRealm;
|
||||||
|
|
||||||
const _PostVideoEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
const _PostVideoEditor(
|
||||||
|
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||||
|
|
||||||
void _selectVideo(BuildContext context) async {
|
void _selectVideo(BuildContext context) async {
|
||||||
final video = await showDialog<SnAttachment?>(
|
final video = await showDialog<SnAttachment?>(
|
||||||
@@ -1022,7 +1085,8 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
|
|
||||||
final result = await showDialog<SnAttachment?>(
|
final result = await showDialog<SnAttachment?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)),
|
builder: (context) => PendingAttachmentAltDialog(
|
||||||
|
media: PostWriteMedia(controller.videoAttachment)),
|
||||||
);
|
);
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
|
|
||||||
@@ -1034,7 +1098,8 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
|
|
||||||
final result = await showDialog<SnAttachmentBoost?>(
|
final result = await showDialog<SnAttachmentBoost?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)),
|
builder: (context) => PendingAttachmentBoostDialog(
|
||||||
|
media: PostWriteMedia(controller.videoAttachment)),
|
||||||
);
|
);
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
|
|
||||||
@@ -1077,7 +1142,8 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
|
await sn.client
|
||||||
|
.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
|
||||||
controller.setVideoAttachment(null);
|
controller.setVideoAttachment(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -1087,143 +1153,159 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Container(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
Column(
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
children: [
|
child: Row(
|
||||||
Material(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
elevation: 2,
|
children: [
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
Column(
|
||||||
child: GestureDetector(
|
children: [
|
||||||
onTap: () {
|
Material(
|
||||||
onTapPublisher?.call();
|
elevation: 2,
|
||||||
},
|
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||||
child: AccountImage(
|
child: GestureDetector(
|
||||||
content: controller.publisher?.avatar,
|
onTap: () {
|
||||||
|
onTapPublisher?.call();
|
||||||
|
},
|
||||||
|
child: AccountImage(
|
||||||
|
content: controller.publisher?.avatar,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Gap(11),
|
||||||
const Gap(11),
|
Material(
|
||||||
Material(
|
elevation: 1,
|
||||||
elevation: 1,
|
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
child: GestureDetector(
|
||||||
child: GestureDetector(
|
onTap: () {
|
||||||
onTap: () {
|
onTapRealm?.call();
|
||||||
onTapRealm?.call();
|
},
|
||||||
},
|
child: AccountImage(
|
||||||
child: AccountImage(
|
content: controller.realm?.avatar,
|
||||||
content: controller.realm?.avatar,
|
fallbackWidget: const Icon(Symbols.globe, size: 20),
|
||||||
fallbackWidget: const Icon(Symbols.globe, size: 20),
|
radius: 14,
|
||||||
radius: 14,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
TextField(
|
|
||||||
controller: controller.titleController,
|
|
||||||
decoration: InputDecoration.collapsed(
|
|
||||||
hintText: 'fieldPostTitle'.tr(),
|
|
||||||
border: InputBorder.none,
|
|
||||||
),
|
),
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
Expanded(
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
child: Column(
|
||||||
).padding(horizontal: 16),
|
children: [
|
||||||
const Gap(8),
|
const Gap(6),
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller.descriptionController,
|
controller: controller.titleController,
|
||||||
decoration: InputDecoration.collapsed(
|
decoration: InputDecoration.collapsed(
|
||||||
hintText: 'fieldPostDescription'.tr(),
|
hintText: 'fieldPostTitle'.tr(),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
maxLines: null,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
keyboardType: TextInputType.multiline,
|
onTapOutside: (_) =>
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
).padding(horizontal: 16),
|
||||||
).padding(horizontal: 16),
|
const Gap(8),
|
||||||
const Gap(12),
|
TextField(
|
||||||
Container(
|
controller: controller.descriptionController,
|
||||||
margin: const EdgeInsets.only(left: 16, right: 16),
|
decoration: InputDecoration.collapsed(
|
||||||
decoration: BoxDecoration(
|
hintText: 'fieldPostDescription'.tr(),
|
||||||
borderRadius: BorderRadius.circular(16),
|
border: InputBorder.none,
|
||||||
border: Border.all(color: Theme.of(context).dividerColor),
|
),
|
||||||
),
|
maxLines: null,
|
||||||
child: ContextMenuRegion(
|
keyboardType: TextInputType.multiline,
|
||||||
contextMenu: ContextMenu(
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
entries: [
|
onTapOutside: (_) =>
|
||||||
MenuItem(
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
label: 'attachmentSetAlt'.tr(),
|
).padding(horizontal: 16),
|
||||||
icon: Symbols.description,
|
const Gap(12),
|
||||||
onSelected: () {
|
Container(
|
||||||
_setAlt(context);
|
margin: const EdgeInsets.only(left: 16, right: 16),
|
||||||
},
|
decoration: BoxDecoration(
|
||||||
),
|
borderRadius: BorderRadius.circular(16),
|
||||||
MenuItem(
|
border: Border.all(color: Theme.of(context).dividerColor),
|
||||||
label: 'attachmentBoost'.tr(),
|
),
|
||||||
icon: Symbols.bolt,
|
child: ContextMenuRegion(
|
||||||
onSelected: () {
|
contextMenu: ContextMenu(
|
||||||
_createBoost(context);
|
entries: [
|
||||||
},
|
MenuItem(
|
||||||
),
|
label: 'attachmentSetAlt'.tr(),
|
||||||
MenuItem(
|
icon: Symbols.description,
|
||||||
label: 'attachmentSetThumbnail'.tr(),
|
onSelected: () {
|
||||||
icon: Symbols.image,
|
_setAlt(context);
|
||||||
onSelected: () {
|
},
|
||||||
_setThumbnail(context);
|
),
|
||||||
},
|
MenuItem(
|
||||||
),
|
label: 'attachmentBoost'.tr(),
|
||||||
MenuItem(
|
icon: Symbols.bolt,
|
||||||
label: 'attachmentCopyRandomId'.tr(),
|
onSelected: () {
|
||||||
icon: Symbols.content_copy,
|
_createBoost(context);
|
||||||
onSelected: () {
|
},
|
||||||
Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid));
|
),
|
||||||
},
|
MenuItem(
|
||||||
),
|
label: 'attachmentSetThumbnail'.tr(),
|
||||||
MenuItem(
|
icon: Symbols.image,
|
||||||
label: 'delete'.tr(),
|
onSelected: () {
|
||||||
icon: Symbols.delete,
|
_setThumbnail(context);
|
||||||
onSelected: () => _deleteAttachment(context),
|
},
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'unlink'.tr(),
|
label: 'attachmentCopyRandomId'.tr(),
|
||||||
icon: Symbols.link_off,
|
icon: Symbols.content_copy,
|
||||||
onSelected: () {
|
onSelected: () {
|
||||||
controller.setVideoAttachment(null);
|
Clipboard.setData(ClipboardData(
|
||||||
},
|
text: controller.videoAttachment!.rid));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
label: 'delete'.tr(),
|
||||||
|
icon: Symbols.delete,
|
||||||
|
onSelected: () => _deleteAttachment(context),
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
label: 'unlink'.tr(),
|
||||||
|
icon: Symbols.link_off,
|
||||||
|
onSelected: () {
|
||||||
|
controller.setVideoAttachment(null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: controller.videoAttachment == null
|
||||||
|
? () => _selectVideo(context)
|
||||||
|
: null,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: controller.videoAttachment == null
|
||||||
|
? Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.add),
|
||||||
|
const Gap(4),
|
||||||
|
Text('postVideoUpload'.tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: controller.videoAttachment!,
|
||||||
|
heroTag: const Uuid().v4(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
onTap: controller.videoAttachment == null ? () => _selectVideo(context) : null,
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
child: controller.videoAttachment == null
|
|
||||||
? Center(
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.add),
|
|
||||||
const Gap(4),
|
|
||||||
Text('postVideoUpload'.tr()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
child: AttachmentItem(
|
|
||||||
data: controller.videoAttachment!,
|
|
||||||
heroTag: const Uuid().v4(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.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_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
@@ -24,6 +25,7 @@ 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,
|
||||||
@@ -47,6 +49,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
late final SharedPreferences _prefs;
|
late final SharedPreferences _prefs;
|
||||||
String _docBasepath = '/';
|
String _docBasepath = '/';
|
||||||
|
|
||||||
|
final TextEditingController _customFontController = TextEditingController();
|
||||||
final TextEditingController _serverUrlController = TextEditingController();
|
final TextEditingController _serverUrlController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -61,11 +64,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
final config = context.read<ConfigProvider>();
|
final config = context.read<ConfigProvider>();
|
||||||
_prefs = config.prefs;
|
_prefs = config.prefs;
|
||||||
_serverUrlController.text = config.serverUrl;
|
_serverUrlController.text = config.serverUrl;
|
||||||
|
if (_prefs.getString(kAppCustomFonts) != null) {
|
||||||
|
_customFontController.text = _prefs.getString(kAppCustomFonts) ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_serverUrlController.dispose();
|
_serverUrlController.dispose();
|
||||||
|
_customFontController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +336,47 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.font_download),
|
||||||
|
title: Text('settingsCustomFonts').tr(),
|
||||||
|
subtitle: Text('settingsCustomFontsDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 14),
|
||||||
|
trailing: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_prefs.remove(kAppCustomFonts);
|
||||||
|
context.showSnackbar('settingsCustomFontApplied'.tr());
|
||||||
|
final theme = context.read<ThemeProvider>();
|
||||||
|
_customFontController.clear();
|
||||||
|
theme.reloadTheme();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: _customFontController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'settingsCustomFontFamily'.tr(),
|
||||||
|
helperText: 'settingsCustomFontFamilyHint'.tr(),
|
||||||
|
prefixIcon: const Icon(Symbols.format_paint),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
onPressed: () {
|
||||||
|
_prefs.setString(
|
||||||
|
kAppCustomFonts,
|
||||||
|
_customFontController.text,
|
||||||
|
);
|
||||||
|
context.showSnackbar('settingsCustomFontApplied'.tr());
|
||||||
|
final theme = context.read<ThemeProvider>();
|
||||||
|
theme.reloadTheme();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
).padding(horizontal: 16, top: 8, bottom: 4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
@@ -533,6 +581,37 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
.fontSize(17)
|
.fontSize(17)
|
||||||
.tr()
|
.tr()
|
||||||
.padding(horizontal: 20, bottom: 4),
|
.padding(horizontal: 20, bottom: 4),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.home_storage),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('cacheSize').tr(),
|
||||||
|
subtitle: FutureBuilder(
|
||||||
|
future: DefaultCacheManager().store.getCacheSize(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData || kIsWeb) {
|
||||||
|
return Text('unknown').tr();
|
||||||
|
}
|
||||||
|
return Text(
|
||||||
|
snapshot.data!.formatBytes(),
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.cleaning_services),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
title: Text('cacheDelete').tr(),
|
||||||
|
subtitle: Text('cacheDeleteDescription').tr(),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
await DefaultCacheManager().emptyCache();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
context.showSnackbar('cacheDeleted'.tr());
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.database),
|
leading: const Icon(Symbols.database),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
@@ -604,6 +683,29 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
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(
|
||||||
|
title: Text('runtimeLogsOpen').tr(),
|
||||||
|
subtitle: Text('runtimeLogsDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.receipt_long),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
GoRouter.of(context).pushNamed('debugLogging');
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('settingsMiscAbout').tr(),
|
title: Text('settingsMiscAbout').tr(),
|
||||||
subtitle: Text('settingsMiscAboutDescription').tr(),
|
subtitle: Text('settingsMiscAboutDescription').tr(),
|
||||||
|
|||||||
@@ -179,7 +179,9 @@ class _StickerScreenState extends State<StickerScreen>
|
|||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
itemCount: _packs.length,
|
itemCount: _packs.length,
|
||||||
onFetchData: _fetchPacks,
|
onFetchData: _fetchPacks,
|
||||||
hasReachedMax: _totalCount != null && _packs.length >= _totalCount!,
|
hasReachedMax:
|
||||||
|
(_totalCount != null && _packs.length >= _totalCount!) ||
|
||||||
|
_tabController.index == 2,
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
final pack = _packs[idx];
|
final pack = _packs[idx];
|
||||||
|
|||||||
@@ -11,10 +11,19 @@ class ThemeSet {
|
|||||||
ThemeSet({required this.light, required this.dark});
|
ThemeSet({required this.light, required this.dark});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3}) async {
|
Future<ThemeSet> createAppThemeSet(
|
||||||
|
{Color? seedColorOverride, bool? useMaterial3, String? customFonts}) async {
|
||||||
return ThemeSet(
|
return ThemeSet(
|
||||||
light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3),
|
light: await createAppTheme(
|
||||||
dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3),
|
Brightness.light,
|
||||||
|
useMaterial3: useMaterial3,
|
||||||
|
customFonts: customFonts,
|
||||||
|
),
|
||||||
|
dark: await createAppTheme(
|
||||||
|
Brightness.dark,
|
||||||
|
useMaterial3: useMaterial3,
|
||||||
|
customFonts: customFonts,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,24 +31,35 @@ Future<ThemeData> createAppTheme(
|
|||||||
Brightness brightness, {
|
Brightness brightness, {
|
||||||
Color? seedColorOverride,
|
Color? seedColorOverride,
|
||||||
bool? useMaterial3,
|
bool? useMaterial3,
|
||||||
|
String? customFonts,
|
||||||
}) async {
|
}) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
final seedColorString = prefs.getInt(kAppColorSchemeStoreKey);
|
final seedColorString = prefs.getInt(kAppColorSchemeStoreKey);
|
||||||
final seedColor = seedColorString != null ? Color(seedColorString) : Colors.indigo;
|
final seedColor =
|
||||||
|
seedColorString != null ? Color(seedColorString) : Colors.indigo;
|
||||||
|
|
||||||
final colorScheme = ColorScheme.fromSeed(
|
final colorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: seedColorOverride ?? seedColor,
|
seedColor: seedColorOverride ?? seedColor,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
);
|
);
|
||||||
|
|
||||||
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
final hasAppBarTransparent =
|
||||||
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||||
|
final useM3 =
|
||||||
|
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||||
|
|
||||||
|
final inUseFonts = (customFonts ?? prefs.getString(kAppCustomFonts))
|
||||||
|
?.split(',')
|
||||||
|
.map((ele) => ele.trim())
|
||||||
|
.toList();
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: useM3,
|
useMaterial3: useM3,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
|
fontFamily: inUseFonts?.firstOrNull,
|
||||||
|
fontFamilyFallback: inUseFonts?.sublist(1),
|
||||||
iconTheme: IconThemeData(
|
iconTheme: IconThemeData(
|
||||||
fill: 0,
|
fill: 0,
|
||||||
weight: 400,
|
weight: 400,
|
||||||
@@ -52,8 +72,10 @@ Future<ThemeData> createAppTheme(
|
|||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
elevation: hasAppBarTransparent ? 0 : null,
|
elevation: hasAppBarTransparent ? 0 : null,
|
||||||
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
|
backgroundColor:
|
||||||
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
|
||||||
|
foregroundColor:
|
||||||
|
hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
pageTransitionsTheme: PageTransitionsTheme(
|
pageTransitionsTheme: PageTransitionsTheme(
|
||||||
builders: {
|
builders: {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class AccountImage extends StatelessWidget {
|
|||||||
final Color? foregroundColor;
|
final Color? foregroundColor;
|
||||||
final double? radius;
|
final double? radius;
|
||||||
final Widget? fallbackWidget;
|
final Widget? fallbackWidget;
|
||||||
|
final Widget? badge;
|
||||||
|
|
||||||
const AccountImage({
|
const AccountImage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -18,6 +19,7 @@ class AccountImage extends StatelessWidget {
|
|||||||
this.foregroundColor,
|
this.foregroundColor,
|
||||||
this.radius,
|
this.radius,
|
||||||
this.fallbackWidget,
|
this.fallbackWidget,
|
||||||
|
this.badge,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -26,26 +28,36 @@ class AccountImage extends StatelessWidget {
|
|||||||
final url = sn.getAttachmentUrl(content ?? '');
|
final url = sn.getAttachmentUrl(content ?? '');
|
||||||
|
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
return CircleAvatar(
|
return Stack(
|
||||||
key: Key('attachment-${content.hashCode}'),
|
children: [
|
||||||
radius: radius,
|
CircleAvatar(
|
||||||
backgroundColor: backgroundColor,
|
key: Key('attachment-${content.hashCode}'),
|
||||||
backgroundImage: (content?.isNotEmpty ?? false)
|
radius: radius,
|
||||||
? ResizeImage(
|
backgroundColor: backgroundColor,
|
||||||
UniversalImage.provider(url),
|
backgroundImage: (content?.isNotEmpty ?? false)
|
||||||
width: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
? ResizeImage(
|
||||||
height: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
UniversalImage.provider(url),
|
||||||
policy: ResizeImagePolicy.fit,
|
width: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
||||||
)
|
height: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
||||||
: null,
|
policy: ResizeImagePolicy.fit,
|
||||||
child: (content?.isEmpty ?? true)
|
)
|
||||||
? (fallbackWidget ??
|
: null,
|
||||||
Icon(
|
child: (content?.isEmpty ?? true)
|
||||||
Symbols.account_circle,
|
? (fallbackWidget ??
|
||||||
size: radius != null ? radius! * 1.2 : 24,
|
Icon(
|
||||||
color: foregroundColor,
|
Symbols.account_circle,
|
||||||
))
|
size: radius != null ? radius! * 1.2 : 24,
|
||||||
: null,
|
color: foregroundColor,
|
||||||
|
))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (badge != null)
|
||||||
|
Positioned(
|
||||||
|
right: -4,
|
||||||
|
bottom: -4,
|
||||||
|
child: badge!,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@@ -8,6 +7,7 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:video_compress/video_compress.dart';
|
import 'package:video_compress/video_compress.dart';
|
||||||
|
|
||||||
@@ -17,10 +17,12 @@ class PendingVideoCompressDialog extends StatefulWidget {
|
|||||||
const PendingVideoCompressDialog({super.key, required this.media});
|
const PendingVideoCompressDialog({super.key, required this.media});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PendingVideoCompressDialog> createState() => _PendingVideoCompressDialogState();
|
State<PendingVideoCompressDialog> createState() =>
|
||||||
|
_PendingVideoCompressDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog> {
|
class _PendingVideoCompressDialogState
|
||||||
|
extends State<PendingVideoCompressDialog> {
|
||||||
VideoQuality _quality = VideoQuality.DefaultQuality;
|
VideoQuality _quality = VideoQuality.DefaultQuality;
|
||||||
|
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
@@ -50,7 +52,7 @@ class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_progressSubscription = VideoCompress.compressProgress$.subscribe((event) {
|
_progressSubscription = VideoCompress.compressProgress$.subscribe((event) {
|
||||||
log('[Compress] Progress: $event');
|
logging.debug('[Paperclip.VideoCompress] Progress: $event');
|
||||||
setState(() {
|
setState(() {
|
||||||
_progress = event / 100;
|
_progress = event / 100;
|
||||||
_isBusy = event < 100;
|
_isBusy = event < 100;
|
||||||
@@ -132,7 +134,9 @@ class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('attachmentCompressQualityHint', style: Theme.of(context).textTheme.bodySmall!).tr(),
|
Text('attachmentCompressQualityHint',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall!)
|
||||||
|
.tr(),
|
||||||
if (_isBusy)
|
if (_isBusy)
|
||||||
TweenAnimationBuilder<double>(
|
TweenAnimationBuilder<double>(
|
||||||
tween: Tween(begin: 0, end: _progress ?? 0),
|
tween: Tween(begin: 0, end: _progress ?? 0),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.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/account/profile_page.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_popover.dart';
|
import 'package:surface/widgets/account/account_popover.dart';
|
||||||
@@ -105,6 +106,22 @@ class ChatMessage extends StatelessWidget {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: AccountImage(
|
child: AccountImage(
|
||||||
content: user?.avatar,
|
content: user?.avatar,
|
||||||
|
badge: (user?.badges.isNotEmpty ?? false)
|
||||||
|
? Icon(
|
||||||
|
kBadgesMeta[user!.badges.first.type]?.$2 ??
|
||||||
|
Symbols.question_mark,
|
||||||
|
color: kBadgesMeta[user.badges.first.type]?.$3,
|
||||||
|
fill: 1,
|
||||||
|
size: 18,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(1, 1),
|
||||||
|
blurRadius: 5.0,
|
||||||
|
color: Color.fromARGB(150, 0, 0, 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
@@ -161,7 +178,7 @@ 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:
|
borderRadius:
|
||||||
@@ -210,10 +227,12 @@ 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(
|
||||||
padding: padding.copyWith(top: 8, left: 48 + padding.left),
|
top: 8,
|
||||||
|
left: isCompact ? padding.left : 48 + padding.left,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (!hasMerged && !isCompact)
|
if (!hasMerged && !isCompact)
|
||||||
const Gap(12)
|
const Gap(12)
|
||||||
@@ -292,14 +311,11 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
buttonItems: items,
|
buttonItems: items,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: MarkdownTextContent(
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
content: data.body['text'],
|
||||||
child: MarkdownTextContent(
|
isAutoWarp: true,
|
||||||
content: data.body['text'],
|
isEnlargeSticker:
|
||||||
isAutoWarp: true,
|
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
|
||||||
isEnlargeSticker:
|
|
||||||
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (data.updatedAt != data.createdAt)
|
if (data.updatedAt != data.createdAt)
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ class ChatMessageInput extends StatefulWidget {
|
|||||||
final ChatMessageController controller;
|
final ChatMessageController controller;
|
||||||
final SnChannelMember? otherMember;
|
final SnChannelMember? otherMember;
|
||||||
|
|
||||||
const ChatMessageInput({super.key, required this.controller, this.otherMember});
|
const ChatMessageInput(
|
||||||
|
{super.key, required this.controller, this.otherMember});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatMessageInput> createState() => ChatMessageInputState();
|
State<ChatMessageInput> createState() => ChatMessageInputState();
|
||||||
@@ -45,12 +46,20 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
|
|
||||||
final HotKey _pasteHotKey = HotKey(
|
final HotKey _pasteHotKey = HotKey(
|
||||||
key: PhysicalKeyboardKey.keyV,
|
key: PhysicalKeyboardKey.keyV,
|
||||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
modifiers: [
|
||||||
|
(!kIsWeb && Platform.isMacOS)
|
||||||
|
? HotKeyModifier.meta
|
||||||
|
: HotKeyModifier.control
|
||||||
|
],
|
||||||
scope: HotKeyScope.inapp,
|
scope: HotKeyScope.inapp,
|
||||||
);
|
);
|
||||||
final HotKey _newLineHotKey = HotKey(
|
final HotKey _newLineHotKey = HotKey(
|
||||||
key: PhysicalKeyboardKey.enter,
|
key: PhysicalKeyboardKey.enter,
|
||||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
modifiers: [
|
||||||
|
(!kIsWeb && Platform.isMacOS)
|
||||||
|
? HotKeyModifier.meta
|
||||||
|
: HotKeyModifier.control
|
||||||
|
],
|
||||||
scope: HotKeyScope.inapp,
|
scope: HotKeyScope.inapp,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -100,7 +109,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
void setEdit(SnChatMessage? value) {
|
void setEdit(SnChatMessage? value) {
|
||||||
_contentController.text = value?.body['text'] ?? '';
|
_contentController.text = value?.body['text'] ?? '';
|
||||||
_attachments.clear();
|
_attachments.clear();
|
||||||
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
_attachments.addAll(
|
||||||
|
value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||||
setState(() => _editingMessage = value);
|
setState(() => _editingMessage = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +149,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
media.name,
|
media.name,
|
||||||
'messaging',
|
'messaging',
|
||||||
null,
|
null,
|
||||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
mimetype: media.raw != null && media.type == SnMediaType.image
|
||||||
|
? 'image/png'
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
final item = await attach.chunkedUploadParts(
|
final item = await attach.chunkedUploadParts(
|
||||||
@@ -171,7 +183,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
widget.controller.sendMessage(
|
widget.controller.sendMessage(
|
||||||
_editingMessage != null ? 'messages.edit' : 'messages.new',
|
_editingMessage != null ? 'messages.edit' : 'messages.new',
|
||||||
_contentController.text,
|
_contentController.text,
|
||||||
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
attachments: _attachments
|
||||||
|
.where((e) => e.attachment != null)
|
||||||
|
.map((e) => e.attachment!.rid)
|
||||||
|
.toList(),
|
||||||
relatedId: _editingMessage?.id,
|
relatedId: _editingMessage?.id,
|
||||||
quoteId: _replyingMessage?.id,
|
quoteId: _replyingMessage?.id,
|
||||||
editingMessage: _editingMessage,
|
editingMessage: _editingMessage,
|
||||||
@@ -232,12 +247,15 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
TweenAnimationBuilder<double>(
|
TweenAnimationBuilder<double>(
|
||||||
tween: Tween(begin: 0, end: _progress),
|
tween: Tween(begin: 0, end: _progress),
|
||||||
duration: Duration(milliseconds: 300),
|
duration: Duration(milliseconds: 300),
|
||||||
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
|
builder: (context, value, _) =>
|
||||||
|
LinearProgressIndicator(value: value, minHeight: 2),
|
||||||
)
|
)
|
||||||
else if (_isBusy)
|
else if (_isBusy)
|
||||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||||
Padding(
|
Padding(
|
||||||
padding: _attachments.isNotEmpty ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
padding: _attachments.isNotEmpty
|
||||||
|
? const EdgeInsets.only(top: 8)
|
||||||
|
: EdgeInsets.zero,
|
||||||
child: PostMediaPendingList(
|
child: PostMediaPendingList(
|
||||||
attachments: _attachments,
|
attachments: _attachments,
|
||||||
isBusy: _isBusy,
|
isBusy: _isBusy,
|
||||||
@@ -249,9 +267,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
},
|
},
|
||||||
onUpdateBusy: (state) => setState(() => _isBusy = state),
|
onUpdateBusy: (state) => setState(() => _isBusy = state),
|
||||||
),
|
),
|
||||||
)
|
).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate(
|
||||||
.height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true)
|
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: _replyingMessage != null
|
child: _replyingMessage != null
|
||||||
@@ -272,7 +289,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
|
_replyingMessage?.body['text'] ??
|
||||||
|
'${_replyingMessage?.sender.nick}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -289,9 +307,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
).padding(vertical: 8),
|
).padding(vertical: 8),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
)
|
).height(_replyingMessage != null ? 38 : 0, animate: true).animate(
|
||||||
.height(_replyingMessage != null ? 38 : 0, animate: true)
|
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: _editingMessage != null
|
child: _editingMessage != null
|
||||||
@@ -312,7 +329,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
|
_editingMessage?.body['text'] ??
|
||||||
|
'${_editingMessage?.sender.nick}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -330,12 +348,13 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
).padding(vertical: 8),
|
).padding(vertical: 8),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
)
|
).height(_editingMessage != null ? 38 : 0, animate: true).animate(
|
||||||
.height(_editingMessage != null ? 38 : 0, animate: true)
|
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
Container(
|
||||||
SizedBox(
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
height: 56,
|
constraints: BoxConstraints(minHeight: 56, maxHeight: 240),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
@@ -347,11 +366,14 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
? 'fieldChatMessageDirect'.tr(args: [
|
? 'fieldChatMessageDirect'.tr(args: [
|
||||||
'@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}',
|
'@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}',
|
||||||
])
|
])
|
||||||
: 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
|
: 'fieldChatMessage'.tr(args: [
|
||||||
|
widget.controller.channel?.name ?? 'loading'.tr()
|
||||||
|
]),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.send,
|
textInputAction: TextInputAction.send,
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
if (_isBusy) return;
|
if (_isBusy) return;
|
||||||
_sendMessage();
|
_sendMessage();
|
||||||
@@ -366,7 +388,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
Symbols.mood,
|
Symbols.mood,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity:
|
||||||
|
const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -386,13 +409,14 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
Symbols.send,
|
Symbols.send,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity:
|
||||||
|
const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).padding(horizontal: 16),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -403,7 +427,8 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
final Function? onDismiss;
|
final Function? onDismiss;
|
||||||
final Function(String)? onInsert;
|
final Function(String)? onInsert;
|
||||||
|
|
||||||
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
|
const _StickerPicker(
|
||||||
|
{this.onDismiss, required this.originalText, this.onInsert});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -413,7 +438,9 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
onDismiss?.call();
|
onDismiss?.call();
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: min(360, MediaQuery.of(context).size.width), maxHeight: 240),
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: min(360, MediaQuery.of(context).size.width),
|
||||||
|
maxHeight: 240),
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
@@ -426,8 +453,10 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
return <Widget>[
|
return <Widget>[
|
||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.only(bottom: 8),
|
margin: EdgeInsets.only(bottom: 8),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding:
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -439,7 +468,8 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
GridView.builder(
|
GridView.builder(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
padding:
|
||||||
|
const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 48,
|
maxCrossAxisExtent: 48,
|
||||||
@@ -462,7 +492,8 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ':${element.pack.prefix}${element.alias}:\n',
|
text:
|
||||||
|
':${element.pack.prefix}${element.alias}:\n',
|
||||||
style: GoogleFonts.robotoMono()),
|
style: GoogleFonts.robotoMono()),
|
||||||
TextSpan(text: element.name).bold(),
|
TextSpan(text: element.name).bold(),
|
||||||
],
|
],
|
||||||
@@ -471,11 +502,15 @@ class _StickerPicker extends StatelessWidget {
|
|||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
Radius.circular(8)),
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHigh,
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8)),
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
sn.getAttachmentUrl(element.attachment.rid),
|
sn.getAttachmentUrl(element.attachment.rid),
|
||||||
width: 48,
|
width: 48,
|
||||||
|
|||||||
@@ -133,7 +133,8 @@ extension AppPromptExtension on BuildContext {
|
|||||||
),
|
),
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () {
|
||||||
launchUrlString('https://kb.solsynth.dev/solar-network');
|
launchUrlString(
|
||||||
|
'https://kb.solsynth.dev/solar-network');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -157,7 +158,17 @@ extension ByteFormatter on int {
|
|||||||
if (this == 0) return '0 Bytes';
|
if (this == 0) return '0 Bytes';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
final dm = decimals < 0 ? 0 : decimals;
|
final dm = decimals < 0 ? 0 : decimals;
|
||||||
final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
final sizes = [
|
||||||
|
'Bytes',
|
||||||
|
'KiB',
|
||||||
|
'MiB',
|
||||||
|
'GiB',
|
||||||
|
'TiB',
|
||||||
|
'PiB',
|
||||||
|
'EiB',
|
||||||
|
'ZiB',
|
||||||
|
'YiB'
|
||||||
|
];
|
||||||
final i = (math.log(this) / math.log(k)).floor().toInt();
|
final i = (math.log(this) / math.log(k)).floor().toInt();
|
||||||
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||||
}
|
}
|
||||||
@@ -167,4 +178,15 @@ extension StringFormatter on String {
|
|||||||
String capitalize() {
|
String capitalize() {
|
||||||
return "${this[0].toUpperCase()}${substring(1)}";
|
return "${this[0].toUpperCase()}${substring(1)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String capitalizeEachWord() {
|
||||||
|
if (isEmpty) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return split(' ')
|
||||||
|
.map((word) => word.isNotEmpty
|
||||||
|
? '${word[0].toUpperCase()}${word.substring(1)}'
|
||||||
|
: '')
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_highlight/flutter_highlight.dart';
|
||||||
|
import 'package:flutter_highlight/theme_map.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
import 'package:flutter_markdown_latex/flutter_markdown_latex.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:markdown/markdown.dart' as markdown;
|
import 'package:markdown/markdown.dart' as markdown;
|
||||||
@@ -72,21 +75,27 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
code: GoogleFonts.robotoMono(height: 1),
|
code: GoogleFonts.robotoMono(height: 1),
|
||||||
),
|
),
|
||||||
builders: {},
|
builders: {
|
||||||
|
'latex': LatexElementBuilder(),
|
||||||
|
'code': HighlightBuilder(),
|
||||||
|
},
|
||||||
softLineBreak: true,
|
softLineBreak: true,
|
||||||
extensionSet: markdown.ExtensionSet(
|
extensionSet: markdown.ExtensionSet(
|
||||||
<markdown.BlockSyntax>[
|
<markdown.BlockSyntax>[
|
||||||
markdown.CodeBlockSyntax(),
|
|
||||||
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||||
|
markdown.CodeBlockSyntax(),
|
||||||
|
markdown.FencedCodeBlockSyntax(),
|
||||||
|
LatexBlockSyntax(),
|
||||||
],
|
],
|
||||||
<markdown.InlineSyntax>[
|
<markdown.InlineSyntax>[
|
||||||
|
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes,
|
||||||
if (isAutoWarp) markdown.LineBreakSyntax(),
|
if (isAutoWarp) markdown.LineBreakSyntax(),
|
||||||
_UserNameCardInlineSyntax(),
|
_UserNameCardInlineSyntax(),
|
||||||
_CustomEmoteInlineSyntax(context),
|
_CustomEmoteInlineSyntax(context),
|
||||||
markdown.AutolinkSyntax(),
|
markdown.AutolinkSyntax(),
|
||||||
markdown.AutolinkExtensionSyntax(),
|
markdown.AutolinkExtensionSyntax(),
|
||||||
markdown.CodeSyntax(),
|
markdown.CodeSyntax(),
|
||||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
LatexInlineSyntax(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTapLink: (text, href, title) async {
|
onTapLink: (text, href, title) async {
|
||||||
@@ -260,3 +269,56 @@ class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HighlightBuilder extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
Widget? visitElementAfterWithContext(
|
||||||
|
BuildContext context,
|
||||||
|
markdown.Element element,
|
||||||
|
TextStyle? preferredStyle,
|
||||||
|
TextStyle? parentStyle,
|
||||||
|
) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
if (element.attributes['class'] == null &&
|
||||||
|
!element.textContent.trim().contains('\n')) {
|
||||||
|
return Container(
|
||||||
|
padding:
|
||||||
|
EdgeInsets.only(top: 0.0, right: 4.0, bottom: 1.75, left: 4.0),
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 2.0),
|
||||||
|
color: Colors.black12,
|
||||||
|
child: Text(
|
||||||
|
element.textContent,
|
||||||
|
style: GoogleFonts.robotoMono(textStyle: preferredStyle),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
var language = 'plaintext';
|
||||||
|
final pattern = RegExp(r'^language-(.+)$');
|
||||||
|
if (element.attributes['class'] != null &&
|
||||||
|
pattern.hasMatch(element.attributes['class'] ?? '')) {
|
||||||
|
language =
|
||||||
|
pattern.firstMatch(element.attributes['class'] ?? '')?.group(1) ??
|
||||||
|
'plaintext';
|
||||||
|
}
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: HighlightView(
|
||||||
|
element.textContent.trim(),
|
||||||
|
language: language,
|
||||||
|
theme: {
|
||||||
|
...(isDark ? themeMap['a11y-dark']! : themeMap['a11y-light']!),
|
||||||
|
'root': (isDark
|
||||||
|
? TextStyle(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
color: Color(0xfff8f8f2))
|
||||||
|
: TextStyle(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
color: Color(0xff545454)))
|
||||||
|
},
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
textStyle: GoogleFonts.robotoMono(textStyle: preferredStyle),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -188,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
|
: TextAlign.start,
|
||||||
: null
|
|
||||||
: 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(
|
CloseWindowButton(
|
||||||
children: [
|
colors: windowButtonColor,
|
||||||
MinimizeWindowButton(
|
onPressed: () => appWindow.hide(),
|
||||||
colors: windowButtonColor),
|
|
||||||
MaximizeWindowButton(
|
|
||||||
colors: windowButtonColor),
|
|
||||||
CloseWindowButton(
|
|
||||||
colors: windowButtonColor),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -226,16 +216,18 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
child: NotifyIndicator()),
|
child: NotifyIndicator()),
|
||||||
if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
|
if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
|
||||||
Positioned(
|
Positioned(
|
||||||
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,
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
|
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
|
|
||||||
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
final rawBytes =
|
||||||
|
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||||
|
.buffer
|
||||||
|
.asUint8List();
|
||||||
|
|
||||||
if (onUpdate != null) {
|
if (onUpdate != null) {
|
||||||
final updatedMedia = PostWriteMedia.fromBytes(
|
final updatedMedia = PostWriteMedia.fromBytes(
|
||||||
@@ -133,7 +136,8 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
|
|
||||||
final result = await showDialog<SnAttachmentBoost?>(
|
final result = await showDialog<SnAttachmentBoost?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PendingAttachmentBoostDialog(media: attachments[idx]),
|
builder: (context) =>
|
||||||
|
PendingAttachmentBoostDialog(media: attachments[idx]),
|
||||||
);
|
);
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
|
|
||||||
@@ -165,11 +169,15 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
onUpdate!(idx, PostWriteMedia(result));
|
onUpdate!(idx, PostWriteMedia(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
ContextMenu _createContextMenu(
|
||||||
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
BuildContext context, int idx, PostWriteMedia media) {
|
||||||
|
final canCompressVideo =
|
||||||
|
!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||||
return ContextMenu(
|
return ContextMenu(
|
||||||
entries: [
|
entries: [
|
||||||
if (media.attachment == null && media.type == SnMediaType.video && canCompressVideo)
|
if (media.attachment == null &&
|
||||||
|
media.type == SnMediaType.video &&
|
||||||
|
canCompressVideo)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'attachmentCompressVideo'.tr(),
|
label: 'attachmentCompressVideo'.tr(),
|
||||||
icon: Symbols.compress,
|
icon: Symbols.compress,
|
||||||
@@ -312,12 +320,15 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: switch (media.type) {
|
child: switch (media.type) {
|
||||||
SnMediaType.image => LayoutBuilder(builder: (context, constraints) {
|
SnMediaType.image =>
|
||||||
|
LayoutBuilder(builder: (context, constraints) {
|
||||||
return Image(
|
return Image(
|
||||||
image: media.getImageProvider(
|
image: media.getImageProvider(
|
||||||
context,
|
context,
|
||||||
width: (constraints.maxWidth * devicePixelRatio).round(),
|
width:
|
||||||
height: (constraints.maxHeight * devicePixelRatio).round(),
|
(constraints.maxWidth * devicePixelRatio).round(),
|
||||||
|
height:
|
||||||
|
(constraints.maxHeight * devicePixelRatio).round(),
|
||||||
)!,
|
)!,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
);
|
);
|
||||||
@@ -326,28 +337,34 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
if (media.attachment?.thumbnail != null)
|
if (media.attachment?.thumbnail != null)
|
||||||
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
AutoResizeUniversalImage(sn.getAttachmentUrl(
|
||||||
const Icon(Symbols.videocam, color: Colors.white, shadows: [
|
media.attachment!.thumbnail!.rid)),
|
||||||
Shadow(
|
const Icon(Symbols.videocam,
|
||||||
offset: Offset(1, 1),
|
color: Colors.white,
|
||||||
blurRadius: 8.0,
|
shadows: [
|
||||||
color: Color.fromARGB(255, 0, 0, 0),
|
Shadow(
|
||||||
),
|
offset: Offset(1, 1),
|
||||||
]),
|
blurRadius: 8.0,
|
||||||
|
color: Color.fromARGB(255, 0, 0, 0),
|
||||||
|
),
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SnMediaType.audio => Stack(
|
SnMediaType.audio => Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
if (media.attachment?.thumbnail != null)
|
if (media.attachment?.thumbnail != null)
|
||||||
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
AutoResizeUniversalImage(sn.getAttachmentUrl(
|
||||||
const Icon(Symbols.audio_file, color: Colors.white, shadows: [
|
media.attachment!.thumbnail!.rid)),
|
||||||
Shadow(
|
const Icon(Symbols.audio_file,
|
||||||
offset: Offset(1, 1),
|
color: Colors.white,
|
||||||
blurRadius: 8.0,
|
shadows: [
|
||||||
color: Color.fromARGB(255, 0, 0, 0),
|
Shadow(
|
||||||
),
|
offset: Offset(1, 1),
|
||||||
]),
|
blurRadius: 8.0,
|
||||||
|
color: Color.fromARGB(255, 0, 0, 0),
|
||||||
|
),
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
_ => Container(
|
_ => Container(
|
||||||
@@ -356,7 +373,8 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (media.type != SnMediaType.image) const VerticalDivider(width: 1, thickness: 1),
|
if (media.type != SnMediaType.image)
|
||||||
|
const VerticalDivider(width: 1, thickness: 1),
|
||||||
if (media.type != SnMediaType.image)
|
if (media.type != SnMediaType.image)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 160,
|
width: 160,
|
||||||
@@ -374,7 +392,8 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
else if (media.file != null)
|
else if (media.file != null)
|
||||||
Text(media.file!.name, maxLines: 1, overflow: TextOverflow.ellipsis)
|
Text(media.file!.name,
|
||||||
|
maxLines: 1, overflow: TextOverflow.ellipsis)
|
||||||
else
|
else
|
||||||
Text('unknown'.tr()),
|
Text('unknown'.tr()),
|
||||||
if (media.attachment != null)
|
if (media.attachment != null)
|
||||||
@@ -387,7 +406,8 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
FutureBuilder<int?>(
|
FutureBuilder<int?>(
|
||||||
future: media.length(),
|
future: media.length(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
if (!snapshot.hasData)
|
||||||
|
return const SizedBox.shrink();
|
||||||
return Text(
|
return Text(
|
||||||
snapshot.data!.formatBytes(),
|
snapshot.data!.formatBytes(),
|
||||||
style: GoogleFonts.robotoMono(fontSize: 13),
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
@@ -398,7 +418,8 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (media.attachment != null && media.attachment!.boosts.isNotEmpty)
|
if (media.attachment != null &&
|
||||||
|
media.attachment!.boosts.isNotEmpty)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Symbols.bolt, size: 16),
|
Icon(Symbols.bolt, size: 16),
|
||||||
@@ -406,7 +427,8 @@ class _PostMediaPendingItem extends StatelessWidget {
|
|||||||
Text('attachmentGotBoosted').tr().fontSize(13),
|
Text('attachmentGotBoosted').tr().fontSize(13),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (media.attachment != null && media.attachment!.compressedId != null)
|
if (media.attachment != null &&
|
||||||
|
media.attachment!.compressedId != null)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Symbols.compress, size: 16),
|
Icon(Symbols.compress, size: 16),
|
||||||
@@ -527,12 +549,20 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopupMenuButton(
|
return PopupMenuButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
style: ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||||
|
),
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Symbols.add_photo_alternate,
|
Symbols.add_photo_alternate,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
if (!kIsWeb &&
|
||||||
|
!Platform.isLinux &&
|
||||||
|
!Platform.isMacOS &&
|
||||||
|
!Platform.isWindows)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -545,7 +575,10 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
_takeMedia(false);
|
_takeMedia(false);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
if (!kIsWeb &&
|
||||||
|
!Platform.isLinux &&
|
||||||
|
!Platform.isMacOS &&
|
||||||
|
!Platform.isWindows)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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):
|
||||||
@@ -191,7 +193,7 @@ PODS:
|
|||||||
- sqlite3_flutter_libs (0.0.1):
|
- sqlite3_flutter_libs (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqlite3 (~> 3.49.0)
|
- sqlite3 (~> 3.49.1)
|
||||||
- sqlite3/dbstatvtab
|
- sqlite3/dbstatvtab
|
||||||
- sqlite3/fts5
|
- sqlite3/fts5
|
||||||
- sqlite3/perf-threadsafe
|
- sqlite3/perf-threadsafe
|
||||||
@@ -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
|
||||||
@@ -372,7 +378,7 @@ SPEC CHECKSUMS:
|
|||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||||
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
|
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||||
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
|
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
|
||||||
|
|||||||
119
pubspec.lock
119
pubspec.lock
@@ -405,10 +405,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dio_web_adapter
|
name: dio_web_adapter
|
||||||
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
|
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.1"
|
||||||
dismissible_page:
|
dismissible_page:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -525,10 +525,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "6f6bfa8797f296965bdc3e1f702574ab49a540c19b9237b401e7c2b25dfe594c"
|
sha256: "9467b7c4eedf0bd4c9306b0ec12455b278f6366962be061d0978a446c103c111"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.0"
|
version: "9.0.1"
|
||||||
file_saver:
|
file_saver:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -679,7 +679,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.2"
|
version: "3.2.2"
|
||||||
flutter_cache_manager:
|
flutter_cache_manager:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_cache_manager
|
name: flutter_cache_manager
|
||||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||||
@@ -710,6 +710,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
flutter_highlight:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_highlight
|
||||||
|
sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.0"
|
||||||
flutter_inappwebview:
|
flutter_inappwebview:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -803,6 +811,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6+2"
|
version: "0.7.6+2"
|
||||||
|
flutter_markdown_latex:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_markdown_latex
|
||||||
|
sha256: "839e76a84abb3632ffcebbd450cf93c7e9894af65622527d23f0084cee1bfd04"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.4"
|
||||||
|
flutter_math_fork:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_math_fork
|
||||||
|
sha256: "284bab89b2fbf1bc3a0baf13d011c1dd324d004e35d177626b77f2fc056366ac"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.3"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -873,18 +897,18 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: freezed
|
name: freezed
|
||||||
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
|
sha256: "532008570b7fd20310db8cb9c8ebc5bafd5aa4e52c4358db4e5ddc29f74f4be3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.8"
|
version: "3.0.1"
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: freezed_annotation
|
name: freezed_annotation
|
||||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.4"
|
version: "3.0.0"
|
||||||
frontend_server_client:
|
frontend_server_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -941,6 +965,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
|
group_button:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: group_button
|
||||||
|
sha256: "0610fcf28ed122bfb4b410fce161a390f7f2531d55d1d65c5375982001415940"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.3.4"
|
||||||
|
highlight:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: highlight
|
||||||
|
sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.0"
|
||||||
home_widget:
|
home_widget:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1393,18 +1433,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
|
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.2.1"
|
version: "8.3.0"
|
||||||
package_info_plus_platform_interface:
|
package_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_info_plus_platform_interface
|
name: package_info_plus_platform_interface
|
||||||
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76"
|
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.2.0"
|
||||||
pasteboard:
|
pasteboard:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1950,10 +1990,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlite3_flutter_libs
|
name: sqlite3_flutter_libs
|
||||||
sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233"
|
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.30"
|
version: "0.5.31"
|
||||||
sqlparser:
|
sqlparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2018,6 +2058,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.1"
|
version: "3.3.1"
|
||||||
|
talker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: talker
|
||||||
|
sha256: "5ab7d974ad92042b3e2382441c41ec4c6e5b3fa2b4b024d8ccbfc4bc2244b7bb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.6.14"
|
||||||
|
talker_dio_logger:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: talker_dio_logger
|
||||||
|
sha256: "71780c52951d36e94964ca06158d827dfc67aa2fb75c8b880603cfefa4377b39"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.6.14"
|
||||||
|
talker_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: talker_flutter
|
||||||
|
sha256: "0cc816260b226c0ff930909c9f22984316b652b140f5eabb97ae9813ee0de135"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.6.14"
|
||||||
|
talker_logger:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: talker_logger
|
||||||
|
sha256: "16ff0cfdf011f65b37957c9ff7ef7043dd9f1c8af3ccb4a44ac4a448defb9eb5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.6.14"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2050,6 +2122,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.2"
|
version: "0.3.2"
|
||||||
|
tuple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: tuple
|
||||||
|
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2309,10 +2389,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
|
||||||
|
|||||||
18
pubspec.yaml
18
pubspec.yaml
@@ -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+71
|
version: 2.3.2+75
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@@ -47,7 +47,7 @@ dependencies:
|
|||||||
dio: ^5.7.0
|
dio: ^5.7.0
|
||||||
dio_smart_retry: ^7.0.1
|
dio_smart_retry: ^7.0.1
|
||||||
very_good_infinite_list: ^0.9.0
|
very_good_infinite_list: ^0.9.0
|
||||||
freezed_annotation: ^2.4.4
|
freezed_annotation: ^3.0.0
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
gap: ^3.0.1
|
gap: ^3.0.1
|
||||||
markdown: ^7.2.2
|
markdown: ^7.2.2
|
||||||
@@ -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
|
||||||
@@ -124,6 +128,12 @@ dependencies:
|
|||||||
drift: ^2.25.1
|
drift: ^2.25.1
|
||||||
drift_flutter: ^0.2.4
|
drift_flutter: ^0.2.4
|
||||||
local_notifier: ^0.1.6
|
local_notifier: ^0.1.6
|
||||||
|
flutter_markdown_latex: ^0.3.4
|
||||||
|
flutter_highlight: ^0.7.0
|
||||||
|
talker_flutter: ^4.6.14
|
||||||
|
talker_dio_logger: ^4.6.14
|
||||||
|
talker: ^4.6.14
|
||||||
|
flutter_cache_manager: ^3.4.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -136,7 +146,7 @@ dev_dependencies:
|
|||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
build_runner: ^2.4.15
|
build_runner: ^2.4.15
|
||||||
freezed: ^2.5.7
|
freezed: ^3.0.1
|
||||||
json_serializable: ^6.8.0
|
json_serializable: ^6.8.0
|
||||||
icons_launcher: ^3.0.0
|
icons_launcher: ^3.0.0
|
||||||
flutter_native_splash: ^2.4.2
|
flutter_native_splash: ^2.4.2
|
||||||
|
|||||||
Reference in New Issue
Block a user