Compare commits
29 Commits
2.3.2+72
...
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 |
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"
|
||||
},
|
||||
"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",
|
||||
"settingsDisplayLanguageDescription": "Set the application language.",
|
||||
"settingsDisplayLanguageSystem": "Follow System",
|
||||
@@ -719,7 +724,16 @@
|
||||
"stickersNewDescription": "Create a new sticker belongs to this pack.",
|
||||
"stickersPackNew": "New Sticker Pack",
|
||||
"trayMenuShow": "Show",
|
||||
"trayMenuMuteNotification": "Do Not Disturb",
|
||||
"update": "Update",
|
||||
"forceUpdate": "Force Update",
|
||||
"forceUpdateDescription": "Force to show the application update popup, even the new version is not available."
|
||||
"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": "{} 条评论"
|
||||
},
|
||||
"settingsAppearance": "外观",
|
||||
"settingsCustomFonts": "自定义字体",
|
||||
"settingsCustomFontsDescription": "设置应用程序使用的字体。",
|
||||
"settingsCustomFontFamily": "应用字体",
|
||||
"settingsCustomFontFamilyHint": "使用英文逗号分割每一种字体,越前优先级越高",
|
||||
"settingsCustomFontApplied": "自定义字体已经应用。",
|
||||
"settingsDisplayLanguage": "显示语言",
|
||||
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
|
||||
"settingsDisplayLanguageSystem": "跟随系统",
|
||||
@@ -717,7 +722,16 @@
|
||||
"stickersNewDescription": "创建一个新的贴图。",
|
||||
"stickersPackNew": "新建贴图包",
|
||||
"trayMenuShow": "显示",
|
||||
"trayMenuMuteNotification": "静音通知",
|
||||
"update": "更新",
|
||||
"forceUpdate": "强制更新",
|
||||
"forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。"
|
||||
"forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。",
|
||||
"runtimeLogs": "运行时日志",
|
||||
"runtimeLogsOpen": "打开日志文件",
|
||||
"runtimeLogsDescription": "显示运行时的日志记录。",
|
||||
"signinResetPasswordHint": "请输入用户名/电子邮箱地址以帮助我们找到您的帐户并重置密码。",
|
||||
"cacheSize": "缓存资源大小",
|
||||
"cacheDelete": "清除缓存",
|
||||
"cacheDeleteDescription": "从磁盘中移除缓存的图片和其他资源,内容将从服务器重新下载。",
|
||||
"cacheDeleted": "所有缓存已被清除。"
|
||||
}
|
||||
|
||||
@@ -201,6 +201,11 @@
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsCustomFonts": "自定義字體",
|
||||
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
|
||||
"settingsCustomFontFamily": "應用字體",
|
||||
"settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
|
||||
"settingsCustomFontApplied": "自定義字體已經應用。",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
@@ -717,7 +722,16 @@
|
||||
"stickersNewDescription": "創建一個新的貼圖。",
|
||||
"stickersPackNew": "新建貼圖包",
|
||||
"trayMenuShow": "顯示",
|
||||
"trayMenuMuteNotification": "靜音通知",
|
||||
"update": "更新",
|
||||
"forceUpdate": "強制更新",
|
||||
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。"
|
||||
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
|
||||
"runtimeLogs": "運行時日誌",
|
||||
"runtimeLogsOpen": "打開日誌文件",
|
||||
"runtimeLogsDescription": "顯示運行時的日誌記錄。",
|
||||
"signinResetPasswordHint": "請輸入用户名/電子郵箱地址以幫助我們找到您的帳户並重置密碼。",
|
||||
"cacheSize": "緩存資源大小",
|
||||
"cacheDelete": "清除緩存",
|
||||
"cacheDeleteDescription": "從磁盤中移除緩存的圖片和其他資源,內容將從服務器重新下載。",
|
||||
"cacheDeleted": "所有緩存已被清除。"
|
||||
}
|
||||
|
||||
@@ -201,6 +201,11 @@
|
||||
"other": "{} 條評論"
|
||||
},
|
||||
"settingsAppearance": "外觀",
|
||||
"settingsCustomFonts": "自定義字體",
|
||||
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
|
||||
"settingsCustomFontFamily": "應用字體",
|
||||
"settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
|
||||
"settingsCustomFontApplied": "自定義字體已經應用。",
|
||||
"settingsDisplayLanguage": "顯示語言",
|
||||
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
|
||||
"settingsDisplayLanguageSystem": "跟隨系統",
|
||||
@@ -717,7 +722,16 @@
|
||||
"stickersNewDescription": "創建一個新的貼圖。",
|
||||
"stickersPackNew": "新建貼圖包",
|
||||
"trayMenuShow": "顯示",
|
||||
"trayMenuMuteNotification": "靜音通知",
|
||||
"update": "更新",
|
||||
"forceUpdate": "強制更新",
|
||||
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。"
|
||||
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
|
||||
"runtimeLogs": "運行時日誌",
|
||||
"runtimeLogsOpen": "打開日誌文件",
|
||||
"runtimeLogsDescription": "顯示運行時的日誌記錄。",
|
||||
"signinResetPasswordHint": "請輸入用戶名/電子郵箱地址以幫助我們找到您的帳戶並重置密碼。",
|
||||
"cacheSize": "緩存資源大小",
|
||||
"cacheDelete": "清除緩存",
|
||||
"cacheDeleteDescription": "從磁盤中移除緩存的圖片和其他資源,內容將從服務器重新下載。",
|
||||
"cacheDeleted": "所有緩存已被清除。"
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ PODS:
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (~> 3.49.0)
|
||||
- sqlite3 (~> 3.49.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
@@ -445,7 +445,7 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
|
||||
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
@@ -8,6 +7,7 @@ import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/database/database.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/database.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
@@ -194,9 +194,11 @@ class ChatMessageController extends ChangeNotifier {
|
||||
channelId: channel!.id,
|
||||
createdAt: Value(message.createdAt),
|
||||
),
|
||||
onConflict: DoUpdate((_) => SnLocalChatMessageCompanion.custom(
|
||||
onConflict: DoUpdate(
|
||||
(_) => SnLocalChatMessageCompanion.custom(
|
||||
content: Constant(jsonEncode(message.toJson())),
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
incomeStrandedQueue.add(message);
|
||||
@@ -212,12 +214,13 @@ class ChatMessageController extends ChangeNotifier {
|
||||
final idx =
|
||||
messages.indexWhere((x) => x.id == message.relatedEventId);
|
||||
if (idx != -1) {
|
||||
final newBody = message.body;
|
||||
final newBody = Map<String, dynamic>.from(message.body);
|
||||
newBody.remove('related_event');
|
||||
messages[idx] = messages[idx].copyWith(
|
||||
body: newBody,
|
||||
updatedAt: message.updatedAt,
|
||||
);
|
||||
}
|
||||
if (message.relatedEventId != null) {
|
||||
await (_dt.db.snLocalChatMessage.update()
|
||||
..where((e) => e.id.equals(message.relatedEventId!)))
|
||||
@@ -228,7 +231,6 @@ class ChatMessageController extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'messages.delete':
|
||||
if (message.relatedEventId != null) {
|
||||
messages.removeWhere((x) => x.id == message.relatedEventId);
|
||||
@@ -322,6 +324,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
final mostRecentMessage = await (_dt.db.snLocalChatMessage.select()
|
||||
..where((e) => e.channelId.equals(channel!.id))
|
||||
..limit(1)
|
||||
..orderBy([
|
||||
(e) =>
|
||||
@@ -529,7 +532,7 @@ class ChatMessageController extends ChangeNotifier {
|
||||
},
|
||||
).toJson(),
|
||||
));
|
||||
log('[Messaging] Send read event request: $_readEventAnchor');
|
||||
logging.debug('[Messaging] Send read event request: $_readEventAnchor');
|
||||
}
|
||||
|
||||
@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:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/firebase_options.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/chat_call.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
@@ -235,7 +236,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
await inAppReview.requestReview();
|
||||
prefs.setBool('rating_requested', true);
|
||||
} else {
|
||||
log('Unable request app review, unavailable');
|
||||
logging.error('Unable request app review, unavailable');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -263,17 +264,18 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
int.tryParse(remoteVersionString.split('+').last) ?? 0;
|
||||
final localBuildNumber =
|
||||
int.tryParse(localVersionString.split('+').last) ?? 0;
|
||||
log("[Update] Local: $localVersionString, Remote: $remoteVersionString");
|
||||
logging.info(
|
||||
"[Update] Local: $localVersionString, Remote: $remoteVersionString");
|
||||
if ((remoteVersion > localVersion ||
|
||||
remoteBuildNumber > localBuildNumber) &&
|
||||
mounted) {
|
||||
final config = context.read<ConfigProvider>();
|
||||
config.setUpdate(
|
||||
remoteVersionString, resp.data?['body'] ?? 'No changelog');
|
||||
log("[Update] Update available: $remoteVersionString");
|
||||
logging.info("[Update] Update available: $remoteVersionString");
|
||||
}
|
||||
} catch (e) {
|
||||
log('[Error] Unable to check update: $e');
|
||||
logging.error('[Error] Unable to check update...', e);
|
||||
if (mounted) context.showErrorDialog('Unable to check update: $e');
|
||||
}
|
||||
}
|
||||
@@ -306,7 +308,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
if (!mounted) return;
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
await sticker.listSticker();
|
||||
log('[Bootstrap] Everything initialized!');
|
||||
logging.info('[Bootstrap] Everything initialized!');
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
await context.showErrorDialog(err);
|
||||
@@ -333,25 +335,20 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _trayInitialization() async {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
|
||||
final icon = Platform.isWindows
|
||||
? 'assets/icon/tray-icon.ico'
|
||||
: 'assets/icon/tray-icon.png';
|
||||
final appVersion = await PackageInfo.fromPlatform();
|
||||
|
||||
trayManager.addListener(this);
|
||||
await trayManager.setIcon(icon);
|
||||
|
||||
Menu menu = Menu(
|
||||
final Menu _appTrayMenu = Menu(
|
||||
items: [
|
||||
MenuItem(
|
||||
key: 'version_label',
|
||||
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
|
||||
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(),
|
||||
@@ -362,14 +359,32 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
),
|
||||
],
|
||||
);
|
||||
await trayManager.setContextMenu(menu);
|
||||
|
||||
Future<void> _trayInitialization() async {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
|
||||
final icon = Platform.isWindows
|
||||
? 'assets/icon/tray-icon.ico'
|
||||
: 'assets/icon/tray-icon.png';
|
||||
final appVersion = await PackageInfo.fromPlatform();
|
||||
|
||||
trayManager.addListener(this);
|
||||
await trayManager.setIcon(icon);
|
||||
|
||||
_appTrayMenu.items![0] = MenuItem(
|
||||
key: 'version_label',
|
||||
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
|
||||
disabled: true,
|
||||
);
|
||||
|
||||
await trayManager.setContextMenu(_appTrayMenu);
|
||||
}
|
||||
|
||||
Future<void> _notifyInitialization() async {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
|
||||
await localNotifier.setup(
|
||||
appName: 'solian',
|
||||
appName: 'Solian',
|
||||
shortcutPolicy: ShortcutPolicy.requireCreate,
|
||||
);
|
||||
}
|
||||
@@ -424,12 +439,23 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
@override
|
||||
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||
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':
|
||||
appWindow.show();
|
||||
// To prevent the window from being hide after just show on macOS
|
||||
Timer(const Duration(milliseconds: 100), () => appWindow.show());
|
||||
break;
|
||||
case 'exit':
|
||||
_appLifecycleListener?.dispose();
|
||||
if (Platform.isWindows) {
|
||||
appWindow.close();
|
||||
} else {
|
||||
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
||||
const kAppExpandPostLink = 'app_expand_post_link';
|
||||
const kAppExpandChatLink = 'app_expand_chat_link';
|
||||
const kAppRealmCompactView = 'app_realm_compact_view';
|
||||
const kAppCustomFonts = 'app_custom_fonts';
|
||||
|
||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||
'settingsImageQualityLowest': FilterQuality.none,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/link.dart';
|
||||
|
||||
@@ -20,7 +20,7 @@ class SnLinkPreviewProvider {
|
||||
final target = b64.encode(url);
|
||||
if (_cache.containsKey(target)) return _cache[target];
|
||||
|
||||
log('[LinkPreview] Fetching $url ($target)');
|
||||
logging.debug('[LinkPreview] Fetching $url ($target)');
|
||||
|
||||
try {
|
||||
final resp = await _sn.client.get('/cgi/re/link/$target');
|
||||
@@ -28,7 +28,7 @@ class SnLinkPreviewProvider {
|
||||
_cache[url] = meta;
|
||||
return meta;
|
||||
} catch (err) {
|
||||
log('[LinkPreview] Failed to fetch $url ($target)...');
|
||||
logging.warning('[LinkPreview] Failed to fetch $url ($target)...', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
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:local_notifier/local_notifier.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
@@ -48,11 +48,13 @@ class NotificationProvider extends ChangeNotifier {
|
||||
var deviceUuid = await FlutterUdid.consistentUdid;
|
||||
|
||||
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;
|
||||
} else {
|
||||
log('Device UUID is $deviceUuid');
|
||||
log('Registering device push notifications...');
|
||||
logging.info('[Push Notification] Device UUID is $deviceUuid');
|
||||
logging
|
||||
.info('[Push Notification] Registering device push notifications...');
|
||||
}
|
||||
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
@@ -62,7 +64,7 @@ class NotificationProvider extends ChangeNotifier {
|
||||
provider = 'fcm';
|
||||
token = await FirebaseMessaging.instance.getToken();
|
||||
}
|
||||
log('Device Push Token is $token');
|
||||
logging.info('[Push Notification] Device Push Token is $token');
|
||||
|
||||
await _sn.client.post(
|
||||
'/cgi/id/notifications/subscription',
|
||||
@@ -78,10 +80,25 @@ class NotificationProvider extends ChangeNotifier {
|
||||
int showingTrayCount = 0;
|
||||
List<SnNotification> notifications = List.empty(growable: true);
|
||||
|
||||
int? skippableNotifyChannel;
|
||||
bool isMuted = false;
|
||||
|
||||
void listen() {
|
||||
_ws.pk.stream.listen((event) {
|
||||
if (event.method == 'notifications.new') {
|
||||
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;
|
||||
showingCount++;
|
||||
showingTrayCount++;
|
||||
@@ -92,10 +109,8 @@ class NotificationProvider extends ChangeNotifier {
|
||||
});
|
||||
notifyListeners();
|
||||
updateTray();
|
||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||
if (doHaptic) HapticFeedback.mediumImpact();
|
||||
|
||||
if (!kIsWeb) {
|
||||
if (!kIsWeb && !isMuted) {
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
LocalNotification notify = LocalNotification(
|
||||
title: notification.title,
|
||||
|
||||
@@ -41,7 +41,8 @@ class SnAttachmentProvider {
|
||||
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 Map<String, int> randomMapping = {};
|
||||
for (int i = 0; i < rids.length; i++) {
|
||||
@@ -62,8 +63,10 @@ class SnAttachmentProvider {
|
||||
'id': pendingFetch.join(','),
|
||||
},
|
||||
);
|
||||
final List<SnAttachment?> out =
|
||||
resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).cast<SnAttachment?>().toList();
|
||||
final List<SnAttachment?> out = resp.data['data']
|
||||
.map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e))
|
||||
.cast<SnAttachment?>()
|
||||
.toList();
|
||||
|
||||
for (final item in out) {
|
||||
if (item == null) continue;
|
||||
@@ -77,7 +80,13 @@ class SnAttachmentProvider {
|
||||
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(
|
||||
Uint8List data,
|
||||
@@ -89,8 +98,11 @@ class SnAttachmentProvider {
|
||||
bool analyzeNow = false,
|
||||
}) async {
|
||||
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
||||
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
final fileAlt = filename.contains('.')
|
||||
? filename.substring(0, filename.lastIndexOf('.'))
|
||||
: filename;
|
||||
final fileExt =
|
||||
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
|
||||
String? mimetypeOverride;
|
||||
if (mimetype != null) {
|
||||
@@ -127,8 +139,11 @@ class SnAttachmentProvider {
|
||||
Map<String, dynamic>? metadata, {
|
||||
String? mimetype,
|
||||
}) async {
|
||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
||||
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
final fileAlt = filename.contains('.')
|
||||
? filename.substring(0, filename.lastIndexOf('.'))
|
||||
: filename;
|
||||
final fileExt =
|
||||
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||
|
||||
String? mimetypeOverride;
|
||||
if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) {
|
||||
@@ -146,7 +161,10 @@ class SnAttachmentProvider {
|
||||
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(
|
||||
@@ -197,7 +215,10 @@ class SnAttachmentProvider {
|
||||
(entry.value + 1) * chunkSize,
|
||||
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(
|
||||
data,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
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:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/widget.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 = [
|
||||
('Solar Network', 'https://api.sn.solsynth.dev'),
|
||||
@@ -36,6 +38,19 @@ class SnNetworkProvider {
|
||||
|
||||
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(
|
||||
dio: client,
|
||||
retries: 3,
|
||||
@@ -69,7 +84,6 @@ class SnNetworkProvider {
|
||||
_prefs = _config.prefs;
|
||||
client.options.baseUrl = _config.serverUrl;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
static Future<Dio> createOffContextClient() async {
|
||||
@@ -91,7 +105,8 @@ class SnNetworkProvider {
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) 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(kRtkStoreKey, rtk);
|
||||
});
|
||||
@@ -103,7 +118,8 @@ class SnNetworkProvider {
|
||||
},
|
||||
),
|
||||
);
|
||||
client.options.baseUrl = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
||||
client.options.baseUrl =
|
||||
prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -119,7 +135,8 @@ class SnNetworkProvider {
|
||||
platformInfo = 'Web; ${deviceInfo.vendor}';
|
||||
} else if (Platform.isAndroid) {
|
||||
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) {
|
||||
final deviceInfo = await DeviceInfoPlugin().iosInfo;
|
||||
platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}';
|
||||
@@ -128,7 +145,8 @@ class SnNetworkProvider {
|
||||
platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}';
|
||||
} else if (Platform.isWindows) {
|
||||
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
|
||||
platformInfo = 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
|
||||
platformInfo =
|
||||
'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
|
||||
} else if (Platform.isLinux) {
|
||||
final deviceInfo = await DeviceInfoPlugin().linuxInfo;
|
||||
platformInfo = 'Linux; ${deviceInfo.prettyName}';
|
||||
@@ -148,12 +166,15 @@ class SnNetworkProvider {
|
||||
final tkLock = Lock();
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
return await _refreshCompleter!.future;
|
||||
} else {
|
||||
@@ -185,7 +206,8 @@ class SnNetworkProvider {
|
||||
final payload = b64.decode(rawPayload);
|
||||
final exp = jsonDecode(payload)['exp'];
|
||||
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);
|
||||
if (result == null) {
|
||||
atk = null;
|
||||
@@ -199,12 +221,12 @@ class SnNetworkProvider {
|
||||
_refreshCompleter!.complete(atk);
|
||||
return atk;
|
||||
} else {
|
||||
log('Access token refresh failed...');
|
||||
logging.error('[Auth] Access token refresh failed...');
|
||||
_refreshCompleter!.complete(null);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log('Failed to authenticate user: $err');
|
||||
logging.error('[Auth] Failed to authenticate user...', err);
|
||||
_refreshCompleter!.completeError(err);
|
||||
} finally {
|
||||
_refreshCompleter = null;
|
||||
@@ -237,7 +259,8 @@ class SnNetworkProvider {
|
||||
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;
|
||||
|
||||
final dio = Dio();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
|
||||
@@ -51,7 +50,7 @@ class SnStickerProvider {
|
||||
return sticker;
|
||||
} catch (err) {
|
||||
_cache[alias] = null;
|
||||
log('[Sticker] Failed to lookup sticker $alias: $err');
|
||||
logging.warning('[Sticker] Failed to lookup sticker $alias', err);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -66,7 +65,7 @@ class SnStickerProvider {
|
||||
_cacheSticker(sticker);
|
||||
}
|
||||
} catch (err) {
|
||||
log('[Sticker] Failed to list stickers: $err');
|
||||
logging.error('[Sticker] Failed to list stickers...', err);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,16 @@ class ThemeProvider extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
void reloadTheme({Color? seedColorOverride, bool? useMaterial3}) {
|
||||
createAppThemeSet(seedColorOverride: seedColorOverride, useMaterial3: useMaterial3).then((value) {
|
||||
void reloadTheme({
|
||||
Color? seedColorOverride,
|
||||
bool? useMaterial3,
|
||||
String? customFonts,
|
||||
}) {
|
||||
createAppThemeSet(
|
||||
seedColorOverride: seedColorOverride,
|
||||
useMaterial3: useMaterial3,
|
||||
customFonts: customFonts,
|
||||
).then((value) {
|
||||
theme = value;
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
@@ -27,8 +27,11 @@ class UserDirectoryProvider {
|
||||
plannedQuery.add(item);
|
||||
}
|
||||
}
|
||||
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();
|
||||
if (plannedQuery.isEmpty) return out;
|
||||
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;
|
||||
for (var idx = 0; idx < out.length; idx++) {
|
||||
if (out[idx] != null) continue;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
@@ -30,8 +29,8 @@ class UserProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
refreshUser().then((value) async {
|
||||
if (value != null) {
|
||||
log('Logged in as @${value.name}');
|
||||
log('Atk: ${await atk}');
|
||||
logging.info('[Auth] Logged in as @${value.name}');
|
||||
logging.debug('[Auth] Access token: ${await atk}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/websocket.dart';
|
||||
@@ -30,7 +30,7 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
if (isConnected) return;
|
||||
if (!_ua.isAuthorized) return;
|
||||
|
||||
log('[WebSocket] Connecting to the server...');
|
||||
logging.debug('[WebSocket] Connecting to the server...');
|
||||
await connect();
|
||||
}
|
||||
|
||||
@@ -62,17 +62,14 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
await conn!.ready;
|
||||
_wsStream = conn!.stream.asBroadcastStream();
|
||||
listen();
|
||||
log('[WebSocket] Connected to server!');
|
||||
logging.info('[WebSocket] Connected to server!');
|
||||
isConnected = true;
|
||||
} catch (err) {
|
||||
if (err is WebSocketChannelException) {
|
||||
log('Failed to connect to websocket: ${(err.inner as dynamic).message}');
|
||||
} else {
|
||||
log('Failed to connect to websocket: $err');
|
||||
}
|
||||
logging.error('[WebSocket] Failed to connect to websocket...', err);
|
||||
|
||||
if (!noRetry) {
|
||||
log('Retry connecting to websocket in 3 seconds...');
|
||||
logging.warning(
|
||||
'[WebSocket] Retry connecting to websocket in 3 seconds...');
|
||||
return Future.delayed(
|
||||
const Duration(seconds: 3),
|
||||
() => connect(noRetry: true),
|
||||
@@ -100,7 +97,8 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
_wsStream!.listen(
|
||||
(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);
|
||||
},
|
||||
onDone: () {
|
||||
|
||||
@@ -21,6 +21,7 @@ import 'package:surface/screens/chat/room.dart';
|
||||
import 'package:surface/screens/explore.dart';
|
||||
import 'package:surface/screens/friend.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_list.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(
|
||||
path: '/album',
|
||||
name: 'album',
|
||||
|
||||
@@ -54,14 +54,20 @@ class AccountSettingsScreen extends StatelessWidget {
|
||||
child: DropdownButton2<Locale?>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) {
|
||||
...EasyLocalization.of(context)!
|
||||
.supportedLocales
|
||||
.mapIndexed((idx, ele) {
|
||||
return DropdownMenuItem<Locale?>(
|
||||
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) {
|
||||
if (value == null) return;
|
||||
_setAccountLanguage(context, value);
|
||||
|
||||
@@ -43,7 +43,8 @@ class UserScreen extends StatefulWidget {
|
||||
State<UserScreen> createState() => _UserScreenState();
|
||||
}
|
||||
|
||||
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
|
||||
class _UserScreenState extends State<UserScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final ScrollController _scrollController = ScrollController();
|
||||
|
||||
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 {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
|
||||
return List.from(
|
||||
final resp =
|
||||
await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
|
||||
setState(() {
|
||||
_records = List.from(
|
||||
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
rethrow;
|
||||
@@ -98,7 +104,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
Future<void> _fetchPublishers() async {
|
||||
try {
|
||||
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(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||
);
|
||||
@@ -144,7 +151,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
'related': _account!.name,
|
||||
});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||
context.showSnackbar(
|
||||
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@@ -160,9 +168,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
|
||||
try {
|
||||
final rel = context.read<SnRelationshipProvider>();
|
||||
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||
await rel.updateRelationship(
|
||||
_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||
context.showSnackbar(
|
||||
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@@ -188,12 +198,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
double _appBarBlur = 0.0;
|
||||
|
||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||
late final _appBarHeight =
|
||||
(_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||
|
||||
void _updateAppBarBlur() {
|
||||
if (_scrollController.offset > _appBarHeight) return;
|
||||
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();
|
||||
_fetchPublishers();
|
||||
_getCheckInRecords();
|
||||
|
||||
try {
|
||||
final rel = context.read<SnRelationshipProvider>();
|
||||
@@ -260,7 +273,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _account!.nick,
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
style:
|
||||
Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: labelShadows,
|
||||
),
|
||||
@@ -268,7 +282,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: '@${_account!.name}',
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: labelShadows,
|
||||
),
|
||||
@@ -339,7 +354,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
visualDensity:
|
||||
VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
@@ -399,7 +415,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
Symbols.circle,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey,
|
||||
color: (_status?.isOnline ?? false)
|
||||
? Colors.green
|
||||
: Colors.grey,
|
||||
).padding(all: 4),
|
||||
const Gap(8),
|
||||
Text(
|
||||
@@ -409,7 +427,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
: 'accountStatusOffline'.tr()
|
||||
: 'loading'.tr(),
|
||||
),
|
||||
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null)
|
||||
if (_status != null &&
|
||||
!_status!.isOnline &&
|
||||
_status!.lastSeenAt != null)
|
||||
Text(
|
||||
'accountStatusLastSeen'.tr(args: [
|
||||
_status!.lastSeenAt != null
|
||||
@@ -429,11 +449,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
(ele) => Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
||||
TextSpan(
|
||||
text: kBadgesMeta[ele.type]?.$1.tr() ??
|
||||
'unknown'.tr()),
|
||||
if (ele.metadata['title'] != null)
|
||||
TextSpan(
|
||||
text: '\n${ele.metadata['title']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
@@ -442,7 +465,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
|
||||
kBadgesMeta[ele.type]?.$2 ??
|
||||
Symbols.question_mark,
|
||||
color: kBadgesMeta[ele.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
@@ -458,7 +482,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
children: [
|
||||
const Icon(Symbols.calendar_add_on),
|
||||
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(
|
||||
@@ -491,17 +517,24 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
children: [
|
||||
const Icon(Symbols.star),
|
||||
const Gap(8),
|
||||
Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
|
||||
Text(
|
||||
'Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
|
||||
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),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: LinearProgressIndicator(
|
||||
value: calcLevelUpProgress(_account?.profile?.experience ?? 0),
|
||||
value: calcLevelUpProgress(
|
||||
_account?.profile?.experience ?? 0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer,
|
||||
).alignment(Alignment.centerLeft),
|
||||
),
|
||||
],
|
||||
@@ -514,21 +547,23 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
SliverToBoxAdapter(child: const Divider()),
|
||||
const SliverGap(12),
|
||||
SliverToBoxAdapter(
|
||||
child: FutureBuilder<List<SnCheckInRecord>>(
|
||||
future: _getCheckInRecords(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||
if (snapshot.data!.length <= 1) {
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (_records == null) return const SizedBox.shrink();
|
||||
if (_records!.length <= 1) {
|
||||
return Text(
|
||||
'accountCheckInNoRecords',
|
||||
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(
|
||||
width: double.infinity,
|
||||
height: 240,
|
||||
child: CheckInRecordChart(records: records),
|
||||
child: CheckInRecordChart(records: _records!),
|
||||
).padding(
|
||||
right: 24,
|
||||
left: 16,
|
||||
@@ -544,7 +579,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
||||
Text('accountBadge')
|
||||
.bold()
|
||||
.fontSize(17)
|
||||
.tr()
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
SizedBox(
|
||||
height: 80,
|
||||
width: double.infinity,
|
||||
@@ -558,7 +597,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
|
||||
kBadgesMeta[badge.type]?.$2 ??
|
||||
Symbols.question_mark,
|
||||
color: kBadgesMeta[badge.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
@@ -568,7 +608,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
||||
subtitle: badge.metadata['title'] != null
|
||||
? Text(badge.metadata['title'])
|
||||
: Text(
|
||||
DateFormat('y/M/d').format(badge.createdAt),
|
||||
DateFormat('y/M/d')
|
||||
.format(badge.createdAt),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -664,7 +705,8 @@ class CheckInRecordChart extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
getTooltipColor: (_) =>
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
@@ -41,6 +42,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Future<void> _fetchWhatsNew() async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/im/whats-new');
|
||||
if (resp.data == null) return;
|
||||
final List<dynamic> out = resp.data;
|
||||
setState(() {
|
||||
_unreadCounts = {for (var v in out) v['channel_id']: v['count']};
|
||||
@@ -72,18 +74,20 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
if (!mounted) return;
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final idSet = <int>{};
|
||||
for (final channel in channels) {
|
||||
if (channel.type == 1) {
|
||||
await ud.listAccount(
|
||||
idSet.addAll(
|
||||
channel.members
|
||||
?.cast<SnChannelMember?>()
|
||||
.map((ele) => ele?.accountId)
|
||||
.where((ele) => ele != null)
|
||||
.toSet() ??
|
||||
{},
|
||||
.cast<int>() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (idSet.isNotEmpty) await ud.listAccount(idSet);
|
||||
|
||||
if (mounted) setState(() => _channels = channels);
|
||||
})
|
||||
@@ -135,6 +139,28 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_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
|
||||
Widget build(BuildContext context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
@@ -258,17 +284,42 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
channel.name),
|
||||
),
|
||||
const Gap(8),
|
||||
if (_unreadCounts?[channel.id] != null)
|
||||
if (_unreadCounts?[channel.id] != null &&
|
||||
_unreadCounts![channel.id]! > 0)
|
||||
Badge(
|
||||
label: Text('${_unreadCounts![channel.id]}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
? Row(
|
||||
children: [
|
||||
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(
|
||||
channel.description,
|
||||
@@ -283,19 +334,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
?.avatar,
|
||||
),
|
||||
onTap: () {
|
||||
if (doExpand) {
|
||||
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);
|
||||
});
|
||||
_onTapChannel(channel);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -305,17 +344,54 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
children: [
|
||||
Expanded(child: Text(channel.name)),
|
||||
const Gap(8),
|
||||
if (_unreadCounts?[channel.id] != null)
|
||||
if (_unreadCounts?[channel.id] != null &&
|
||||
_unreadCounts![channel.id]! > 0)
|
||||
Badge(
|
||||
label: Text('${_unreadCounts![channel.id]}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: lastMessage != null
|
||||
? Text(
|
||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||
? Row(
|
||||
children: [
|
||||
Badge(
|
||||
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(
|
||||
channel.description,
|
||||
@@ -325,23 +401,16 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(
|
||||
content: null,
|
||||
content: channel.realm?.avatar,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onTap: () {
|
||||
if (doExpand) {
|
||||
_unreadCounts?[channel.id] = 0;
|
||||
setState(() => _focusChannel = channel);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (value == true) _refreshChannels(noRemote: true);
|
||||
});
|
||||
_onTapChannel(channel);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:surface/controllers/chat_message_controller.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/channel.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/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
@@ -84,6 +85,10 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
orElse: () => null,
|
||||
);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
final nty = context.read<NotificationProvider>();
|
||||
nty.skippableNotifyChannel = _channel!.id;
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@@ -232,6 +237,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
void dispose() {
|
||||
_wsSubscription?.cancel();
|
||||
_messageController.dispose();
|
||||
final nty = context.read<NotificationProvider>();
|
||||
nty.skippableNotifyChannel = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -95,7 +94,11 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
children: [
|
||||
_HomeDashUpdateWidget(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8, left: 8, right: 8)),
|
||||
bottom: 8,
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
),
|
||||
_HomeDashSpecialDayWidget().padding(horizontal: 8),
|
||||
StaggeredGrid.extent(
|
||||
maxCrossAxisExtent: 280,
|
||||
|
||||
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,
|
||||
'interactive.subscription': Symbols.subscriptions,
|
||||
'interactive.feedback': Symbols.add_reaction,
|
||||
'interactive.reply': Symbols.reply,
|
||||
'messaging.callStart': Symbols.call_received,
|
||||
'wallet.transaction.new': Symbols.receipt,
|
||||
};
|
||||
@@ -57,10 +58,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
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'];
|
||||
_notifications.addAll(
|
||||
resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? [],
|
||||
resp.data['data']
|
||||
?.map((e) => SnNotification.fromJson(e))
|
||||
.cast<SnNotification>() ??
|
||||
[],
|
||||
);
|
||||
nty.updateTray();
|
||||
} catch (err) {
|
||||
@@ -186,7 +194,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
_fetchNotifications();
|
||||
},
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
|
||||
hasReachedMax: _totalCount != null &&
|
||||
_notifications.length >= _totalCount!,
|
||||
itemBuilder: (context, idx) {
|
||||
final nty = _notifications[idx];
|
||||
return Row(
|
||||
@@ -218,13 +227,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
isAutoWarp: true,
|
||||
),
|
||||
),
|
||||
if (['interactive.reply', 'interactive.feedback', 'interactive.subscription']
|
||||
.contains(nty.topic) &&
|
||||
if ([
|
||||
'interactive.reply',
|
||||
'interactive.feedback',
|
||||
'interactive.subscription'
|
||||
].contains(nty.topic) &&
|
||||
nty.metadata['related_post'] != null)
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
@@ -243,7 +256,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postDetail',
|
||||
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(
|
||||
icon: const Icon(Symbols.check),
|
||||
padding: EdgeInsets.all(0),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed:
|
||||
_isSubmitting ? null : () => _markOneAsRead(nty),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16);
|
||||
|
||||
@@ -95,8 +95,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||
);
|
||||
final beforeId = config.prefs.getInt('int_last_publisher_id');
|
||||
_writeController
|
||||
.setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull);
|
||||
_writeController.setPublisher(
|
||||
_publishers?.where((ele) => ele.id == beforeId).firstOrNull ??
|
||||
_publishers?.firstOrNull);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@@ -125,7 +126,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyV,
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
modifiers: [
|
||||
(!kIsWeb && Platform.isMacOS)
|
||||
? HotKeyModifier.meta
|
||||
: HotKeyModifier.control
|
||||
],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
|
||||
@@ -232,7 +237,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
if (widget.extraProps != null) {
|
||||
_writeController.contentController.text = widget.extraProps!.text ?? '';
|
||||
_writeController.titleController.text = widget.extraProps!.title ?? '';
|
||||
_writeController.descriptionController.text = widget.extraProps!.description ?? '';
|
||||
_writeController.descriptionController.text =
|
||||
widget.extraProps!.description ?? '';
|
||||
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
|
||||
}
|
||||
}
|
||||
@@ -253,7 +259,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(),
|
||||
text: _writeController.title.isNotEmpty
|
||||
? _writeController.title
|
||||
: 'untitled'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
@@ -280,7 +288,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
children: [
|
||||
if (_writeController.editingPost != null)
|
||||
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(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
@@ -294,13 +303,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
children: [
|
||||
const Icon(Icons.edit, size: 16),
|
||||
const Gap(10),
|
||||
Text('postEditingNotice').tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
||||
Text('postEditingNotice').tr(args: [
|
||||
'@${_writeController.editingPost!.publisher.name}'
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_writeController.replyingPost != null)
|
||||
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(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
@@ -314,7 +326,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
children: [
|
||||
const Icon(Symbols.reply, size: 16),
|
||||
const Gap(10),
|
||||
Text('@${_writeController.replyingPost!.publisher.name}').bold(),
|
||||
Text('@${_writeController.replyingPost!.publisher.name}')
|
||||
.bold(),
|
||||
const Gap(4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -328,7 +341,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
if (_writeController.repostingPost != null)
|
||||
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(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
@@ -342,7 +356,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
children: [
|
||||
const Icon(Symbols.forward, size: 16),
|
||||
const Gap(10),
|
||||
Text('@${_writeController.repostingPost!.publisher.name}').bold(),
|
||||
Text('@${_writeController.repostingPost!.publisher.name}')
|
||||
.bold(),
|
||||
const Gap(4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -384,7 +399,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
})
|
||||
.padding(top: 8),
|
||||
),
|
||||
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
|
||||
if (_writeController.attachments.isNotEmpty ||
|
||||
_writeController.thumbnail != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
@@ -393,16 +409,19 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
attachments: _writeController.attachments,
|
||||
isBusy: _writeController.isBusy,
|
||||
onUpload: (int idx) async {
|
||||
await _writeController.uploadSingleAttachment(context, idx);
|
||||
await _writeController.uploadSingleAttachment(
|
||||
context, idx);
|
||||
},
|
||||
onInsertLink: (int idx) async {
|
||||
_writeController.contentController.text +=
|
||||
'\n';
|
||||
},
|
||||
onUpdate: (int idx, PostWriteMedia updatedMedia) async {
|
||||
onUpdate:
|
||||
(int idx, PostWriteMedia updatedMedia) async {
|
||||
_writeController.setIsBusy(true);
|
||||
try {
|
||||
_writeController.setAttachmentAt(idx, updatedMedia);
|
||||
_writeController.setAttachmentAt(
|
||||
idx, updatedMedia);
|
||||
} finally {
|
||||
_writeController.setIsBusy(false);
|
||||
}
|
||||
@@ -415,7 +434,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
_writeController.setIsBusy(false);
|
||||
}
|
||||
},
|
||||
onUpdateBusy: (state) => _writeController.setIsBusy(state),
|
||||
onUpdateBusy: (state) =>
|
||||
_writeController.setIsBusy(state),
|
||||
).padding(bottom: 8),
|
||||
),
|
||||
],
|
||||
@@ -426,11 +446,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_writeController.isBusy && _writeController.progress != null)
|
||||
if (_writeController.isBusy &&
|
||||
_writeController.progress != null)
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: _writeController.progress),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
|
||||
builder: (context, value, _) =>
|
||||
LinearProgressIndicator(value: value, minHeight: 2),
|
||||
)
|
||||
else if (_writeController.isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
@@ -439,12 +461,14 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
Container(
|
||||
child: _writeController.temporaryRestored
|
||||
? 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(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
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: [
|
||||
const Icon(Icons.restore, size: 20),
|
||||
const Gap(8),
|
||||
Expanded(child: Text('postLocalDraftRestored').tr()),
|
||||
Expanded(
|
||||
child:
|
||||
Text('postLocalDraftRestored').tr()),
|
||||
InkWell(
|
||||
child: Text('dialogDismiss').tr(),
|
||||
onTap: () {
|
||||
@@ -464,8 +490,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
))
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
|
||||
.height(_writeController.temporaryRestored ? 32 : 0,
|
||||
animate: true)
|
||||
.animate(const Duration(milliseconds: 300),
|
||||
Curves.fastLinearToSlowEaseIn),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -485,11 +513,18 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
if (_writeController.mode == 'stories')
|
||||
IconButton(
|
||||
icon: Icon(Symbols.poll, color: Theme.of(context).colorScheme.primary),
|
||||
icon: Icon(Symbols.poll,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: _writeController.poll == null
|
||||
backgroundColor:
|
||||
_writeController.poll == null
|
||||
? null
|
||||
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
|
||||
: WidgetStatePropertyAll(
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer),
|
||||
),
|
||||
onPressed: () {
|
||||
_showPollEditorDialog();
|
||||
@@ -497,14 +532,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
if (_writeController.mode == 'articles')
|
||||
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(
|
||||
backgroundColor: _writeController.thumbnail == null
|
||||
backgroundColor:
|
||||
_writeController.thumbnail == null
|
||||
? null
|
||||
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
|
||||
: WidgetStatePropertyAll(
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_writeController.thumbnail != null) {
|
||||
if (_writeController.thumbnail !=
|
||||
null) {
|
||||
_writeController.setThumbnail(null);
|
||||
return;
|
||||
}
|
||||
@@ -517,7 +560,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: (_writeController.isBusy || _writeController.publisher == null)
|
||||
onPressed: (_writeController.isBusy ||
|
||||
_writeController.publisher == null)
|
||||
? null
|
||||
: () {
|
||||
_writeController.sendPost(context).then((_) {
|
||||
@@ -556,7 +600,8 @@ class _PostPublisherPopup extends StatelessWidget {
|
||||
final List<SnPublisher>? publishers;
|
||||
final Function onUpdate;
|
||||
|
||||
const _PostPublisherPopup({required this.controller, this.publishers, required this.onUpdate});
|
||||
const _PostPublisherPopup(
|
||||
{required this.controller, this.publishers, required this.onUpdate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -568,7 +613,9 @@ class _PostPublisherPopup extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.face, size: 24),
|
||||
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),
|
||||
ListTile(
|
||||
@@ -612,7 +659,8 @@ class _PostRealmPopup extends StatelessWidget {
|
||||
final List<SnRealm>? realms;
|
||||
final Function onUpdate;
|
||||
|
||||
const _PostRealmPopup({required this.controller, this.realms, required this.onUpdate});
|
||||
const _PostRealmPopup(
|
||||
{required this.controller, this.realms, required this.onUpdate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -624,7 +672,8 @@ class _PostRealmPopup extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Symbols.face, size: 24),
|
||||
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),
|
||||
ListTile(
|
||||
@@ -665,7 +714,8 @@ class _PostStoryEditor extends StatelessWidget {
|
||||
final Function? onTapPublisher;
|
||||
final Function? onTapRealm;
|
||||
|
||||
const _PostStoryEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
const _PostStoryEditor(
|
||||
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -717,7 +767,8 @@ class _PostStoryEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
@@ -732,8 +783,10 @@ class _PostStoryEditor extends StatelessWidget {
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration:
|
||||
controller.contentInsertionConfiguration,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -749,7 +802,8 @@ class _PostArticleEditor extends StatelessWidget {
|
||||
final Function? onTapPublisher;
|
||||
final Function? onTapRealm;
|
||||
|
||||
const _PostArticleEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
const _PostArticleEditor(
|
||||
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -857,8 +911,10 @@ class _PostArticleEditor extends StatelessWidget {
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration:
|
||||
controller.contentInsertionConfiguration,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@@ -893,7 +949,8 @@ class _PostArticleEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
contentInsertionConfiguration:
|
||||
controller.contentInsertionConfiguration,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -906,7 +963,8 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
final Function? onTapPublisher;
|
||||
final Function? onTapRealm;
|
||||
|
||||
const _PostQuestionEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
const _PostQuestionEditor(
|
||||
{required this.controller, this.onTapPublisher, this.onTapRealm});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -958,7 +1016,8 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
@@ -969,7 +1028,8 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
@@ -984,8 +1044,10 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration: controller.contentInsertionConfiguration,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
contentInsertionConfiguration:
|
||||
controller.contentInsertionConfiguration,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1001,7 +1063,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
final Function? onTapPublisher;
|
||||
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 {
|
||||
final video = await showDialog<SnAttachment?>(
|
||||
@@ -1022,7 +1085,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
|
||||
final result = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)),
|
||||
builder: (context) => PendingAttachmentAltDialog(
|
||||
media: PostWriteMedia(controller.videoAttachment)),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
@@ -1034,7 +1098,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
|
||||
final result = await showDialog<SnAttachmentBoost?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)),
|
||||
builder: (context) => PendingAttachmentBoostDialog(
|
||||
media: PostWriteMedia(controller.videoAttachment)),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
@@ -1077,7 +1142,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
|
||||
try {
|
||||
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);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
@@ -1087,7 +1153,11 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
@@ -1120,7 +1190,10 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
const Gap(6),
|
||||
TextField(
|
||||
controller: controller.titleController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
@@ -1128,7 +1201,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
@@ -1140,7 +1214,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(12),
|
||||
Container(
|
||||
@@ -1177,7 +1252,8 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
label: 'attachmentCopyRandomId'.tr(),
|
||||
icon: Symbols.content_copy,
|
||||
onSelected: () {
|
||||
Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid));
|
||||
Clipboard.setData(ClipboardData(
|
||||
text: controller.videoAttachment!.rid));
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
@@ -1196,7 +1272,9 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: controller.videoAttachment == null ? () => _selectVideo(context) : null,
|
||||
onTap: controller.videoAttachment == null
|
||||
? () => _selectVideo(context)
|
||||
: null,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: controller.videoAttachment == null
|
||||
@@ -1224,6 +1302,10 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -48,6 +49,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
late final SharedPreferences _prefs;
|
||||
String _docBasepath = '/';
|
||||
|
||||
final TextEditingController _customFontController = TextEditingController();
|
||||
final TextEditingController _serverUrlController = TextEditingController();
|
||||
|
||||
@override
|
||||
@@ -62,11 +64,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final config = context.read<ConfigProvider>();
|
||||
_prefs = config.prefs;
|
||||
_serverUrlController.text = config.serverUrl;
|
||||
if (_prefs.getString(kAppCustomFonts) != null) {
|
||||
_customFontController.text = _prefs.getString(kAppCustomFonts) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_serverUrlController.dispose();
|
||||
_customFontController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -330,6 +336,47 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
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(
|
||||
@@ -534,6 +581,37 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
.fontSize(17)
|
||||
.tr()
|
||||
.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(
|
||||
leading: const Icon(Symbols.database),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
@@ -618,6 +696,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
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(
|
||||
title: Text('settingsMiscAbout').tr(),
|
||||
subtitle: Text('settingsMiscAboutDescription').tr(),
|
||||
|
||||
@@ -179,7 +179,9 @@ class _StickerScreenState extends State<StickerScreen>
|
||||
child: InfiniteList(
|
||||
itemCount: _packs.length,
|
||||
onFetchData: _fetchPacks,
|
||||
hasReachedMax: _totalCount != null && _packs.length >= _totalCount!,
|
||||
hasReachedMax:
|
||||
(_totalCount != null && _packs.length >= _totalCount!) ||
|
||||
_tabController.index == 2,
|
||||
isLoading: _isBusy,
|
||||
itemBuilder: (context, idx) {
|
||||
final pack = _packs[idx];
|
||||
|
||||
@@ -11,10 +11,19 @@ class ThemeSet {
|
||||
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(
|
||||
light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3),
|
||||
dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3),
|
||||
light: await createAppTheme(
|
||||
Brightness.light,
|
||||
useMaterial3: useMaterial3,
|
||||
customFonts: customFonts,
|
||||
),
|
||||
dark: await createAppTheme(
|
||||
Brightness.dark,
|
||||
useMaterial3: useMaterial3,
|
||||
customFonts: customFonts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,24 +31,35 @@ Future<ThemeData> createAppTheme(
|
||||
Brightness brightness, {
|
||||
Color? seedColorOverride,
|
||||
bool? useMaterial3,
|
||||
String? customFonts,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
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(
|
||||
seedColor: seedColorOverride ?? seedColor,
|
||||
brightness: brightness,
|
||||
);
|
||||
|
||||
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||
final hasAppBarTransparent =
|
||||
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(
|
||||
useMaterial3: useM3,
|
||||
colorScheme: colorScheme,
|
||||
brightness: brightness,
|
||||
fontFamily: inUseFonts?.firstOrNull,
|
||||
fontFamilyFallback: inUseFonts?.sublist(1),
|
||||
iconTheme: IconThemeData(
|
||||
fill: 0,
|
||||
weight: 400,
|
||||
@@ -52,8 +72,10 @@ Future<ThemeData> createAppTheme(
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: hasAppBarTransparent ? 0 : null,
|
||||
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
|
||||
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
backgroundColor:
|
||||
hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
|
||||
foregroundColor:
|
||||
hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
),
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
|
||||
@@ -10,6 +10,7 @@ class AccountImage extends StatelessWidget {
|
||||
final Color? foregroundColor;
|
||||
final double? radius;
|
||||
final Widget? fallbackWidget;
|
||||
final Widget? badge;
|
||||
|
||||
const AccountImage({
|
||||
super.key,
|
||||
@@ -18,6 +19,7 @@ class AccountImage extends StatelessWidget {
|
||||
this.foregroundColor,
|
||||
this.radius,
|
||||
this.fallbackWidget,
|
||||
this.badge,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -26,7 +28,9 @@ class AccountImage extends StatelessWidget {
|
||||
final url = sn.getAttachmentUrl(content ?? '');
|
||||
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
return CircleAvatar(
|
||||
return Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
key: Key('attachment-${content.hashCode}'),
|
||||
radius: radius,
|
||||
backgroundColor: backgroundColor,
|
||||
@@ -46,6 +50,14 @@ class AccountImage extends StatelessWidget {
|
||||
color: foregroundColor,
|
||||
))
|
||||
: null,
|
||||
),
|
||||
if (badge != null)
|
||||
Positioned(
|
||||
right: -4,
|
||||
bottom: -4,
|
||||
child: badge!,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:cross_file/cross_file.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:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
@@ -17,10 +17,12 @@ class PendingVideoCompressDialog extends StatefulWidget {
|
||||
const PendingVideoCompressDialog({super.key, required this.media});
|
||||
|
||||
@override
|
||||
State<PendingVideoCompressDialog> createState() => _PendingVideoCompressDialogState();
|
||||
State<PendingVideoCompressDialog> createState() =>
|
||||
_PendingVideoCompressDialogState();
|
||||
}
|
||||
|
||||
class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog> {
|
||||
class _PendingVideoCompressDialogState
|
||||
extends State<PendingVideoCompressDialog> {
|
||||
VideoQuality _quality = VideoQuality.DefaultQuality;
|
||||
|
||||
bool _isBusy = false;
|
||||
@@ -50,7 +52,7 @@ class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog>
|
||||
void initState() {
|
||||
super.initState();
|
||||
_progressSubscription = VideoCompress.compressProgress$.subscribe((event) {
|
||||
log('[Compress] Progress: $event');
|
||||
logging.debug('[Paperclip.VideoCompress] Progress: $event');
|
||||
setState(() {
|
||||
_progress = event / 100;
|
||||
_isBusy = event < 100;
|
||||
@@ -132,7 +134,9 @@ class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog>
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text('attachmentCompressQualityHint', style: Theme.of(context).textTheme.bodySmall!).tr(),
|
||||
Text('attachmentCompressQualityHint',
|
||||
style: Theme.of(context).textTheme.bodySmall!)
|
||||
.tr(),
|
||||
if (_isBusy)
|
||||
TweenAnimationBuilder<double>(
|
||||
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/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/account_popover.dart';
|
||||
@@ -105,6 +106,22 @@ class ChatMessage extends StatelessWidget {
|
||||
GestureDetector(
|
||||
child: AccountImage(
|
||||
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: () {
|
||||
if (user == null) return;
|
||||
@@ -161,7 +178,7 @@ class ChatMessage extends StatelessWidget {
|
||||
if (data.preload?.quoteEvent != null)
|
||||
StyledWidget(Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 480,
|
||||
maxWidth: 360,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
@@ -210,10 +227,12 @@ class ChatMessage extends StatelessWidget {
|
||||
AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
bordered: true,
|
||||
maxHeight: 560,
|
||||
maxWidth: 480,
|
||||
minWidth: 480,
|
||||
padding: padding.copyWith(top: 8, left: 48 + padding.left),
|
||||
maxHeight: 360,
|
||||
maxWidth: 480 - 48 - padding.left,
|
||||
padding: padding.copyWith(
|
||||
top: 8,
|
||||
left: isCompact ? padding.left : 48 + padding.left,
|
||||
),
|
||||
),
|
||||
if (!hasMerged && !isCompact)
|
||||
const Gap(12)
|
||||
@@ -292,8 +311,6 @@ class _ChatMessageText extends StatelessWidget {
|
||||
buttonItems: items,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: MarkdownTextContent(
|
||||
content: data.body['text'],
|
||||
isAutoWarp: true,
|
||||
@@ -301,7 +318,6 @@ class _ChatMessageText extends StatelessWidget {
|
||||
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (data.updatedAt != data.createdAt)
|
||||
Text(
|
||||
'messageEditedHint'.tr(),
|
||||
|
||||
@@ -28,7 +28,8 @@ class ChatMessageInput extends StatefulWidget {
|
||||
final ChatMessageController controller;
|
||||
final SnChannelMember? otherMember;
|
||||
|
||||
const ChatMessageInput({super.key, required this.controller, this.otherMember});
|
||||
const ChatMessageInput(
|
||||
{super.key, required this.controller, this.otherMember});
|
||||
|
||||
@override
|
||||
State<ChatMessageInput> createState() => ChatMessageInputState();
|
||||
@@ -45,12 +46,20 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyV,
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
modifiers: [
|
||||
(!kIsWeb && Platform.isMacOS)
|
||||
? HotKeyModifier.meta
|
||||
: HotKeyModifier.control
|
||||
],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
final HotKey _newLineHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.enter,
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
modifiers: [
|
||||
(!kIsWeb && Platform.isMacOS)
|
||||
? HotKeyModifier.meta
|
||||
: HotKeyModifier.control
|
||||
],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
|
||||
@@ -100,7 +109,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
void setEdit(SnChatMessage? value) {
|
||||
_contentController.text = value?.body['text'] ?? '';
|
||||
_attachments.clear();
|
||||
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||
_attachments.addAll(
|
||||
value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
|
||||
setState(() => _editingMessage = value);
|
||||
}
|
||||
|
||||
@@ -139,7 +149,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
media.name,
|
||||
'messaging',
|
||||
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(
|
||||
@@ -171,7 +183,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
widget.controller.sendMessage(
|
||||
_editingMessage != null ? 'messages.edit' : 'messages.new',
|
||||
_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,
|
||||
quoteId: _replyingMessage?.id,
|
||||
editingMessage: _editingMessage,
|
||||
@@ -232,12 +247,15 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: _progress),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
|
||||
builder: (context, value, _) =>
|
||||
LinearProgressIndicator(value: value, minHeight: 2),
|
||||
)
|
||||
else if (_isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
Padding(
|
||||
padding: _attachments.isNotEmpty ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
||||
padding: _attachments.isNotEmpty
|
||||
? const EdgeInsets.only(top: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: PostMediaPendingList(
|
||||
attachments: _attachments,
|
||||
isBusy: _isBusy,
|
||||
@@ -249,9 +267,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
},
|
||||
onUpdateBusy: (state) => setState(() => _isBusy = state),
|
||||
),
|
||||
)
|
||||
.height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: _replyingMessage != null
|
||||
@@ -272,7 +289,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
|
||||
_replyingMessage?.body['text'] ??
|
||||
'${_replyingMessage?.sender.nick}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -289,9 +307,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
).padding(vertical: 8),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
.height(_replyingMessage != null ? 38 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
).height(_replyingMessage != null ? 38 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: _editingMessage != null
|
||||
@@ -312,7 +329,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
|
||||
_editingMessage?.body['text'] ??
|
||||
'${_editingMessage?.sender.nick}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -330,12 +348,13 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
).padding(vertical: 8),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
.height(_editingMessage != null ? 38 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
).height(_editingMessage != null ? 38 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
constraints: BoxConstraints(minHeight: 56, maxHeight: 240),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
@@ -347,11 +366,14 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
? 'fieldChatMessageDirect'.tr(args: [
|
||||
'@${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,
|
||||
),
|
||||
textInputAction: TextInputAction.send,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onSubmitted: (_) {
|
||||
if (_isBusy) return;
|
||||
_sendMessage();
|
||||
@@ -366,7 +388,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
Symbols.mood,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
@@ -386,13 +409,14 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
Symbols.send,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(horizontal: 16),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -403,7 +427,8 @@ class _StickerPicker extends StatelessWidget {
|
||||
final Function? onDismiss;
|
||||
final Function(String)? onInsert;
|
||||
|
||||
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
|
||||
const _StickerPicker(
|
||||
{this.onDismiss, required this.originalText, this.onInsert});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -413,7 +438,9 @@ class _StickerPicker extends StatelessWidget {
|
||||
onDismiss?.call();
|
||||
},
|
||||
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(
|
||||
elevation: 8,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
@@ -426,8 +453,10 @@ class _StickerPicker extends StatelessWidget {
|
||||
return <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 8),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -439,7 +468,8 @@ class _StickerPicker extends StatelessWidget {
|
||||
),
|
||||
GridView.builder(
|
||||
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,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 48,
|
||||
@@ -462,7 +492,8 @@ class _StickerPicker extends StatelessWidget {
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ':${element.pack.prefix}${element.alias}:\n',
|
||||
text:
|
||||
':${element.pack.prefix}${element.alias}:\n',
|
||||
style: GoogleFonts.robotoMono()),
|
||||
TextSpan(text: element.name).bold(),
|
||||
],
|
||||
@@ -471,11 +502,15 @@ class _StickerPicker extends StatelessWidget {
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8)),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8)),
|
||||
child: UniversalImage(
|
||||
sn.getAttachmentUrl(element.attachment.rid),
|
||||
width: 48,
|
||||
|
||||
@@ -133,7 +133,8 @@ extension AppPromptExtension on BuildContext {
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..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';
|
||||
const k = 1024;
|
||||
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();
|
||||
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||
}
|
||||
@@ -167,4 +178,15 @@ extension StringFormatter on String {
|
||||
String capitalize() {
|
||||
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: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_latex/flutter_markdown_latex.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:markdown/markdown.dart' as markdown;
|
||||
@@ -72,21 +75,27 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
),
|
||||
code: GoogleFonts.robotoMono(height: 1),
|
||||
),
|
||||
builders: {},
|
||||
builders: {
|
||||
'latex': LatexElementBuilder(),
|
||||
'code': HighlightBuilder(),
|
||||
},
|
||||
softLineBreak: true,
|
||||
extensionSet: markdown.ExtensionSet(
|
||||
<markdown.BlockSyntax>[
|
||||
markdown.CodeBlockSyntax(),
|
||||
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||
markdown.CodeBlockSyntax(),
|
||||
markdown.FencedCodeBlockSyntax(),
|
||||
LatexBlockSyntax(),
|
||||
],
|
||||
<markdown.InlineSyntax>[
|
||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes,
|
||||
if (isAutoWarp) markdown.LineBreakSyntax(),
|
||||
_UserNameCardInlineSyntax(),
|
||||
_CustomEmoteInlineSyntax(context),
|
||||
markdown.AutolinkSyntax(),
|
||||
markdown.AutolinkExtensionSyntax(),
|
||||
markdown.CodeSyntax(),
|
||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
||||
LatexInlineSyntax(),
|
||||
],
|
||||
),
|
||||
onTapLink: (text, href, title) async {
|
||||
@@ -260,3 +269,56 @@ class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
|
||||
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(
|
||||
'Solar Network',
|
||||
style: GoogleFonts.spaceGrotesk(),
|
||||
textAlign: !kIsWeb
|
||||
? Platform.isMacOS
|
||||
textAlign: Platform.isMacOS
|
||||
? TextAlign.center
|
||||
: null
|
||||
: null,
|
||||
: TextAlign.start,
|
||||
).padding(horizontal: 12, vertical: 5),
|
||||
),
|
||||
if (!Platform.isMacOS)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(child: MoveWindow()),
|
||||
Row(
|
||||
children: [
|
||||
MinimizeWindowButton(
|
||||
colors: windowButtonColor),
|
||||
MaximizeWindowButton(
|
||||
colors: windowButtonColor),
|
||||
MinimizeWindowButton(colors: windowButtonColor),
|
||||
if (!Platform.isMacOS)
|
||||
MaximizeWindowButton(colors: windowButtonColor),
|
||||
if (!Platform.isMacOS)
|
||||
CloseWindowButton(
|
||||
colors: windowButtonColor),
|
||||
],
|
||||
),
|
||||
],
|
||||
colors: windowButtonColor,
|
||||
onPressed: () => appWindow.hide(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -229,13 +219,15 @@ class AppRootScaffold extends StatelessWidget {
|
||||
bottom: safeBottom > 0 ? safeBottom : 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: ConnectionIndicator())
|
||||
child: ConnectionIndicator(),
|
||||
)
|
||||
else
|
||||
Positioned(
|
||||
top: safeTop > 0 ? safeTop : 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: ConnectionIndicator()),
|
||||
child: ConnectionIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||
|
||||
@@ -67,7 +67,10 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
|
||||
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) {
|
||||
final updatedMedia = PostWriteMedia.fromBytes(
|
||||
@@ -133,7 +136,8 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
|
||||
final result = await showDialog<SnAttachmentBoost?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentBoostDialog(media: attachments[idx]),
|
||||
builder: (context) =>
|
||||
PendingAttachmentBoostDialog(media: attachments[idx]),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
@@ -165,11 +169,15 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
onUpdate!(idx, PostWriteMedia(result));
|
||||
}
|
||||
|
||||
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
||||
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||
ContextMenu _createContextMenu(
|
||||
BuildContext context, int idx, PostWriteMedia media) {
|
||||
final canCompressVideo =
|
||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||
return ContextMenu(
|
||||
entries: [
|
||||
if (media.attachment == null && media.type == SnMediaType.video && canCompressVideo)
|
||||
if (media.attachment == null &&
|
||||
media.type == SnMediaType.video &&
|
||||
canCompressVideo)
|
||||
MenuItem(
|
||||
label: 'attachmentCompressVideo'.tr(),
|
||||
icon: Symbols.compress,
|
||||
@@ -312,12 +320,15 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: switch (media.type) {
|
||||
SnMediaType.image => LayoutBuilder(builder: (context, constraints) {
|
||||
SnMediaType.image =>
|
||||
LayoutBuilder(builder: (context, constraints) {
|
||||
return Image(
|
||||
image: media.getImageProvider(
|
||||
context,
|
||||
width: (constraints.maxWidth * devicePixelRatio).round(),
|
||||
height: (constraints.maxHeight * devicePixelRatio).round(),
|
||||
width:
|
||||
(constraints.maxWidth * devicePixelRatio).round(),
|
||||
height:
|
||||
(constraints.maxHeight * devicePixelRatio).round(),
|
||||
)!,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
@@ -326,8 +337,11 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (media.attachment?.thumbnail != null)
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
||||
const Icon(Symbols.videocam, color: Colors.white, shadows: [
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(
|
||||
media.attachment!.thumbnail!.rid)),
|
||||
const Icon(Symbols.videocam,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 8.0,
|
||||
@@ -340,8 +354,11 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (media.attachment?.thumbnail != null)
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
||||
const Icon(Symbols.audio_file, color: Colors.white, shadows: [
|
||||
AutoResizeUniversalImage(sn.getAttachmentUrl(
|
||||
media.attachment!.thumbnail!.rid)),
|
||||
const Icon(Symbols.audio_file,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 8.0,
|
||||
@@ -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)
|
||||
SizedBox(
|
||||
width: 160,
|
||||
@@ -374,7 +392,8 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
else if (media.file != null)
|
||||
Text(media.file!.name, maxLines: 1, overflow: TextOverflow.ellipsis)
|
||||
Text(media.file!.name,
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis)
|
||||
else
|
||||
Text('unknown'.tr()),
|
||||
if (media.attachment != null)
|
||||
@@ -387,7 +406,8 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
FutureBuilder<int?>(
|
||||
future: media.length(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||
if (!snapshot.hasData)
|
||||
return const SizedBox.shrink();
|
||||
return Text(
|
||||
snapshot.data!.formatBytes(),
|
||||
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(
|
||||
children: [
|
||||
Icon(Symbols.bolt, size: 16),
|
||||
@@ -406,7 +427,8 @@ class _PostMediaPendingItem extends StatelessWidget {
|
||||
Text('attachmentGotBoosted').tr().fontSize(13),
|
||||
],
|
||||
),
|
||||
if (media.attachment != null && media.attachment!.compressedId != null)
|
||||
if (media.attachment != null &&
|
||||
media.attachment!.compressedId != null)
|
||||
Row(
|
||||
children: [
|
||||
Icon(Symbols.compress, size: 16),
|
||||
@@ -527,12 +549,20 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
icon: Icon(
|
||||
Symbols.add_photo_alternate,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
||||
if (!kIsWeb &&
|
||||
!Platform.isLinux &&
|
||||
!Platform.isMacOS &&
|
||||
!Platform.isWindows)
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -545,7 +575,10 @@ class AddPostMediaButton extends StatelessWidget {
|
||||
_takeMedia(false);
|
||||
},
|
||||
),
|
||||
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
||||
if (!kIsWeb &&
|
||||
!Platform.isLinux &&
|
||||
!Platform.isMacOS &&
|
||||
!Platform.isWindows)
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -193,7 +193,7 @@ PODS:
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (~> 3.49.0)
|
||||
- sqlite3 (~> 3.49.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
@@ -378,7 +378,7 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
|
||||
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
|
||||
|
||||
110
pubspec.lock
110
pubspec.lock
@@ -405,10 +405,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
|
||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
dismissible_page:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -525,10 +525,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: "6f6bfa8797f296965bdc3e1f702574ab49a540c19b9237b401e7c2b25dfe594c"
|
||||
sha256: "9467b7c4eedf0bd4c9306b0ec12455b278f6366962be061d0978a446c103c111"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "9.0.1"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -679,7 +679,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||
@@ -710,6 +710,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -803,6 +811,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -873,18 +897,18 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
|
||||
sha256: "532008570b7fd20310db8cb9c8ebc5bafd5aa4e52c4358db4e5ddc29f74f4be3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.8"
|
||||
version: "3.0.1"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "3.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -941,6 +965,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1393,18 +1433,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.1"
|
||||
version: "8.3.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76"
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.2.0"
|
||||
pasteboard:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1950,10 +1990,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233"
|
||||
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.30"
|
||||
version: "0.5.31"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2018,6 +2058,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2050,6 +2122,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: tuple
|
||||
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
12
pubspec.yaml
12
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
|
||||
# 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.
|
||||
version: 2.3.2+72
|
||||
version: 2.3.2+75
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@@ -47,7 +47,7 @@ dependencies:
|
||||
dio: ^5.7.0
|
||||
dio_smart_retry: ^7.0.1
|
||||
very_good_infinite_list: ^0.9.0
|
||||
freezed_annotation: ^2.4.4
|
||||
freezed_annotation: ^3.0.0
|
||||
json_annotation: ^4.9.0
|
||||
gap: ^3.0.1
|
||||
markdown: ^7.2.2
|
||||
@@ -128,6 +128,12 @@ dependencies:
|
||||
drift: ^2.25.1
|
||||
drift_flutter: ^0.2.4
|
||||
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:
|
||||
flutter_test:
|
||||
@@ -140,7 +146,7 @@ dev_dependencies:
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^5.0.0
|
||||
build_runner: ^2.4.15
|
||||
freezed: ^2.5.7
|
||||
freezed: ^3.0.1
|
||||
json_serializable: ^6.8.0
|
||||
icons_launcher: ^3.0.0
|
||||
flutter_native_splash: ^2.4.2
|
||||
|
||||
Reference in New Issue
Block a user