Compare commits
16 Commits
2.4.2+80
...
71b41d470a
| Author | SHA1 | Date | |
|---|---|---|---|
| 71b41d470a | |||
| 7052b5b635 | |||
| f356e08f79 | |||
| 152872db65 | |||
| dfe117d04f | |||
| caf63f0cbe | |||
| b8f5cc82f9 | |||
| 360bc50f21 | |||
| 2de93a0486 | |||
| 02227852f8 | |||
| ad16de595b | |||
| 9f8c8923d9 | |||
| 060bfa4887 | |||
| e68ada2d04 | |||
| d6013078bd | |||
| 5976d61997 |
@@ -130,7 +130,7 @@
|
|||||||
"accountPublishersSubtitle": "Manage your publish identities.",
|
"accountPublishersSubtitle": "Manage your publish identities.",
|
||||||
"accountSettings": "Account Settings",
|
"accountSettings": "Account Settings",
|
||||||
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
||||||
"accountProfileEdit": "Edit your profile",
|
"accountProfileEdit": "Edit Profile",
|
||||||
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
||||||
"accountWallet": "Wallet",
|
"accountWallet": "Wallet",
|
||||||
"accountWalletSubtitle": "View your balance and transactions.",
|
"accountWalletSubtitle": "View your balance and transactions.",
|
||||||
@@ -338,6 +338,7 @@
|
|||||||
"fieldAttachmentRandomId": "Random ID",
|
"fieldAttachmentRandomId": "Random ID",
|
||||||
"fieldAttachmentAlt": "Alternative text",
|
"fieldAttachmentAlt": "Alternative text",
|
||||||
"addAttachmentFromAlbum": "Add from album",
|
"addAttachmentFromAlbum": "Add from album",
|
||||||
|
"addAttachmentFromFiles": "Add from files",
|
||||||
"addAttachmentFromClipboard": "Paste file",
|
"addAttachmentFromClipboard": "Paste file",
|
||||||
"addAttachmentFromCameraPhoto": "Take photo",
|
"addAttachmentFromCameraPhoto": "Take photo",
|
||||||
"addAttachmentFromCameraVideo": "Take video",
|
"addAttachmentFromCameraVideo": "Take video",
|
||||||
@@ -846,5 +847,39 @@
|
|||||||
"translating": "Translating…",
|
"translating": "Translating…",
|
||||||
"translated": "Translated",
|
"translated": "Translated",
|
||||||
"settingsAutoTranslate": "Auto Translate",
|
"settingsAutoTranslate": "Auto Translate",
|
||||||
"settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages."
|
"settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages.",
|
||||||
|
"trayMenuHide": "Hide",
|
||||||
|
"accountSettingsNotify": "Notify Settings",
|
||||||
|
"accountSettingsNotifyDescription": "Adjust the types of notifications you receive.",
|
||||||
|
"accountSettingsSecurity": "Security Settings",
|
||||||
|
"accountSettingsSecurityDescription": "Adjust your account security settings.",
|
||||||
|
"save": "Save",
|
||||||
|
"notificationTopicPostFeedback": "Post Feedback",
|
||||||
|
"notificationTopicPostReply": "Post Replies",
|
||||||
|
"notificationTopicPostSubscription": "Post Subscriptions",
|
||||||
|
"notificationTopicMessaging": "New Messages",
|
||||||
|
"notificationTopicMessagingCall": "Incoming Calls",
|
||||||
|
"notificationTopicGeneral": "General",
|
||||||
|
"authMaximumAuthSteps": "Maximum Authenticate Steps",
|
||||||
|
"authMaximumAuthStepsDescription": {
|
||||||
|
"one": "Maximum ask for {} step authenticate",
|
||||||
|
"other": "Maximum ask for {} steps authenticate"
|
||||||
|
},
|
||||||
|
"authAlwaysRisky": "Always Risky",
|
||||||
|
"authAlwaysRiskyDescription": "Always ask for the highest steps count of authentication when logging in.",
|
||||||
|
"chatUnjoined": "Unjoined Channel",
|
||||||
|
"chatUnjoinedDescription": "You haven't joined this channel, so you can't send messages either view messages in it.",
|
||||||
|
"chatUnjoinedPublicDescription": "Fortunately, this is a public channel, so you can join it as you want.",
|
||||||
|
"chatJoin": "Join the Channel",
|
||||||
|
"appInitStarting": "Starting",
|
||||||
|
"appInitNetwork": "Initializing Network",
|
||||||
|
"appInitUserdata": "Initializing User Data",
|
||||||
|
"appInitWebsocket": "Establishing Solar Link",
|
||||||
|
"appInitNotification": "Initializing Push Notifications",
|
||||||
|
"appInitKeyPair": "Initializing Key Pairs",
|
||||||
|
"appInitStickers": "Initializing Stickers",
|
||||||
|
"appInitUserDirectory": "Initializing User Directory",
|
||||||
|
"appInitRealm": "Initializing Realms",
|
||||||
|
"appInitChat": "Initializing Chat",
|
||||||
|
"appInitDone": "Completed"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,6 +336,7 @@
|
|||||||
"fieldAttachmentRandomId": "访问 ID",
|
"fieldAttachmentRandomId": "访问 ID",
|
||||||
"fieldAttachmentAlt": "概述文字",
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||||
|
"addAttachmentFromFiles": "从文件中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘贴附件",
|
"addAttachmentFromClipboard": "粘贴附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||||
@@ -844,5 +845,39 @@
|
|||||||
"translating": "正在翻译……",
|
"translating": "正在翻译……",
|
||||||
"translated": "已翻译",
|
"translated": "已翻译",
|
||||||
"settingsAutoTranslate": "自动翻译",
|
"settingsAutoTranslate": "自动翻译",
|
||||||
"settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。"
|
"settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。",
|
||||||
|
"trayMenuHide": "隐藏",
|
||||||
|
"accountSettingsNotify": "通知设置",
|
||||||
|
"accountSettingsNotifyDescription": "调整你所收到的通知种类。",
|
||||||
|
"accountSettingsSecurity": "安全设置",
|
||||||
|
"accountSettingsSecurityDescription": "调整你的帐户安全设置。",
|
||||||
|
"save": "保存",
|
||||||
|
"notificationTopicPostFeedback": "帖子数据反馈",
|
||||||
|
"notificationTopicPostReply": "帖子回复",
|
||||||
|
"notificationTopicPostSubscription": "帖子订阅",
|
||||||
|
"notificationTopicMessaging": "消息",
|
||||||
|
"notificationTopicMessagingCall": "通话",
|
||||||
|
"notificationTopicGeneral": "杂项",
|
||||||
|
"authMaximumAuthSteps": "最大验证步骤",
|
||||||
|
"authMaximumAuthStepsDescription": {
|
||||||
|
"one": "登入时最多要求 {} 步验证",
|
||||||
|
"other": "登入时最多要求 {} 步验证"
|
||||||
|
},
|
||||||
|
"authAlwaysRisky": "总是风险",
|
||||||
|
"authAlwaysRiskyDescription": "在登入时始终按最高标准要求验证。",
|
||||||
|
"chatUnjoined": "未加入频道",
|
||||||
|
"chatUnjoinedDescription": "你没有加入这个频道,所以你也无法发送消息或者查看这个频道中的消息。",
|
||||||
|
"chatUnjoinedPublicDescription": "但幸运的是,这是一个公开频道,所以你可以主动加入。",
|
||||||
|
"chatJoin": "加入频道",
|
||||||
|
"appInitStarting": "启动中",
|
||||||
|
"appInitNetwork": "正在初始化网络",
|
||||||
|
"appInitUserdata": "正在初始化用户数据",
|
||||||
|
"appInitWebsocket": "正在建立 Solar Link",
|
||||||
|
"appInitNotification": "正在初始化推送通知",
|
||||||
|
"appInitKeyPair": "正在初始化密钥对",
|
||||||
|
"appInitStickers": "正在初始化贴图包",
|
||||||
|
"appInitUserDirectory": "正在初始化用户目录",
|
||||||
|
"appInitRealm": "正在初始化领域信息",
|
||||||
|
"appInitChat": "正在初始化聊天",
|
||||||
|
"appInitDone": "完成"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,6 +336,7 @@
|
|||||||
"fieldAttachmentRandomId": "訪問 ID",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
"fieldAttachmentAlt": "概述文字",
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
|
"addAttachmentFromFiles": "從文件中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
@@ -844,5 +845,39 @@
|
|||||||
"translating": "正在翻譯……",
|
"translating": "正在翻譯……",
|
||||||
"translated": "已翻譯",
|
"translated": "已翻譯",
|
||||||
"settingsAutoTranslate": "自動翻譯",
|
"settingsAutoTranslate": "自動翻譯",
|
||||||
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
|
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
|
||||||
|
"trayMenuHide": "隱藏",
|
||||||
|
"accountSettingsNotify": "通知設置",
|
||||||
|
"accountSettingsNotifyDescription": "調整你所收到的通知種類。",
|
||||||
|
"accountSettingsSecurity": "安全設置",
|
||||||
|
"accountSettingsSecurityDescription": "調整你的帳户安全設置。",
|
||||||
|
"save": "保存",
|
||||||
|
"notificationTopicPostFeedback": "帖子數據反饋",
|
||||||
|
"notificationTopicPostReply": "帖子回覆",
|
||||||
|
"notificationTopicPostSubscription": "帖子訂閲",
|
||||||
|
"notificationTopicMessaging": "消息",
|
||||||
|
"notificationTopicMessagingCall": "通話",
|
||||||
|
"notificationTopicGeneral": "雜項",
|
||||||
|
"authMaximumAuthSteps": "最大驗證步驟",
|
||||||
|
"authMaximumAuthStepsDescription": {
|
||||||
|
"one": "登入時最多要求 {} 步驗證",
|
||||||
|
"other": "登入時最多要求 {} 步驗證"
|
||||||
|
},
|
||||||
|
"authAlwaysRisky": "總是風險",
|
||||||
|
"authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
|
||||||
|
"chatUnjoined": "未加入頻道",
|
||||||
|
"chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
|
||||||
|
"chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
|
||||||
|
"chatJoin": "加入頻道",
|
||||||
|
"appInitStarting": "啓動中",
|
||||||
|
"appInitNetwork": "正在初始化網絡",
|
||||||
|
"appInitUserdata": "正在初始化用户數據",
|
||||||
|
"appInitWebsocket": "正在建立 Solar Link",
|
||||||
|
"appInitNotification": "正在初始化推送通知",
|
||||||
|
"appInitKeyPair": "正在初始化密鑰對",
|
||||||
|
"appInitStickers": "正在初始化貼圖包",
|
||||||
|
"appInitUserDirectory": "正在初始化用户目錄",
|
||||||
|
"appInitRealm": "正在初始化領域信息",
|
||||||
|
"appInitChat": "正在初始化聊天",
|
||||||
|
"appInitDone": "完成"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,6 +336,7 @@
|
|||||||
"fieldAttachmentRandomId": "訪問 ID",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
"fieldAttachmentAlt": "概述文字",
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
|
"addAttachmentFromFiles": "從文件中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
@@ -844,5 +845,39 @@
|
|||||||
"translating": "正在翻譯……",
|
"translating": "正在翻譯……",
|
||||||
"translated": "已翻譯",
|
"translated": "已翻譯",
|
||||||
"settingsAutoTranslate": "自動翻譯",
|
"settingsAutoTranslate": "自動翻譯",
|
||||||
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
|
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
|
||||||
|
"trayMenuHide": "隱藏",
|
||||||
|
"accountSettingsNotify": "通知設置",
|
||||||
|
"accountSettingsNotifyDescription": "調整你所收到的通知種類。",
|
||||||
|
"accountSettingsSecurity": "安全設置",
|
||||||
|
"accountSettingsSecurityDescription": "調整你的帳戶安全設置。",
|
||||||
|
"save": "保存",
|
||||||
|
"notificationTopicPostFeedback": "帖子數據反饋",
|
||||||
|
"notificationTopicPostReply": "帖子回覆",
|
||||||
|
"notificationTopicPostSubscription": "帖子訂閱",
|
||||||
|
"notificationTopicMessaging": "消息",
|
||||||
|
"notificationTopicMessagingCall": "通話",
|
||||||
|
"notificationTopicGeneral": "雜項",
|
||||||
|
"authMaximumAuthSteps": "最大驗證步驟",
|
||||||
|
"authMaximumAuthStepsDescription": {
|
||||||
|
"one": "登入時最多要求 {} 步驗證",
|
||||||
|
"other": "登入時最多要求 {} 步驗證"
|
||||||
|
},
|
||||||
|
"authAlwaysRisky": "總是風險",
|
||||||
|
"authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
|
||||||
|
"chatUnjoined": "未加入頻道",
|
||||||
|
"chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
|
||||||
|
"chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
|
||||||
|
"chatJoin": "加入頻道",
|
||||||
|
"appInitStarting": "啟動中",
|
||||||
|
"appInitNetwork": "正在初始化網絡",
|
||||||
|
"appInitUserdata": "正在初始化用戶數據",
|
||||||
|
"appInitWebsocket": "正在建立 Solar Link",
|
||||||
|
"appInitNotification": "正在初始化推送通知",
|
||||||
|
"appInitKeyPair": "正在初始化密鑰對",
|
||||||
|
"appInitStickers": "正在初始化貼圖包",
|
||||||
|
"appInitUserDirectory": "正在初始化用戶目錄",
|
||||||
|
"appInitRealm": "正在初始化領域信息",
|
||||||
|
"appInitChat": "正在初始化聊天",
|
||||||
|
"appInitDone": "完成"
|
||||||
}
|
}
|
||||||
|
|||||||
1
drift_schemas/my_database/drift_schema_v4.json
Normal file
1
drift_schemas/my_database/drift_schema_v4.json
Normal file
File diff suppressed because one or more lines are too long
@@ -79,6 +79,8 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import 'package:surface/database/attachment.dart';
|
|||||||
import 'package:surface/database/chat.dart';
|
import 'package:surface/database/chat.dart';
|
||||||
import 'package:surface/database/database.steps.dart';
|
import 'package:surface/database/database.steps.dart';
|
||||||
import 'package:surface/database/keypair.dart';
|
import 'package:surface/database/keypair.dart';
|
||||||
|
import 'package:surface/database/realm.dart';
|
||||||
import 'package:surface/database/sticker.dart';
|
import 'package:surface/database/sticker.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
@@ -22,12 +24,13 @@ part 'database.g.dart';
|
|||||||
SnLocalAttachment,
|
SnLocalAttachment,
|
||||||
SnLocalSticker,
|
SnLocalSticker,
|
||||||
SnLocalStickerPack,
|
SnLocalStickerPack,
|
||||||
|
SnLocalRealm,
|
||||||
])
|
])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 3;
|
int get schemaVersion => 4;
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(
|
return driftDatabase(
|
||||||
@@ -49,6 +52,10 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
// Nothing else to do here
|
// Nothing else to do here
|
||||||
}, from2To3: (m, schema) async {
|
}, from2To3: (m, schema) async {
|
||||||
// Nothing else to do here, too
|
// Nothing else to do here, too
|
||||||
|
}, from3To4: (m, schema) async {
|
||||||
|
m.createTable(schema.snLocalRealm);
|
||||||
|
m.createIndex(schema.idxRealmAccount);
|
||||||
|
m.createIndex(schema.idxRealmAlias);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2454,6 +2454,351 @@ class SnLocalStickerPackCompanion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class $SnLocalRealmTable extends SnLocalRealm
|
||||||
|
with TableInfo<$SnLocalRealmTable, SnLocalRealmData> {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$SnLocalRealmTable(this.attachedDatabase, [this._alias]);
|
||||||
|
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||||
|
'id', aliasedName, false,
|
||||||
|
hasAutoIncrement: true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints:
|
||||||
|
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||||
|
static const VerificationMeta _aliasMeta = const VerificationMeta('alias');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> alias = GeneratedColumn<String>(
|
||||||
|
'alias', aliasedName, false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||||
|
@override
|
||||||
|
late final GeneratedColumnWithTypeConverter<SnRealm, String> content =
|
||||||
|
GeneratedColumn<String>('content', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true)
|
||||||
|
.withConverter<SnRealm>($SnLocalRealmTable.$convertercontent);
|
||||||
|
static const VerificationMeta _accountIdMeta =
|
||||||
|
const VerificationMeta('accountId');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> accountId = GeneratedColumn<int>(
|
||||||
|
'account_id', aliasedName, false,
|
||||||
|
type: DriftSqlType.int, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _createdAtMeta =
|
||||||
|
const VerificationMeta('createdAt');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||||
|
'created_at', aliasedName, false,
|
||||||
|
type: DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: currentDateAndTime);
|
||||||
|
static const VerificationMeta _cacheExpiredAtMeta =
|
||||||
|
const VerificationMeta('cacheExpiredAt');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> cacheExpiredAt =
|
||||||
|
GeneratedColumn<DateTime>('cache_expired_at', aliasedName, false,
|
||||||
|
type: DriftSqlType.dateTime, requiredDuringInsert: true);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns =>
|
||||||
|
[id, alias, content, accountId, createdAt, cacheExpiredAt];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'sn_local_realm';
|
||||||
|
@override
|
||||||
|
VerificationContext validateIntegrity(Insertable<SnLocalRealmData> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('id')) {
|
||||||
|
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('alias')) {
|
||||||
|
context.handle(
|
||||||
|
_aliasMeta, alias.isAcceptableOrUnknown(data['alias']!, _aliasMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_aliasMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('account_id')) {
|
||||||
|
context.handle(_accountIdMeta,
|
||||||
|
accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_accountIdMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('created_at')) {
|
||||||
|
context.handle(_createdAtMeta,
|
||||||
|
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('cache_expired_at')) {
|
||||||
|
context.handle(
|
||||||
|
_cacheExpiredAtMeta,
|
||||||
|
cacheExpiredAt.isAcceptableOrUnknown(
|
||||||
|
data['cache_expired_at']!, _cacheExpiredAtMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_cacheExpiredAtMeta);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
SnLocalRealmData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return SnLocalRealmData(
|
||||||
|
id: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||||
|
alias: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}alias'])!,
|
||||||
|
content: $SnLocalRealmTable.$convertercontent.fromSql(attachedDatabase
|
||||||
|
.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}content'])!),
|
||||||
|
accountId: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.int, data['${effectivePrefix}account_id'])!,
|
||||||
|
createdAt: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||||
|
cacheExpiredAt: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.dateTime, data['${effectivePrefix}cache_expired_at'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$SnLocalRealmTable createAlias(String alias) {
|
||||||
|
return $SnLocalRealmTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
static JsonTypeConverter2<SnRealm, String, Map<String, Object?>>
|
||||||
|
$convertercontent = const SnRealmConverter();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnLocalRealmData extends DataClass
|
||||||
|
implements Insertable<SnLocalRealmData> {
|
||||||
|
final int id;
|
||||||
|
final String alias;
|
||||||
|
final SnRealm content;
|
||||||
|
final int accountId;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime cacheExpiredAt;
|
||||||
|
const SnLocalRealmData(
|
||||||
|
{required this.id,
|
||||||
|
required this.alias,
|
||||||
|
required this.content,
|
||||||
|
required this.accountId,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.cacheExpiredAt});
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
map['id'] = Variable<int>(id);
|
||||||
|
map['alias'] = Variable<String>(alias);
|
||||||
|
{
|
||||||
|
map['content'] =
|
||||||
|
Variable<String>($SnLocalRealmTable.$convertercontent.toSql(content));
|
||||||
|
}
|
||||||
|
map['account_id'] = Variable<int>(accountId);
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt);
|
||||||
|
map['cache_expired_at'] = Variable<DateTime>(cacheExpiredAt);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
SnLocalRealmCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return SnLocalRealmCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
alias: Value(alias),
|
||||||
|
content: Value(content),
|
||||||
|
accountId: Value(accountId),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
cacheExpiredAt: Value(cacheExpiredAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory SnLocalRealmData.fromJson(Map<String, dynamic> json,
|
||||||
|
{ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return SnLocalRealmData(
|
||||||
|
id: serializer.fromJson<int>(json['id']),
|
||||||
|
alias: serializer.fromJson<String>(json['alias']),
|
||||||
|
content: $SnLocalRealmTable.$convertercontent
|
||||||
|
.fromJson(serializer.fromJson<Map<String, Object?>>(json['content'])),
|
||||||
|
accountId: serializer.fromJson<int>(json['accountId']),
|
||||||
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
cacheExpiredAt: serializer.fromJson<DateTime>(json['cacheExpiredAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': serializer.toJson<int>(id),
|
||||||
|
'alias': serializer.toJson<String>(alias),
|
||||||
|
'content': serializer.toJson<Map<String, Object?>>(
|
||||||
|
$SnLocalRealmTable.$convertercontent.toJson(content)),
|
||||||
|
'accountId': serializer.toJson<int>(accountId),
|
||||||
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
|
'cacheExpiredAt': serializer.toJson<DateTime>(cacheExpiredAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
SnLocalRealmData copyWith(
|
||||||
|
{int? id,
|
||||||
|
String? alias,
|
||||||
|
SnRealm? content,
|
||||||
|
int? accountId,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? cacheExpiredAt}) =>
|
||||||
|
SnLocalRealmData(
|
||||||
|
id: id ?? this.id,
|
||||||
|
alias: alias ?? this.alias,
|
||||||
|
content: content ?? this.content,
|
||||||
|
accountId: accountId ?? this.accountId,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
cacheExpiredAt: cacheExpiredAt ?? this.cacheExpiredAt,
|
||||||
|
);
|
||||||
|
SnLocalRealmData copyWithCompanion(SnLocalRealmCompanion data) {
|
||||||
|
return SnLocalRealmData(
|
||||||
|
id: data.id.present ? data.id.value : this.id,
|
||||||
|
alias: data.alias.present ? data.alias.value : this.alias,
|
||||||
|
content: data.content.present ? data.content.value : this.content,
|
||||||
|
accountId: data.accountId.present ? data.accountId.value : this.accountId,
|
||||||
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
|
cacheExpiredAt: data.cacheExpiredAt.present
|
||||||
|
? data.cacheExpiredAt.value
|
||||||
|
: this.cacheExpiredAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('SnLocalRealmData(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('alias: $alias, ')
|
||||||
|
..write('content: $content, ')
|
||||||
|
..write('accountId: $accountId, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('cacheExpiredAt: $cacheExpiredAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(id, alias, content, accountId, createdAt, cacheExpiredAt);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is SnLocalRealmData &&
|
||||||
|
other.id == this.id &&
|
||||||
|
other.alias == this.alias &&
|
||||||
|
other.content == this.content &&
|
||||||
|
other.accountId == this.accountId &&
|
||||||
|
other.createdAt == this.createdAt &&
|
||||||
|
other.cacheExpiredAt == this.cacheExpiredAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnLocalRealmCompanion extends UpdateCompanion<SnLocalRealmData> {
|
||||||
|
final Value<int> id;
|
||||||
|
final Value<String> alias;
|
||||||
|
final Value<SnRealm> content;
|
||||||
|
final Value<int> accountId;
|
||||||
|
final Value<DateTime> createdAt;
|
||||||
|
final Value<DateTime> cacheExpiredAt;
|
||||||
|
const SnLocalRealmCompanion({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
this.alias = const Value.absent(),
|
||||||
|
this.content = const Value.absent(),
|
||||||
|
this.accountId = const Value.absent(),
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
this.cacheExpiredAt = const Value.absent(),
|
||||||
|
});
|
||||||
|
SnLocalRealmCompanion.insert({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
required String alias,
|
||||||
|
required SnRealm content,
|
||||||
|
required int accountId,
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
required DateTime cacheExpiredAt,
|
||||||
|
}) : alias = Value(alias),
|
||||||
|
content = Value(content),
|
||||||
|
accountId = Value(accountId),
|
||||||
|
cacheExpiredAt = Value(cacheExpiredAt);
|
||||||
|
static Insertable<SnLocalRealmData> custom({
|
||||||
|
Expression<int>? id,
|
||||||
|
Expression<String>? alias,
|
||||||
|
Expression<String>? content,
|
||||||
|
Expression<int>? accountId,
|
||||||
|
Expression<DateTime>? createdAt,
|
||||||
|
Expression<DateTime>? cacheExpiredAt,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
if (alias != null) 'alias': alias,
|
||||||
|
if (content != null) 'content': content,
|
||||||
|
if (accountId != null) 'account_id': accountId,
|
||||||
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
if (cacheExpiredAt != null) 'cache_expired_at': cacheExpiredAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SnLocalRealmCompanion copyWith(
|
||||||
|
{Value<int>? id,
|
||||||
|
Value<String>? alias,
|
||||||
|
Value<SnRealm>? content,
|
||||||
|
Value<int>? accountId,
|
||||||
|
Value<DateTime>? createdAt,
|
||||||
|
Value<DateTime>? cacheExpiredAt}) {
|
||||||
|
return SnLocalRealmCompanion(
|
||||||
|
id: id ?? this.id,
|
||||||
|
alias: alias ?? this.alias,
|
||||||
|
content: content ?? this.content,
|
||||||
|
accountId: accountId ?? this.accountId,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
cacheExpiredAt: cacheExpiredAt ?? this.cacheExpiredAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (id.present) {
|
||||||
|
map['id'] = Variable<int>(id.value);
|
||||||
|
}
|
||||||
|
if (alias.present) {
|
||||||
|
map['alias'] = Variable<String>(alias.value);
|
||||||
|
}
|
||||||
|
if (content.present) {
|
||||||
|
map['content'] = Variable<String>(
|
||||||
|
$SnLocalRealmTable.$convertercontent.toSql(content.value));
|
||||||
|
}
|
||||||
|
if (accountId.present) {
|
||||||
|
map['account_id'] = Variable<int>(accountId.value);
|
||||||
|
}
|
||||||
|
if (createdAt.present) {
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||||
|
}
|
||||||
|
if (cacheExpiredAt.present) {
|
||||||
|
map['cache_expired_at'] = Variable<DateTime>(cacheExpiredAt.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('SnLocalRealmCompanion(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('alias: $alias, ')
|
||||||
|
..write('content: $content, ')
|
||||||
|
..write('accountId: $accountId, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('cacheExpiredAt: $cacheExpiredAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract class _$AppDatabase extends GeneratedDatabase {
|
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||||
_$AppDatabase(QueryExecutor e) : super(e);
|
_$AppDatabase(QueryExecutor e) : super(e);
|
||||||
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||||
@@ -2470,6 +2815,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
late final $SnLocalStickerTable snLocalSticker = $SnLocalStickerTable(this);
|
late final $SnLocalStickerTable snLocalSticker = $SnLocalStickerTable(this);
|
||||||
late final $SnLocalStickerPackTable snLocalStickerPack =
|
late final $SnLocalStickerPackTable snLocalStickerPack =
|
||||||
$SnLocalStickerPackTable(this);
|
$SnLocalStickerPackTable(this);
|
||||||
|
late final $SnLocalRealmTable snLocalRealm = $SnLocalRealmTable(this);
|
||||||
late final Index idxChannelAlias = Index('idx_channel_alias',
|
late final Index idxChannelAlias = Index('idx_channel_alias',
|
||||||
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
||||||
late final Index idxChatChannel = Index('idx_chat_channel',
|
late final Index idxChatChannel = Index('idx_chat_channel',
|
||||||
@@ -2480,6 +2826,10 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
|
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
|
||||||
late final Index idxAttachmentAccount = Index('idx_attachment_account',
|
late final Index idxAttachmentAccount = Index('idx_attachment_account',
|
||||||
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
|
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
|
||||||
|
late final Index idxRealmAlias = Index('idx_realm_alias',
|
||||||
|
'CREATE INDEX idx_realm_alias ON sn_local_realm (alias)');
|
||||||
|
late final Index idxRealmAccount = Index('idx_realm_account',
|
||||||
|
'CREATE INDEX idx_realm_account ON sn_local_realm (account_id)');
|
||||||
@override
|
@override
|
||||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
@@ -2493,11 +2843,14 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
snLocalAttachment,
|
snLocalAttachment,
|
||||||
snLocalSticker,
|
snLocalSticker,
|
||||||
snLocalStickerPack,
|
snLocalStickerPack,
|
||||||
|
snLocalRealm,
|
||||||
idxChannelAlias,
|
idxChannelAlias,
|
||||||
idxChatChannel,
|
idxChatChannel,
|
||||||
idxAccountName,
|
idxAccountName,
|
||||||
idxAttachmentRid,
|
idxAttachmentRid,
|
||||||
idxAttachmentAccount
|
idxAttachmentAccount,
|
||||||
|
idxRealmAlias,
|
||||||
|
idxRealmAccount
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3888,6 +4241,192 @@ typedef $$SnLocalStickerPackTableProcessedTableManager = ProcessedTableManager<
|
|||||||
),
|
),
|
||||||
SnLocalStickerPackData,
|
SnLocalStickerPackData,
|
||||||
PrefetchHooks Function()>;
|
PrefetchHooks Function()>;
|
||||||
|
typedef $$SnLocalRealmTableCreateCompanionBuilder = SnLocalRealmCompanion
|
||||||
|
Function({
|
||||||
|
Value<int> id,
|
||||||
|
required String alias,
|
||||||
|
required SnRealm content,
|
||||||
|
required int accountId,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
required DateTime cacheExpiredAt,
|
||||||
|
});
|
||||||
|
typedef $$SnLocalRealmTableUpdateCompanionBuilder = SnLocalRealmCompanion
|
||||||
|
Function({
|
||||||
|
Value<int> id,
|
||||||
|
Value<String> alias,
|
||||||
|
Value<SnRealm> content,
|
||||||
|
Value<int> accountId,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
Value<DateTime> cacheExpiredAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $$SnLocalRealmTableFilterComposer
|
||||||
|
extends Composer<_$AppDatabase, $SnLocalRealmTable> {
|
||||||
|
$$SnLocalRealmTableFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnFilters<int> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get alias => $composableBuilder(
|
||||||
|
column: $table.alias, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnWithTypeConverterFilters<SnRealm, SnRealm, String> get content =>
|
||||||
|
$composableBuilder(
|
||||||
|
column: $table.content,
|
||||||
|
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<int> get accountId => $composableBuilder(
|
||||||
|
column: $table.accountId, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get cacheExpiredAt => $composableBuilder(
|
||||||
|
column: $table.cacheExpiredAt,
|
||||||
|
builder: (column) => ColumnFilters(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$SnLocalRealmTableOrderingComposer
|
||||||
|
extends Composer<_$AppDatabase, $SnLocalRealmTable> {
|
||||||
|
$$SnLocalRealmTableOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnOrderings<int> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get alias => $composableBuilder(
|
||||||
|
column: $table.alias, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get content => $composableBuilder(
|
||||||
|
column: $table.content, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<int> get accountId => $composableBuilder(
|
||||||
|
column: $table.accountId, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get cacheExpiredAt => $composableBuilder(
|
||||||
|
column: $table.cacheExpiredAt,
|
||||||
|
builder: (column) => ColumnOrderings(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$SnLocalRealmTableAnnotationComposer
|
||||||
|
extends Composer<_$AppDatabase, $SnLocalRealmTable> {
|
||||||
|
$$SnLocalRealmTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
GeneratedColumn<int> get id =>
|
||||||
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get alias =>
|
||||||
|
$composableBuilder(column: $table.alias, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumnWithTypeConverter<SnRealm, String> get content =>
|
||||||
|
$composableBuilder(column: $table.content, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<int> get accountId =>
|
||||||
|
$composableBuilder(column: $table.accountId, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get cacheExpiredAt => $composableBuilder(
|
||||||
|
column: $table.cacheExpiredAt, builder: (column) => column);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$SnLocalRealmTableTableManager extends RootTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$SnLocalRealmTable,
|
||||||
|
SnLocalRealmData,
|
||||||
|
$$SnLocalRealmTableFilterComposer,
|
||||||
|
$$SnLocalRealmTableOrderingComposer,
|
||||||
|
$$SnLocalRealmTableAnnotationComposer,
|
||||||
|
$$SnLocalRealmTableCreateCompanionBuilder,
|
||||||
|
$$SnLocalRealmTableUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
SnLocalRealmData,
|
||||||
|
BaseReferences<_$AppDatabase, $SnLocalRealmTable, SnLocalRealmData>
|
||||||
|
),
|
||||||
|
SnLocalRealmData,
|
||||||
|
PrefetchHooks Function()> {
|
||||||
|
$$SnLocalRealmTableTableManager(_$AppDatabase db, $SnLocalRealmTable table)
|
||||||
|
: super(TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
$$SnLocalRealmTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
$$SnLocalRealmTableOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
$$SnLocalRealmTableAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
Value<String> alias = const Value.absent(),
|
||||||
|
Value<SnRealm> content = const Value.absent(),
|
||||||
|
Value<int> accountId = const Value.absent(),
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
Value<DateTime> cacheExpiredAt = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
SnLocalRealmCompanion(
|
||||||
|
id: id,
|
||||||
|
alias: alias,
|
||||||
|
content: content,
|
||||||
|
accountId: accountId,
|
||||||
|
createdAt: createdAt,
|
||||||
|
cacheExpiredAt: cacheExpiredAt,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
required String alias,
|
||||||
|
required SnRealm content,
|
||||||
|
required int accountId,
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
required DateTime cacheExpiredAt,
|
||||||
|
}) =>
|
||||||
|
SnLocalRealmCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
alias: alias,
|
||||||
|
content: content,
|
||||||
|
accountId: accountId,
|
||||||
|
createdAt: createdAt,
|
||||||
|
cacheExpiredAt: cacheExpiredAt,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$SnLocalRealmTableProcessedTableManager = ProcessedTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$SnLocalRealmTable,
|
||||||
|
SnLocalRealmData,
|
||||||
|
$$SnLocalRealmTableFilterComposer,
|
||||||
|
$$SnLocalRealmTableOrderingComposer,
|
||||||
|
$$SnLocalRealmTableAnnotationComposer,
|
||||||
|
$$SnLocalRealmTableCreateCompanionBuilder,
|
||||||
|
$$SnLocalRealmTableUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
SnLocalRealmData,
|
||||||
|
BaseReferences<_$AppDatabase, $SnLocalRealmTable, SnLocalRealmData>
|
||||||
|
),
|
||||||
|
SnLocalRealmData,
|
||||||
|
PrefetchHooks Function()>;
|
||||||
|
|
||||||
class $AppDatabaseManager {
|
class $AppDatabaseManager {
|
||||||
final _$AppDatabase _db;
|
final _$AppDatabase _db;
|
||||||
@@ -3908,4 +4447,6 @@ class $AppDatabaseManager {
|
|||||||
$$SnLocalStickerTableTableManager(_db, _db.snLocalSticker);
|
$$SnLocalStickerTableTableManager(_db, _db.snLocalSticker);
|
||||||
$$SnLocalStickerPackTableTableManager get snLocalStickerPack =>
|
$$SnLocalStickerPackTableTableManager get snLocalStickerPack =>
|
||||||
$$SnLocalStickerPackTableTableManager(_db, _db.snLocalStickerPack);
|
$$SnLocalStickerPackTableTableManager(_db, _db.snLocalStickerPack);
|
||||||
|
$$SnLocalRealmTableTableManager get snLocalRealm =>
|
||||||
|
$$SnLocalRealmTableTableManager(_db, _db.snLocalRealm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -412,9 +412,214 @@ class Shape8 extends i0.VersionedTable {
|
|||||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class Schema4 extends i0.VersionedSchema {
|
||||||
|
Schema4({required super.database}) : super(version: 4);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
snLocalChatChannel,
|
||||||
|
snLocalChatMessage,
|
||||||
|
snLocalChannelMember,
|
||||||
|
snLocalKeyPair,
|
||||||
|
snLocalAccount,
|
||||||
|
snLocalAttachment,
|
||||||
|
snLocalSticker,
|
||||||
|
snLocalStickerPack,
|
||||||
|
snLocalRealm,
|
||||||
|
idxChannelAlias,
|
||||||
|
idxChatChannel,
|
||||||
|
idxAccountName,
|
||||||
|
idxAttachmentRid,
|
||||||
|
idxAttachmentAccount,
|
||||||
|
idxRealmAlias,
|
||||||
|
idxRealmAccount,
|
||||||
|
];
|
||||||
|
late final Shape0 snLocalChatChannel = Shape0(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_chat_channel',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape3 snLocalChatMessage = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_chat_message',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_4,
|
||||||
|
_column_10,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape4 snLocalChannelMember = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_channel_member',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_4,
|
||||||
|
_column_6,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape2 snLocalKeyPair = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_key_pair',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_5,
|
||||||
|
_column_6,
|
||||||
|
_column_7,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape5 snLocalAccount = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_account',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_12,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape6 snLocalAttachment = Shape6(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_attachment',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_2,
|
||||||
|
_column_6,
|
||||||
|
_column_3,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape7 snLocalSticker = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_sticker',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_15,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape8 snLocalStickerPack = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_sticker_pack',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape9 snLocalRealm = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_realm',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_16,
|
||||||
|
_column_2,
|
||||||
|
_column_6,
|
||||||
|
_column_3,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
final i1.Index idxChannelAlias = i1.Index('idx_channel_alias',
|
||||||
|
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
||||||
|
final i1.Index idxChatChannel = i1.Index('idx_chat_channel',
|
||||||
|
'CREATE INDEX idx_chat_channel ON sn_local_chat_message (channel_id)');
|
||||||
|
final i1.Index idxAccountName = i1.Index('idx_account_name',
|
||||||
|
'CREATE INDEX idx_account_name ON sn_local_account (name)');
|
||||||
|
final i1.Index idxAttachmentRid = i1.Index('idx_attachment_rid',
|
||||||
|
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
|
||||||
|
final i1.Index idxAttachmentAccount = i1.Index('idx_attachment_account',
|
||||||
|
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
|
||||||
|
final i1.Index idxRealmAlias = i1.Index('idx_realm_alias',
|
||||||
|
'CREATE INDEX idx_realm_alias ON sn_local_realm (alias)');
|
||||||
|
final i1.Index idxRealmAccount = i1.Index('idx_realm_account',
|
||||||
|
'CREATE INDEX idx_realm_account ON sn_local_realm (account_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape9 extends i0.VersionedTable {
|
||||||
|
Shape9({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get alias =>
|
||||||
|
columnsByName['alias']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get accountId =>
|
||||||
|
columnsByName['account_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
|
||||||
|
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_16(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('alias', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -428,6 +633,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from2To3(migrator, schema);
|
await from2To3(migrator, schema);
|
||||||
return 3;
|
return 3;
|
||||||
|
case 3:
|
||||||
|
final schema = Schema4(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from3To4(migrator, schema);
|
||||||
|
return 4;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -437,9 +647,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
i1.OnUpgrade stepByStep({
|
i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||||
}) =>
|
}) =>
|
||||||
i0.VersionedSchema.stepByStepHelper(
|
i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
from2To3: from2To3,
|
from2To3: from2To3,
|
||||||
|
from3To4: from3To4,
|
||||||
));
|
));
|
||||||
|
|||||||
45
lib/database/realm.dart
Normal file
45
lib/database/realm.dart
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
|
|
||||||
|
class SnRealmConverter extends TypeConverter<SnRealm, String>
|
||||||
|
with JsonTypeConverter2<SnRealm, String, Map<String, Object?>> {
|
||||||
|
const SnRealmConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnRealm fromSql(String fromDb) {
|
||||||
|
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(SnRealm value) {
|
||||||
|
return jsonEncode(toJson(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnRealm fromJson(Map<String, Object?> json) {
|
||||||
|
return SnRealm.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Object?> toJson(SnRealm value) {
|
||||||
|
return value.toJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TableIndex(name: 'idx_realm_alias', columns: {#alias})
|
||||||
|
@TableIndex(name: 'idx_realm_account', columns: {#accountId})
|
||||||
|
class SnLocalRealm extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
|
||||||
|
TextColumn get alias => text().unique()();
|
||||||
|
|
||||||
|
TextColumn get content => text().map(const SnRealmConverter())();
|
||||||
|
|
||||||
|
IntColumn get accountId => integer()();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
DateTimeColumn get cacheExpiredAt => dateTime()();
|
||||||
|
}
|
||||||
114
lib/main.dart
114
lib/main.dart
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math' hide log;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
@@ -12,6 +13,7 @@ import 'package:firebase_core/firebase_core.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
@@ -19,6 +21,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/firebase_options.dart';
|
import 'package:surface/firebase_options.dart';
|
||||||
import 'package:surface/logger.dart';
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
@@ -46,6 +49,7 @@ import 'package:surface/router.dart';
|
|||||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/menu_bar.dart';
|
import 'package:surface/widgets/menu_bar.dart';
|
||||||
|
import 'package:surface/widgets/version_label.dart';
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
@@ -228,6 +232,9 @@ class _AppSplashScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||||
|
bool _isBusy = false;
|
||||||
|
String _phaseText = 'appInitStarting';
|
||||||
|
|
||||||
void _tryRequestRating() async {
|
void _tryRequestRating() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (prefs.containsKey('first_boot_time')) {
|
if (prefs.containsKey('first_boot_time')) {
|
||||||
@@ -287,6 +294,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setPhaseText(String text) {
|
||||||
|
_phaseText = 'appInit${text.capitalize()}'.tr();
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _initialize() async {
|
Future<void> _initialize() async {
|
||||||
try {
|
try {
|
||||||
final cfg = context.read<ConfigProvider>();
|
final cfg = context.read<ConfigProvider>();
|
||||||
@@ -299,31 +311,45 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
// The Network initialization must be done after the HomeWidget initialization
|
// The Network initialization must be done after the HomeWidget initialization
|
||||||
// The Network initialization will save the server url to the HomeWidget
|
// The Network initialization will save the server url to the HomeWidget
|
||||||
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
||||||
|
_setPhaseText('network');
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.initializeUserAgent();
|
await sn.initializeUserAgent();
|
||||||
await sn.setConfigWithNative();
|
await sn.setConfigWithNative();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('userdata');
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
await ua.initialize();
|
await ua.initialize();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('websocket');
|
||||||
final ws = context.read<WebSocketProvider>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
await ws.tryConnect();
|
await ws.tryConnect();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('notification');
|
||||||
final notify = context.read<NotificationProvider>();
|
final notify = context.read<NotificationProvider>();
|
||||||
notify.listen();
|
notify.listen();
|
||||||
await notify.registerPushNotifications();
|
await notify.registerPushNotifications();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('keyPair');
|
||||||
final kp = context.read<KeyPairProvider>();
|
final kp = context.read<KeyPairProvider>();
|
||||||
await kp.reloadActive();
|
await kp.reloadActive();
|
||||||
kp.listen();
|
kp.listen();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('stickers');
|
||||||
final sticker = context.read<SnStickerProvider>();
|
final sticker = context.read<SnStickerProvider>();
|
||||||
await sticker.listSticker();
|
await sticker.listSticker();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('userDirectory');
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final userCacheSize = await ud.loadAccountCache();
|
await ud.loadAccountCache();
|
||||||
logging.info('[Users] Loaded local user cache, size: $userCacheSize');
|
if (!mounted) return;
|
||||||
logging.info('[Bootstrap] Everything initialized!');
|
_setPhaseText('realm');
|
||||||
|
final rm = context.read<SnRealmProvider>();
|
||||||
|
await rm.refreshAvailableRealms();
|
||||||
|
if (!mounted) return;
|
||||||
|
_setPhaseText('chat');
|
||||||
|
final ct = context.read<ChatChannelProvider>();
|
||||||
|
await ct.refreshAvailableChannels();
|
||||||
|
_setPhaseText('done');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await context.showErrorDialog(err);
|
await context.showErrorDialog(err);
|
||||||
@@ -399,6 +425,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_isBusy = true;
|
||||||
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
||||||
_appLifecycleListener = AppLifecycleListener(
|
_appLifecycleListener = AppLifecycleListener(
|
||||||
onExitRequested: _onExitRequested,
|
onExitRequested: _onExitRequested,
|
||||||
@@ -412,6 +439,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
_postInitialization();
|
_postInitialization();
|
||||||
_tryRequestRating();
|
_tryRequestRating();
|
||||||
_checkForUpdate();
|
_checkForUpdate();
|
||||||
|
setState(() => _isBusy = false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,7 +529,44 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return SizeChangedLayoutNotifier(
|
return SizeChangedLayoutNotifier(
|
||||||
child: widget.child,
|
child: _isBusy
|
||||||
|
? Material(
|
||||||
|
key: Key('app-splash-screen-$_isBusy'),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
CustomPaint(painter: GraphPainter()),
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 240,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/icon/icon.png',
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
Text('Solar Network').bold(),
|
||||||
|
AppVersionLabel(),
|
||||||
|
Gap(8),
|
||||||
|
Text(
|
||||||
|
_phaseText,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
Gap(16),
|
||||||
|
const LinearProgressIndicator(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: widget.child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -509,3 +574,44 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GraphPainter extends CustomPainter {
|
||||||
|
final Random random = Random();
|
||||||
|
final int numNodes = 20;
|
||||||
|
final double maxDistance = 100; // Max distance to draw a line
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paintNode = Paint()..color = Colors.white;
|
||||||
|
final paintEdge = Paint()
|
||||||
|
..color = Colors.white.withOpacity(0.3)
|
||||||
|
..strokeWidth = 1;
|
||||||
|
|
||||||
|
// Generate random points
|
||||||
|
List<Offset> nodes = List.generate(
|
||||||
|
numNodes,
|
||||||
|
(_) => Offset(
|
||||||
|
random.nextDouble() * size.width,
|
||||||
|
random.nextDouble() * size.height,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw edges between close nodes
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
for (var j = i + 1; j < nodes.length; j++) {
|
||||||
|
double distance = (nodes[i] - nodes[j]).distance;
|
||||||
|
if (distance < maxDistance) {
|
||||||
|
canvas.drawLine(nodes[i], nodes[j], paintEdge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw nodes
|
||||||
|
for (var node in nodes) {
|
||||||
|
canvas.drawCircle(node, 4, paintNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,19 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
_rels = context.read<SnRealmProvider>();
|
_rels = context.read<SnRealmProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final List<SnChannel> _availableChannels = List.empty(growable: true);
|
||||||
|
|
||||||
|
List<SnChannel> get availableChannels => _availableChannels;
|
||||||
|
|
||||||
|
Future<void> refreshAvailableChannels() async {
|
||||||
|
final stream = fetchChannels();
|
||||||
|
stream.listen((ele) {
|
||||||
|
_availableChannels.clear();
|
||||||
|
_availableChannels.addAll(ele);
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
channels.map(
|
channels.map(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
|
|
||||||
class AppNavDestination {
|
class AppNavDestination {
|
||||||
final String label;
|
final String label;
|
||||||
@@ -88,7 +89,7 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
'home',
|
'home',
|
||||||
'explore',
|
'explore',
|
||||||
'chat',
|
'chat',
|
||||||
'account',
|
'realm',
|
||||||
];
|
];
|
||||||
|
|
||||||
List<AppNavDestination> destinations = [];
|
List<AppNavDestination> destinations = [];
|
||||||
@@ -143,4 +144,11 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
_currentIndex = idx;
|
_currentIndex = idx;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SnRealm? focusedRealm;
|
||||||
|
|
||||||
|
void setFocusedRealm(SnRealm? realm) {
|
||||||
|
focusedRealm = realm;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,13 +321,13 @@ class SnAttachmentProvider {
|
|||||||
uuid: ele.uuid,
|
uuid: ele.uuid,
|
||||||
content: ele,
|
content: ele,
|
||||||
accountId: ele.accountId,
|
accountId: ele.accountId,
|
||||||
cacheExpiredAt: DateTime.now().add(const Duration(days: 7)),
|
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
|
||||||
),
|
),
|
||||||
onConflict: DoUpdate(
|
onConflict: DoUpdate(
|
||||||
(_) => SnLocalAttachmentCompanion.custom(
|
(_) => SnLocalAttachmentCompanion.custom(
|
||||||
content: Constant(jsonEncode(ele.toJson())),
|
content: Constant(jsonEncode(ele.toJson())),
|
||||||
cacheExpiredAt:
|
cacheExpiredAt:
|
||||||
Constant(DateTime.now().add(const Duration(days: 7))),
|
Constant(DateTime.now().add(const Duration(hours: 1))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/database/database.dart';
|
||||||
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
|
|
||||||
class SnRealmProvider {
|
class SnRealmProvider {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
|
late final DatabaseProvider _dt;
|
||||||
|
|
||||||
SnRealmProvider(BuildContext context) {
|
SnRealmProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
|
_dt = context.read<DatabaseProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, SnRealm> _cache = {};
|
final Map<String, SnRealm> _cache = {};
|
||||||
|
List<SnRealm> _availableRealms = List.empty(growable: true);
|
||||||
|
|
||||||
|
Future<void> refreshAvailableRealms() async {
|
||||||
|
_availableRealms = await listAvailableRealms();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SnRealm> get availableRealms => _availableRealms;
|
||||||
|
|
||||||
Future<List<SnRealm>> listAvailableRealms() async {
|
Future<List<SnRealm>> listAvailableRealms() async {
|
||||||
final resp = await _sn.client.get('/cgi/id/realms/me/available');
|
final resp = await _sn.client.get('/cgi/id/realms/me/available');
|
||||||
@@ -21,6 +35,7 @@ class SnRealmProvider {
|
|||||||
_cache[realm.alias] = realm;
|
_cache[realm.alias] = realm;
|
||||||
_cache[realm.id.toString()] = realm;
|
_cache[realm.id.toString()] = realm;
|
||||||
}
|
}
|
||||||
|
_saveToLocal(out);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,10 +43,43 @@ class SnRealmProvider {
|
|||||||
if (_cache.containsKey(aliasOrId.toString())) {
|
if (_cache.containsKey(aliasOrId.toString())) {
|
||||||
return _cache[aliasOrId.toString()]!;
|
return _cache[aliasOrId.toString()]!;
|
||||||
}
|
}
|
||||||
|
final localResp = await (_dt.db.snLocalRealm.select()
|
||||||
|
..where((e) =>
|
||||||
|
e.id.equals(aliasOrId is int ? aliasOrId : 0) |
|
||||||
|
e.alias.equals(aliasOrId.toString()))
|
||||||
|
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (localResp != null) {
|
||||||
|
_cache[localResp.content.id.toString()] = localResp.content;
|
||||||
|
_cache[localResp.content.alias] = localResp.content;
|
||||||
|
return localResp.content;
|
||||||
|
}
|
||||||
final resp = await _sn.client.get('/cgi/id/realms/$aliasOrId');
|
final resp = await _sn.client.get('/cgi/id/realms/$aliasOrId');
|
||||||
final out = SnRealm.fromJson(resp.data);
|
final out = SnRealm.fromJson(resp.data);
|
||||||
_cache[out.alias] = out;
|
_cache[out.alias] = out;
|
||||||
_cache[out.id.toString()] = out;
|
_cache[out.id.toString()] = out;
|
||||||
|
_saveToLocal([out]);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _saveToLocal(Iterable<SnRealm> out) async {
|
||||||
|
for (final ele in out) {
|
||||||
|
await _dt.db.snLocalRealm.insertOne(
|
||||||
|
SnLocalRealmCompanion.insert(
|
||||||
|
id: Value(ele.id),
|
||||||
|
alias: ele.alias,
|
||||||
|
content: ele,
|
||||||
|
accountId: ele.accountId,
|
||||||
|
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
|
||||||
|
),
|
||||||
|
onConflict: DoUpdate(
|
||||||
|
(_) => SnLocalRealmCompanion.custom(
|
||||||
|
content: Constant(jsonEncode(ele.toJson())),
|
||||||
|
cacheExpiredAt:
|
||||||
|
Constant(DateTime.now().add(const Duration(hours: 1))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import 'package:surface/screens/account/badges.dart';
|
|||||||
import 'package:surface/screens/account/contact_methods.dart';
|
import 'package:surface/screens/account/contact_methods.dart';
|
||||||
import 'package:surface/screens/account/factor_settings.dart';
|
import 'package:surface/screens/account/factor_settings.dart';
|
||||||
import 'package:surface/screens/account/keypairs.dart';
|
import 'package:surface/screens/account/keypairs.dart';
|
||||||
|
import 'package:surface/screens/account/prefs/notify.dart';
|
||||||
|
import 'package:surface/screens/account/prefs/security.dart';
|
||||||
import 'package:surface/screens/account/profile_page.dart';
|
import 'package:surface/screens/account/profile_page.dart';
|
||||||
import 'package:surface/screens/account/profile_edit.dart';
|
import 'package:surface/screens/account/profile_edit.dart';
|
||||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||||
@@ -161,6 +163,18 @@ final _appRoutes = [
|
|||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'accountSettings',
|
name: 'accountSettings',
|
||||||
builder: (context, state) => AccountSettingsScreen(),
|
builder: (context, state) => AccountSettingsScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/notify',
|
||||||
|
name: 'accountSettingsNotify',
|
||||||
|
builder: (context, state) => const AccountNotifyPrefsScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/auth',
|
||||||
|
name: 'accountSettingsSecurity',
|
||||||
|
builder: (context, state) => const AccountSecurityPrefsScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/settings/factors',
|
path: '/settings/factors',
|
||||||
|
|||||||
@@ -97,6 +97,26 @@ class AccountSettingsScreen extends StatelessWidget {
|
|||||||
GoRouter.of(context).pushNamed('accountContactMethods');
|
GoRouter.of(context).pushNamed('accountContactMethods');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('accountSettingsNotify').tr(),
|
||||||
|
subtitle: Text('accountSettingsNotifyDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.notifications),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed('accountSettingsNotify');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('accountSettingsSecurity').tr(),
|
||||||
|
subtitle: Text('accountSettingsSecurityDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.shield),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed('accountSettingsSecurity');
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('accountProfileEdit').tr(),
|
title: Text('accountProfileEdit').tr(),
|
||||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||||
|
|||||||
@@ -1,11 +1,122 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
class AccountNotifyPrefsScreen extends StatelessWidget {
|
final Map<String, String> kNotifyTopicMap = {
|
||||||
|
'interactive.reply': 'notificationTopicPostReply'.tr(),
|
||||||
|
'interactive.feedback': 'notificationTopicPostFeedback'.tr(),
|
||||||
|
'interactive.subscription': 'notificationTopicPostSubscription'.tr(),
|
||||||
|
'messaging.message': 'notificationTopicMessaging'.tr(),
|
||||||
|
'messaging.call': 'notificationTopicMessagingCall'.tr(),
|
||||||
|
'general': 'notificationTopicGeneral'.tr(),
|
||||||
|
};
|
||||||
|
|
||||||
|
class AccountNotifyPrefsScreen extends StatefulWidget {
|
||||||
const AccountNotifyPrefsScreen({super.key});
|
const AccountNotifyPrefsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AccountNotifyPrefsScreen> createState() =>
|
||||||
|
_AccountNotifyPrefsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> {
|
||||||
|
bool _isBusy = true;
|
||||||
|
|
||||||
|
Map<String, bool> _config = {};
|
||||||
|
|
||||||
|
Future<void> _getPreferences() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final resp = await sn.client.get('/cgi/id/preferences/notifications');
|
||||||
|
_config = resp.data['config']
|
||||||
|
.map((k, v) => MapEntry(k, v as bool))
|
||||||
|
.cast<String, bool>();
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _savePreferences() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sn.client.put(
|
||||||
|
'/cgi/id/preferences/notifications',
|
||||||
|
data: {
|
||||||
|
'config': _config,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('accountSettingsApplied'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold();
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('accountSettingsNotify').tr(),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
ListTile(
|
||||||
|
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Icons.save),
|
||||||
|
title: Text('save').tr(),
|
||||||
|
enabled: !_isBusy,
|
||||||
|
onTap: () {
|
||||||
|
_savePreferences();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: kNotifyTopicMap.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final element = kNotifyTopicMap.entries.elementAt(index);
|
||||||
|
return CheckboxListTile(
|
||||||
|
title: Text(element.value),
|
||||||
|
subtitle: Text(
|
||||||
|
element.key,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||||
|
),
|
||||||
|
value: _config[element.key] ?? true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_config[element.key] = value ?? false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
lib/screens/account/prefs/security.dart
Normal file
147
lib/screens/account/prefs/security.dart
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/loading_indicator.dart';
|
||||||
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
|
class AccountSecurityPrefsScreen extends StatefulWidget {
|
||||||
|
const AccountSecurityPrefsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AccountSecurityPrefsScreen> createState() =>
|
||||||
|
_AccountSecurityPrefsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccountSecurityPrefsScreenState
|
||||||
|
extends State<AccountSecurityPrefsScreen> {
|
||||||
|
bool _isBusy = true;
|
||||||
|
|
||||||
|
Map<String, dynamic> _config = {
|
||||||
|
'maximum_auth_steps': 2,
|
||||||
|
'always_risky': false,
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<void> _getPreferences() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final resp = await sn.client.get('/cgi/id/preferences/auth');
|
||||||
|
_config = resp.data['config']
|
||||||
|
.map((k, v) => MapEntry(k, v as bool))
|
||||||
|
.cast<String, bool>();
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _savePreferences() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sn.client.put(
|
||||||
|
'/cgi/id/preferences/auth',
|
||||||
|
data: {
|
||||||
|
'config': _config,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('accountSettingsApplied'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const PageBackButton(),
|
||||||
|
title: Text('accountSettingsSecurity').tr(),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
LoadingIndicator(isActive: _isBusy),
|
||||||
|
ListTile(
|
||||||
|
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Icons.save),
|
||||||
|
title: Text('save').tr(),
|
||||||
|
enabled: !_isBusy,
|
||||||
|
onTap: () {
|
||||||
|
_savePreferences();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text('authMaximumAuthSteps').tr(),
|
||||||
|
subtitle: Text('authMaximumAuthStepsDescription')
|
||||||
|
.plural(_config['maximum_auth_steps'] ?? 2),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
icon: const Icon(Symbols.remove),
|
||||||
|
onPressed: () {
|
||||||
|
if (_config['maximum_auth_steps'] > 1) {
|
||||||
|
setState(() => _config['maximum_auth_steps']--);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
icon: const Icon(Symbols.add),
|
||||||
|
onPressed: () {
|
||||||
|
if (_config['maximum_auth_steps'] < 99) {
|
||||||
|
setState(() => _config['maximum_auth_steps']++);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text('authAlwaysRisky').tr(),
|
||||||
|
subtitle: Text('authAlwaysRiskyDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
value: _config['always_risky'] ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _config['always_risky'] = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,8 +52,10 @@ class ChatRoomScreen extends StatefulWidget {
|
|||||||
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
bool _isCalling = false;
|
bool _isCalling = false;
|
||||||
|
bool _isJoining = false;
|
||||||
|
|
||||||
SnChannel? _channel;
|
SnChannel? _channel;
|
||||||
|
SnChannelMember? _currentMember;
|
||||||
SnChannelMember? _otherMember;
|
SnChannelMember? _otherMember;
|
||||||
SnChatCall? _ongoingCall;
|
SnChatCall? _ongoingCall;
|
||||||
|
|
||||||
@@ -67,7 +69,24 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
|
|
||||||
StreamSubscription? _wsSubscription;
|
StreamSubscription? _wsSubscription;
|
||||||
|
|
||||||
// TODO fetch user identity and ask them to join the channel or not
|
Future<void> _joinChannel() async {
|
||||||
|
try {
|
||||||
|
setState(() => _isJoining = true);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
await sn.client
|
||||||
|
.post('/cgi/im/channels/${_channel!.keyPath}/members', data: {
|
||||||
|
'related': ua.user?.name,
|
||||||
|
});
|
||||||
|
_initializeChat();
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isJoining = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _fetchChannel() async {
|
Future<void> _fetchChannel() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
@@ -76,6 +95,12 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
_channel = await chan.getChannel('${widget.scope}:${widget.alias}');
|
_channel = await chan.getChannel('${widget.scope}:${widget.alias}');
|
||||||
|
|
||||||
if (!mounted || _channel == null) return;
|
if (!mounted || _channel == null) return;
|
||||||
|
final ct = context.read<ChatChannelProvider>();
|
||||||
|
try {
|
||||||
|
_currentMember = await ct.getChannelProfile(_channel!);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
if (!mounted || _currentMember == null) return;
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
if (_channel!.type == 1) {
|
if (_channel!.type == 1) {
|
||||||
@@ -204,11 +229,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _initializeChat() async {
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_messageController = ChatMessageController(context);
|
|
||||||
_fetchChannel().then((_) async {
|
_fetchChannel().then((_) async {
|
||||||
|
if (_currentMember == null) return;
|
||||||
await _messageController.initialize(_channel!);
|
await _messageController.initialize(_channel!);
|
||||||
|
|
||||||
if (widget.extra != null) {
|
if (widget.extra != null) {
|
||||||
@@ -230,6 +253,13 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
_fetchOngoingCall(),
|
_fetchOngoingCall(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_messageController = ChatMessageController(context);
|
||||||
|
_initializeChat();
|
||||||
|
|
||||||
_wsSubscription = _ws.pk.stream.listen((event) {
|
_wsSubscription = _ws.pk.stream.listen((event) {
|
||||||
switch (event.method) {
|
switch (event.method) {
|
||||||
@@ -281,6 +311,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
: _channel?.name ?? 'loading'.tr(),
|
: _channel?.name ?? 'loading'.tr(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
if (_currentMember != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _isEncrypted = !_isEncrypted);
|
setState(() => _isEncrypted = !_isEncrypted);
|
||||||
@@ -290,6 +321,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
? const Icon(Symbols.lock)
|
? const Icon(Symbols.lock)
|
||||||
: const Icon(Symbols.no_encryption),
|
: const Icon(Symbols.no_encryption),
|
||||||
),
|
),
|
||||||
|
if (_currentMember != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: _ongoingCall == null
|
icon: _ongoingCall == null
|
||||||
? const Icon(Symbols.call)
|
? const Icon(Symbols.call)
|
||||||
@@ -348,7 +380,41 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
|
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
|
||||||
const Duration(milliseconds: 300),
|
const Duration(milliseconds: 300),
|
||||||
Curves.fastLinearToSlowEaseIn),
|
Curves.fastLinearToSlowEaseIn),
|
||||||
if (_messageController.isPending)
|
if (_currentMember == null && !_isBusy)
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 280),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.person_remove, size: 40, fill: 1),
|
||||||
|
const Gap(8),
|
||||||
|
Text('chatUnjoined'.tr(), textAlign: TextAlign.center)
|
||||||
|
.fontSize(16)
|
||||||
|
.bold(),
|
||||||
|
Text('chatUnjoinedDescription'.tr(),
|
||||||
|
textAlign: TextAlign.center)
|
||||||
|
.fontSize(13),
|
||||||
|
if (_channel!.isPublic)
|
||||||
|
Text('chatUnjoinedPublicDescription'.tr(),
|
||||||
|
textAlign: TextAlign.center)
|
||||||
|
.fontSize(13)
|
||||||
|
.padding(top: 8),
|
||||||
|
if (_channel!.isPublic)
|
||||||
|
TextButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
onPressed: _isJoining ? null : _joinChannel,
|
||||||
|
child: Text('chatJoin').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (_messageController.isPending)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: const CircularProgressIndicator().center(),
|
child: const CircularProgressIndicator().center(),
|
||||||
)
|
)
|
||||||
@@ -403,7 +469,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!_messageController.isPending)
|
if (!_messageController.isPending && _currentMember != null)
|
||||||
Material(
|
Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
data: ele.toJson(),
|
data: ele.toJson(),
|
||||||
createdAt: ele.createdAt)),
|
createdAt: ele.createdAt)),
|
||||||
);
|
);
|
||||||
_hasLoadedAll = postCount >= _feed.length;
|
_hasLoadedAll = _feed.length >= postCount;
|
||||||
|
|
||||||
if (mounted) setState(() => _isBusy = false);
|
if (mounted) setState(() => _isBusy = false);
|
||||||
}
|
}
|
||||||
@@ -551,9 +551,11 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
maxWidth: 640,
|
maxWidth: 640,
|
||||||
);
|
);
|
||||||
case 'reader.news':
|
case 'reader.news':
|
||||||
return Container(
|
return Center(
|
||||||
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
child: NewsFeedEntry(data: ele),
|
child: NewsFeedEntry(data: ele),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return Container(
|
return Container(
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
|
|||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text('serviceStatusOperational').tr(),
|
Text('loading').tr(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: switch (_serviceStatus) {
|
: switch (_serviceStatus) {
|
||||||
@@ -434,6 +434,7 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
|
|||||||
padding: EdgeInsets.only(top: 6),
|
padding: EdgeInsets.only(top: 6),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
for (final entry in _statuses!.entries)
|
for (final entry in _statuses!.entries)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
@@ -441,6 +442,8 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
|
|||||||
? 'serviceName${kServicesName[entry.key]}'.tr()
|
? 'serviceName${kServicesName[entry.key]}'.tr()
|
||||||
: 'unknown'.tr(),
|
: 'unknown'.tr(),
|
||||||
child: Chip(
|
child: Chip(
|
||||||
|
visualDensity:
|
||||||
|
VisualDensity(horizontal: -4, vertical: -4),
|
||||||
avatar: entry.value
|
avatar: entry.value
|
||||||
? const Icon(
|
? const Icon(
|
||||||
Symbols.circle,
|
Symbols.circle,
|
||||||
@@ -877,8 +880,10 @@ class _HomeDashRecommendationPostWidgetState
|
|||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text('${_currentPage + 1}/${_posts?.length ?? 0}',
|
Text(
|
||||||
style: GoogleFonts.robotoMono())
|
'${_currentPage + 1}/${_posts?.length ?? 0}',
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -896,6 +901,7 @@ class _HomeDashRecommendationPostWidgetState
|
|||||||
child: PostItem(
|
child: PostItem(
|
||||||
data: _posts![index],
|
data: _posts![index],
|
||||||
showMenu: false,
|
showMenu: false,
|
||||||
|
showFullPost: true,
|
||||||
).padding(bottom: 8),
|
).padding(bottom: 8),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context)
|
GoRouter.of(context)
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
queryParameters: {'take': 10, 'offset': _notifications.length},
|
queryParameters: {'take': 10, 'offset': _notifications.length},
|
||||||
);
|
);
|
||||||
_totalCount = resp.data['count'];
|
_totalCount = resp.data['count'];
|
||||||
_notifications.addAll(resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? []);
|
_notifications.addAll(resp.data['data']
|
||||||
|
?.map((e) => SnNotification.fromJson(e))
|
||||||
|
.cast<SnNotification>() ??
|
||||||
|
[]);
|
||||||
nty.updateTray();
|
nty.updateTray();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -98,7 +101,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
nty.clear();
|
nty.clear();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('notificationMarkAllReadPrompt'.plural(resp.data['count']));
|
context.showSnackbar(
|
||||||
|
'notificationMarkAllReadPrompt'.plural(resp.data['count']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -122,7 +126,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
|
context.showSnackbar(
|
||||||
|
'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -143,7 +148,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenNotification').tr()),
|
appBar: AppBar(
|
||||||
|
leading: AutoAppBarLeading(),
|
||||||
|
title: Text('screenNotification').tr()),
|
||||||
body: Center(child: UnauthorizedHint()),
|
body: Center(child: UnauthorizedHint()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -153,7 +160,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenNotification').tr(),
|
title: Text('screenNotification').tr(),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(icon: const Icon(Symbols.checklist), onPressed: _isSubmitting ? null : _markAllAsRead),
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.checklist),
|
||||||
|
onPressed: _isSubmitting ? null : _markAllAsRead),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -167,13 +176,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
return _fetchNotifications();
|
return _fetchNotifications();
|
||||||
},
|
},
|
||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
padding: EdgeInsets.only(top: 16, bottom: math.max(MediaQuery.of(context).padding.bottom, 16)),
|
padding: EdgeInsets.only(
|
||||||
|
top: 16,
|
||||||
|
bottom:
|
||||||
|
math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||||
itemCount: _notifications.length,
|
itemCount: _notifications.length,
|
||||||
onFetchData: () {
|
onFetchData: () {
|
||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
},
|
},
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
|
hasReachedMax: _totalCount != null &&
|
||||||
|
_notifications.length >= _totalCount!,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
final nty = _notifications[idx];
|
final nty = _notifications[idx];
|
||||||
return Row(
|
return Row(
|
||||||
@@ -186,12 +199,19 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (nty.readAt == null)
|
if (nty.readAt == null)
|
||||||
StyledWidget(Badge(label: Text('notificationUnread').tr())).padding(bottom: 4),
|
StyledWidget(Badge(
|
||||||
Text(nty.title, style: Theme.of(context).textTheme.titleMedium),
|
label: Text('notificationUnread').tr()))
|
||||||
|
.padding(bottom: 4),
|
||||||
|
Text(nty.title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
if (nty.subtitle != null)
|
if (nty.subtitle != null)
|
||||||
Text(nty.subtitle!, style: Theme.of(context).textTheme.titleSmall),
|
Text(nty.subtitle!,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleSmall),
|
||||||
if (nty.subtitle != null) const Gap(4),
|
if (nty.subtitle != null) const Gap(4),
|
||||||
SelectionArea(child: MarkdownTextContent(content: nty.body, isAutoWarp: true)),
|
SelectionArea(
|
||||||
|
child: MarkdownTextContent(
|
||||||
|
content: nty.body, isAutoWarp: true)),
|
||||||
if ([
|
if ([
|
||||||
'interactive.reply',
|
'interactive.reply',
|
||||||
'interactive.feedback',
|
'interactive.feedback',
|
||||||
@@ -201,31 +221,43 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(
|
||||||
border: Border.all(color: Theme.of(context).dividerColor, width: 1),
|
Radius.circular(8)),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1),
|
||||||
),
|
),
|
||||||
child: PostItem(
|
child: PostItem(
|
||||||
data: SnPost.fromJson(nty.metadata['related_post']!),
|
data: SnPost.fromJson(
|
||||||
|
nty.metadata['related_post']!),
|
||||||
showComments: false,
|
showComments: false,
|
||||||
showReactions: false,
|
showReactions: false,
|
||||||
showMenu: false,
|
showMenu: false,
|
||||||
),
|
).padding(vertical: 4),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postDetail',
|
'postDetail',
|
||||||
pathParameters: {'slug': nty.metadata['related_post']!['id'].toString()},
|
pathParameters: {
|
||||||
|
'slug': nty
|
||||||
|
.metadata['related_post']!['id']
|
||||||
|
.toString()
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).padding(top: 8),
|
).padding(top: 8),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(DateFormat('yy/MM/dd').format(nty.createdAt)).fontSize(12),
|
Text(DateFormat('yy/MM/dd')
|
||||||
|
.format(nty.createdAt))
|
||||||
|
.fontSize(12),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text('·', style: TextStyle(fontSize: 12)),
|
Text('·', style: TextStyle(fontSize: 12)),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(RelativeTime(context).format(nty.createdAt)).fontSize(12),
|
Text(RelativeTime(context)
|
||||||
|
.format(nty.createdAt))
|
||||||
|
.fontSize(12),
|
||||||
],
|
],
|
||||||
).opacity(0.75),
|
).opacity(0.75),
|
||||||
],
|
],
|
||||||
@@ -235,8 +267,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.check),
|
icon: const Icon(Symbols.check),
|
||||||
padding: EdgeInsets.all(0),
|
padding: EdgeInsets.all(0),
|
||||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity:
|
||||||
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
|
const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
|
onPressed:
|
||||||
|
_isSubmitting ? null : () => _markOneAsRead(nty),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16);
|
).padding(horizontal: 16);
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ class PostDetailScreen extends StatefulWidget {
|
|||||||
final SnPost? preload;
|
final SnPost? preload;
|
||||||
final Function? onBack;
|
final Function? onBack;
|
||||||
|
|
||||||
const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
|
const PostDetailScreen(
|
||||||
|
{super.key, required this.slug, this.preload, this.onBack});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
||||||
@@ -88,14 +89,16 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
color:
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'postDetail'.tr(),
|
text: 'postDetail'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
color:
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
@@ -124,8 +127,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_data != null && _data!.type != 'video') const SliverToBoxAdapter(child: Divider(height: 1)),
|
if (_data != null)
|
||||||
if (_data != null && _data!.type != 'video')
|
SliverToBoxAdapter(
|
||||||
|
child: Divider(height: 1).padding(top: 8),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
@@ -141,7 +147,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
).padding(horizontal: 20, vertical: 12).center(),
|
).padding(horizontal: 20, vertical: 12).center(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_data != null && ua.isAuthorized && _data!.type != 'video')
|
if (_data != null && ua.isAuthorized)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: PostCommentQuickAction(
|
child: PostCommentQuickAction(
|
||||||
parentPost: _data!,
|
parentPost: _data!,
|
||||||
@@ -158,13 +164,15 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_data != null && _data!.type != 'video')
|
if (_data != null) SliverGap(8),
|
||||||
|
if (_data != null)
|
||||||
PostCommentSliverList(
|
PostCommentSliverList(
|
||||||
key: _childListKey,
|
key: _childListKey,
|
||||||
parentPost: _data!,
|
parentPost: _data!,
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
),
|
),
|
||||||
if (_data != null && _data!.type == 'video') SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
if (_data != null)
|
||||||
|
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
@@ -77,7 +77,8 @@ class _PostDraftBoxState extends State<PostDraftBox> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (_, __) => const Gap(8),
|
separatorBuilder: (_, __) =>
|
||||||
|
const Divider().padding(vertical: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchPosts() async {
|
Future<void> _fetchPosts() async {
|
||||||
if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty) return;
|
if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty)
|
||||||
|
return;
|
||||||
if (_postCount != null && _posts.length >= _postCount!) return;
|
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
@@ -152,7 +153,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (_, __) => const Gap(8),
|
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 16,
|
top: 16,
|
||||||
@@ -166,7 +167,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
padding: const WidgetStatePropertyAll(
|
padding: const WidgetStatePropertyAll(
|
||||||
EdgeInsets.symmetric(horizontal: 24),
|
EdgeInsets.symmetric(horizontal: 24),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_searchTerm = value;
|
_searchTerm = value;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ class PostPublisherScreen extends StatefulWidget {
|
|||||||
State<PostPublisherScreen> createState() => _PostPublisherScreenState();
|
State<PostPublisherScreen> createState() => _PostPublisherScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTickerProviderStateMixin {
|
class _PostPublisherScreenState extends State<PostPublisherScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late final ScrollController _scrollController = ScrollController();
|
late final ScrollController _scrollController = ScrollController();
|
||||||
late final TabController _tabController = TabController(length: 3, vsync: this);
|
late final TabController _tabController =
|
||||||
|
TabController(length: 3, vsync: this);
|
||||||
|
|
||||||
SnPublisher? _publisher;
|
SnPublisher? _publisher;
|
||||||
SnAccount? _account;
|
SnAccount? _account;
|
||||||
@@ -66,7 +68,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
_account = await ud.getAccount(_publisher?.accountId);
|
_account = await ud.getAccount(_publisher?.accountId);
|
||||||
_accountRelationship = await rel.getRelationship(_account!.id);
|
_accountRelationship = await rel.getRelationship(_account!.id);
|
||||||
if (_publisher?.realmId != null && _publisher!.realmId != 0) {
|
if (_publisher?.realmId != null && _publisher!.realmId != 0) {
|
||||||
final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
|
||||||
_realm = SnRealm.fromJson(resp.data);
|
_realm = SnRealm.fromJson(resp.data);
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -133,12 +136,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
double _appBarBlur = 0.0;
|
double _appBarBlur = 0.0;
|
||||||
|
|
||||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||||
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
|
late final _appBarHeight =
|
||||||
|
(_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||||
|
|
||||||
void _updateAppBarBlur() {
|
void _updateAppBarBlur() {
|
||||||
if (_scrollController.offset > _appBarHeight) return;
|
if (_scrollController.offset > _appBarHeight) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
_appBarBlur =
|
||||||
|
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +198,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
'related': _account!.name,
|
'related': _account!.name,
|
||||||
});
|
});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
context.showSnackbar(
|
||||||
|
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -209,9 +215,11 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final rel = context.read<SnRelationshipProvider>();
|
final rel = context.read<SnRelationshipProvider>();
|
||||||
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
await rel.updateRelationship(
|
||||||
|
_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
context.showSnackbar(
|
||||||
|
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -299,7 +307,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
text: TextSpan(children: [
|
text: TextSpan(children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _publisher!.nick,
|
text: _publisher!.nick,
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleLarge!
|
||||||
|
.copyWith(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shadows: labelShadows,
|
shadows: labelShadows,
|
||||||
),
|
),
|
||||||
@@ -307,7 +318,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '@${_publisher!.name}',
|
text: '@${_publisher!.name}',
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall!
|
||||||
|
.copyWith(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shadows: labelShadows,
|
shadows: labelShadows,
|
||||||
),
|
),
|
||||||
@@ -330,13 +344,16 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
Container(
|
Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer,
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: 56 + MediaQuery.of(context).padding.top,
|
height:
|
||||||
|
56 + MediaQuery.of(context).padding.top,
|
||||||
child: ClipRect(
|
child: ClipRect(
|
||||||
child: BackdropFilter(
|
child: BackdropFilter(
|
||||||
filter: ImageFilter.blur(
|
filter: ImageFilter.blur(
|
||||||
@@ -345,7 +362,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.black.withOpacity(
|
color: Colors.black.withOpacity(
|
||||||
clampDouble(_appBarBlur * 0.1, 0, 0.5),
|
clampDouble(
|
||||||
|
_appBarBlur * 0.1, 0, 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -372,11 +390,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
const Gap(16),
|
const Gap(16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_publisher!.nick,
|
_publisher!.nick,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium,
|
||||||
).bold(),
|
).bold(),
|
||||||
Text('@${_publisher!.name}').fontSize(13),
|
Text('@${_publisher!.name}').fontSize(13),
|
||||||
],
|
],
|
||||||
@@ -387,7 +408,9 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
elevation: WidgetStatePropertyAll(0),
|
elevation: WidgetStatePropertyAll(0),
|
||||||
),
|
),
|
||||||
onPressed: _isSubscribing ? null : _toggleSubscription,
|
onPressed: _isSubscribing
|
||||||
|
? null
|
||||||
|
: _toggleSubscription,
|
||||||
label: Text('subscribe').tr(),
|
label: Text('subscribe').tr(),
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
)
|
)
|
||||||
@@ -396,14 +419,17 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
elevation: WidgetStatePropertyAll(0),
|
elevation: WidgetStatePropertyAll(0),
|
||||||
),
|
),
|
||||||
onPressed: _isSubscribing ? null : _toggleSubscription,
|
onPressed: _isSubscribing
|
||||||
|
? null
|
||||||
|
: _toggleSubscription,
|
||||||
label: Text('unsubscribe').tr(),
|
label: Text('unsubscribe').tr(),
|
||||||
icon: const Icon(Symbols.remove),
|
icon: const Icon(Symbols.remove),
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity: VisualDensity(
|
||||||
|
horizontal: -4, vertical: -4),
|
||||||
),
|
),
|
||||||
itemBuilder: (BuildContext context) => [
|
itemBuilder: (BuildContext context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@@ -443,7 +469,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Text(_publisher!.description).padding(horizontal: 8),
|
Text(_publisher!.description)
|
||||||
|
.padding(horizontal: 8),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -451,8 +478,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.calendar_add_on),
|
const Icon(Symbols.calendar_add_on),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('publisherJoinedAt')
|
Text('publisherJoinedAt').tr(args: [
|
||||||
.tr(args: [DateFormat('y/M/d').format(_publisher!.createdAt)]),
|
DateFormat('y/M/d')
|
||||||
|
.format(_publisher!.createdAt)
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -460,7 +489,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
const Icon(Symbols.trending_up),
|
const Icon(Symbols.trending_up),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('publisherSocialPointTotal').plural(
|
Text('publisherSocialPointTotal').plural(
|
||||||
_publisher!.totalUpvote - _publisher!.totalDownvote,
|
_publisher!.totalUpvote -
|
||||||
|
_publisher!.totalDownvote,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -470,18 +500,22 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
const Icon(Symbols.group_work),
|
const Icon(Symbols.group_work),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Text('publisherAffiliatedBy').tr(args: [
|
child: Text('publisherAffiliatedBy')
|
||||||
|
.tr(args: [
|
||||||
'@${_realm?.alias ?? 'unknown'}',
|
'@${_realm?.alias ?? 'unknown'}',
|
||||||
]),
|
]),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'realmDetail',
|
'realmDetail',
|
||||||
pathParameters: {'alias': _realm!.alias},
|
pathParameters: {
|
||||||
|
'alias': _realm!.alias
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
AccountImage(content: _realm?.avatar, radius: 8),
|
AccountImage(
|
||||||
|
content: _realm?.avatar, radius: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -502,7 +536,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
AccountImage(content: _account?.avatar, radius: 8),
|
AccountImage(
|
||||||
|
content: _account?.avatar, radius: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -606,7 +641,7 @@ class _PublisherPostList extends StatelessWidget {
|
|||||||
onDeleted: onDeleted,
|
onDeleted: onDeleted,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (_, __) => const Gap(8),
|
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ Future<ThemeData> createAppTheme(
|
|||||||
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
key: Key('chat-message-${data.id}'),
|
key: Key('chat-message-${data.id}'),
|
||||||
iconOnLeftSwipe: Symbols.reply,
|
iconOnLeftSwipe: Symbols.reply,
|
||||||
iconOnRightSwipe: Symbols.edit,
|
iconOnRightSwipe: Symbols.edit,
|
||||||
swipeSensitivity: 20,
|
swipeSensitivity: 10,
|
||||||
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
||||||
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
||||||
child: ContextMenuArea(
|
child: ContextMenuArea(
|
||||||
|
|||||||
@@ -36,10 +36,12 @@ class ChatTypingIndicator extends StatelessWidget {
|
|||||||
'messageTyping'
|
'messageTyping'
|
||||||
.plural(controller.typingMembers.length, args: [
|
.plural(controller.typingMembers.length, args: [
|
||||||
controller.typingMembers
|
controller.typingMembers
|
||||||
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
.map(
|
||||||
|
(ele) => (ele.nick?.isNotEmpty ?? false)
|
||||||
? ele.nick!
|
? ele.nick!
|
||||||
: ud.getFromCache(ele.accountId)?.name ??
|
: ud.getFromCache(ele.accountId)?.nick ??
|
||||||
'unknown')
|
'unknown',
|
||||||
|
)
|
||||||
.join(', '),
|
.join(', '),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ class NewsFeedEntry extends StatelessWidget {
|
|||||||
.cast<SnNewsArticle>()
|
.cast<SnNewsArticle>()
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return Card(
|
return Column(
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@@ -101,7 +100,6 @@ class NewsFeedEntry extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ class FeedUnknownEntry extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Column(
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.help, size: 36),
|
const Icon(Symbols.help, size: 36),
|
||||||
@@ -21,7 +20,6 @@ class FeedUnknownEntry extends StatelessWidget {
|
|||||||
Text('feedUnknownItem').tr(),
|
Text('feedUnknownItem').tr(),
|
||||||
Text(data.type, style: GoogleFonts.robotoMono()),
|
Text(data.type, style: GoogleFonts.robotoMono()),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 12, vertical: 8),
|
).padding(horizontal: 12, vertical: 8);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: attachment.metadata['ratio'] ??
|
aspectRatio: attachment.metadata['ratio']?.toDouble() ??
|
||||||
switch (attachment.mimetype
|
switch (attachment.mimetype
|
||||||
.split('/')
|
.split('/')
|
||||||
.firstOrNull) {
|
.firstOrNull) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -83,6 +84,16 @@ class AppSystemMenuBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
PlatformMenuItem(
|
||||||
|
shortcut: const SingleActivator(
|
||||||
|
LogicalKeyboardKey.keyH,
|
||||||
|
meta: true,
|
||||||
|
),
|
||||||
|
label: 'trayMenuHide'.tr(),
|
||||||
|
onSelected: () {
|
||||||
|
appWindow.hide();
|
||||||
|
},
|
||||||
|
),
|
||||||
if (onQuit != null)
|
if (onQuit != null)
|
||||||
PlatformMenuItem(
|
PlatformMenuItem(
|
||||||
shortcut: const SingleActivator(
|
shortcut: const SingleActivator(
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:animations/animations.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/navigation.dart';
|
import 'package:surface/providers/navigation.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/sn_realm.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
import 'package:surface/widgets/version_label.dart';
|
import 'package:surface/widgets/version_label.dart';
|
||||||
|
|
||||||
class AppNavigationDrawer extends StatefulWidget {
|
class AppNavigationDrawer extends StatefulWidget {
|
||||||
@@ -25,31 +33,37 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
|
context
|
||||||
|
.read<NavigationProvider>()
|
||||||
|
.autoDetectIndex(GoRouter.maybeOf(context));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final nav = context.watch<NavigationProvider>();
|
final nav = context.watch<NavigationProvider>();
|
||||||
final cfg = context.watch<ConfigProvider>();
|
final cfg = context.watch<ConfigProvider>();
|
||||||
|
final rel = context.read<SnRealmProvider>();
|
||||||
|
|
||||||
final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
|
final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: nav,
|
listenable: nav,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final destinations = [
|
return Drawer(
|
||||||
...nav.destinations.where((ele) => ele.isPinned),
|
|
||||||
...nav.destinations.where((ele) => !ele.isPinned),
|
|
||||||
];
|
|
||||||
|
|
||||||
return NavigationDrawer(
|
|
||||||
elevation: widget.elevation,
|
elevation: widget.elevation,
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
selectedIndex: nav.currentIndex,
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !cfg.drawerIsExpanded)
|
if (!kIsWeb &&
|
||||||
|
(Platform.isWindows ||
|
||||||
|
Platform.isLinux ||
|
||||||
|
Platform.isMacOS) &&
|
||||||
|
!cfg.drawerIsExpanded)
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
@@ -61,6 +75,26 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
),
|
),
|
||||||
child: WindowTitleBarBox(),
|
child: WindowTitleBarBox(),
|
||||||
),
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.top),
|
||||||
|
Expanded(
|
||||||
|
child: PageTransitionSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (Widget child,
|
||||||
|
Animation<double> primaryAnimation,
|
||||||
|
Animation<double> secondaryAnimation) {
|
||||||
|
return SharedAxisTransition(
|
||||||
|
animation: primaryAnimation,
|
||||||
|
secondaryAnimation: secondaryAnimation,
|
||||||
|
fillColor: Colors.transparent,
|
||||||
|
transitionType: SharedAxisTransitionType.horizontal,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: nav.focusedRealm == null
|
||||||
|
? ListView(
|
||||||
|
key: const Key('realm-list-view'),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -72,25 +106,144 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
horizontal: 32,
|
horizontal: 32,
|
||||||
vertical: 12,
|
vertical: 12,
|
||||||
),
|
),
|
||||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
...rel.availableRealms.map((ele) {
|
||||||
return NavigationDrawerDestination(
|
return ListTile(
|
||||||
icon: ele.icon,
|
minTileHeight: 48,
|
||||||
label: Text(ele.label).tr(),
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: AccountImage(
|
||||||
|
content: ele.avatar,
|
||||||
|
radius: 16,
|
||||||
|
),
|
||||||
|
title: Text(ele.name),
|
||||||
|
onTap: () {
|
||||||
|
if (Scaffold.of(context).isDrawerOpen) {
|
||||||
|
GoRouter.of(context).goNamed(
|
||||||
|
'realmDetail',
|
||||||
|
pathParameters: {
|
||||||
|
'alias': ele.alias,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}),
|
Scaffold.of(context).closeDrawer();
|
||||||
const Divider(),
|
}
|
||||||
...destinations.where((ele) => !ele.isPinned).map((ele) {
|
nav.setFocusedRealm(ele);
|
||||||
return NavigationDrawerDestination(
|
},
|
||||||
icon: ele.icon,
|
|
||||||
label: Text(ele.label).tr(),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
onDestinationSelected: (idx) {
|
)
|
||||||
nav.setIndex(idx);
|
: ListView(
|
||||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
key: ValueKey(nav.focusedRealm),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
if (nav.focusedRealm!.banner != null)
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(
|
||||||
|
nav.focusedRealm!.banner!,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
tileColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer,
|
||||||
|
contentPadding: EdgeInsets.only(
|
||||||
|
left: 24,
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
leading: AccountImage(
|
||||||
|
content: nav.focusedRealm!.avatar,
|
||||||
|
radius: 16,
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Symbols.close),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () {
|
||||||
|
nav.setFocusedRealm(null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(nav.focusedRealm!.name),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'realmDetail',
|
||||||
|
pathParameters: {
|
||||||
|
'alias': nav.focusedRealm!.alias,
|
||||||
|
},
|
||||||
|
);
|
||||||
Scaffold.of(context).closeDrawer();
|
Scaffold.of(context).closeDrawer();
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: nav.destinations.where((ele) => ele.isPinned).map(
|
||||||
|
(ele) {
|
||||||
|
return Expanded(
|
||||||
|
child: Tooltip(
|
||||||
|
message: ele.label.tr(),
|
||||||
|
child: IconButton.filledTonal(
|
||||||
|
icon: ele.icon,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).goNamed(ele.screen);
|
||||||
|
Scaffold.of(context).closeDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: AccountImage(content: ua.user?.avatar),
|
||||||
|
title: Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15),
|
||||||
|
subtitle:
|
||||||
|
Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.notifications, fill: 1),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pushNamed('notification');
|
||||||
|
Scaffold.of(context).closeDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.settings, fill: 1),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).pushNamed('settings');
|
||||||
|
Scaffold.of(context).closeDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed('account');
|
||||||
|
Scaffold.of(context).closeDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import 'package:surface/providers/config.dart';
|
|||||||
import 'package:surface/providers/navigation.dart';
|
import 'package:surface/providers/navigation.dart';
|
||||||
import 'package:surface/widgets/connection_indicator.dart';
|
import 'package:surface/widgets/connection_indicator.dart';
|
||||||
import 'package:surface/widgets/navigation/app_background.dart';
|
import 'package:surface/widgets/navigation/app_background.dart';
|
||||||
import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
|
|
||||||
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
|
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
|
||||||
import 'package:surface/widgets/navigation/app_rail_navigation.dart';
|
import 'package:surface/widgets/navigation/app_rail_navigation.dart';
|
||||||
import 'package:surface/widgets/notify_indicator.dart';
|
import 'package:surface/widgets/notify_indicator.dart';
|
||||||
@@ -118,10 +117,6 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
.last
|
.last
|
||||||
.route
|
.route
|
||||||
.name;
|
.name;
|
||||||
final isShowBottomNavigation =
|
|
||||||
NavigationProvider.kShowBottomNavScreen.contains(routeName)
|
|
||||||
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
|
||||||
: false;
|
|
||||||
final isPopable = !NavigationProvider.kAllDestination
|
final isPopable = !NavigationProvider.kAllDestination
|
||||||
.map((ele) => ele.screen)
|
.map((ele) => ele.screen)
|
||||||
.contains(routeName);
|
.contains(routeName);
|
||||||
@@ -232,8 +227,6 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||||
drawerEdgeDragWidth: isPopable ? 0 : null,
|
drawerEdgeDragWidth: isPopable ? 0 : null,
|
||||||
bottomNavigationBar:
|
|
||||||
isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ class FediversePostWidget extends StatelessWidget {
|
|||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
child: Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -75,7 +73,6 @@ class FediversePostWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postDetail',
|
'postDetail',
|
||||||
pathParameters: {'slug': _posts[idx].id.toString()},
|
pathParameters: {'slug': _posts[idx].id.toString()},
|
||||||
@@ -225,6 +226,9 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
|||||||
onPost: () {
|
onPost: () {
|
||||||
_childListKey.currentState!.refresh();
|
_childListKey.currentState!.refresh();
|
||||||
},
|
},
|
||||||
|
onExpand: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class OpenablePostItem extends StatelessWidget {
|
|||||||
transitionType: ContainerTransitionType.fade,
|
transitionType: ContainerTransitionType.fade,
|
||||||
closedElevation: 0,
|
closedElevation: 0,
|
||||||
closedColor: Theme.of(context).colorScheme.surface.withOpacity(
|
closedColor: Theme.of(context).colorScheme.surface.withOpacity(
|
||||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0 : 1,
|
||||||
),
|
),
|
||||||
closedShape: const RoundedRectangleBorder(
|
closedShape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
@@ -122,6 +122,7 @@ class PostItem extends StatefulWidget {
|
|||||||
final bool showMenu;
|
final bool showMenu;
|
||||||
final bool showFullPost;
|
final bool showFullPost;
|
||||||
final bool showAvatar;
|
final bool showAvatar;
|
||||||
|
final bool showCompactAvatar;
|
||||||
final bool showExpandableComments;
|
final bool showExpandableComments;
|
||||||
final double? maxWidth;
|
final double? maxWidth;
|
||||||
final Function(SnPost data)? onChanged;
|
final Function(SnPost data)? onChanged;
|
||||||
@@ -137,6 +138,7 @@ class PostItem extends StatefulWidget {
|
|||||||
this.showMenu = true,
|
this.showMenu = true,
|
||||||
this.showFullPost = false,
|
this.showFullPost = false,
|
||||||
this.showAvatar = true,
|
this.showAvatar = true,
|
||||||
|
this.showCompactAvatar = false,
|
||||||
this.showExpandableComments = false,
|
this.showExpandableComments = false,
|
||||||
this.maxWidth,
|
this.maxWidth,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
@@ -166,6 +168,14 @@ class _PostItemState extends State<PostItem> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant PostItem oldWidget) {
|
||||||
|
_displayText = widget.data.body['content'] ?? '';
|
||||||
|
_displayTitle = widget.data.body['title'] ?? '';
|
||||||
|
_displayDescription = widget.data.body['description'] ?? '';
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _translateText() async {
|
Future<void> _translateText() async {
|
||||||
final ta = context.read<SnTranslator>();
|
final ta = context.read<SnTranslator>();
|
||||||
setState(() => _isTranslating = true);
|
setState(() => _isTranslating = true);
|
||||||
@@ -284,6 +294,243 @@ class _PostItemState extends State<PostItem> {
|
|||||||
attachmentSize -= 80;
|
attachmentSize -= 80;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget.showFullPost) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: widget.maxWidth ?? double.infinity,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (widget.showAvatar)
|
||||||
|
_PostAvatar(
|
||||||
|
data: widget.data,
|
||||||
|
isCompact: false,
|
||||||
|
),
|
||||||
|
if (widget.showAvatar) const Gap(12),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (widget.showCompactAvatar)
|
||||||
|
_PostAvatar(
|
||||||
|
data: widget.data,
|
||||||
|
isCompact: true,
|
||||||
|
),
|
||||||
|
if (widget.showAvatar) const Gap(8),
|
||||||
|
_PostContentHeader(
|
||||||
|
isRelativeDate: !widget.showFullPost,
|
||||||
|
isCompact: false,
|
||||||
|
data: widget.data,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_PostActionPopup(
|
||||||
|
data: widget.data,
|
||||||
|
isAuthor: isAuthor,
|
||||||
|
onShare: () => _doShare(context),
|
||||||
|
onShareImage: () => _doShareViaPicture(context),
|
||||||
|
onSelectAnswer: widget.onSelectAnswer,
|
||||||
|
onDeleted: () {
|
||||||
|
widget.onDeleted?.call();
|
||||||
|
},
|
||||||
|
onTranslate: () {
|
||||||
|
_translateText();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
if (widget.data.preload?.thumbnail != null)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
child: AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(
|
||||||
|
widget.data.preload!.thumbnail!.rid,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.data.preload?.video != null)
|
||||||
|
_PostVideoPlayer(data: widget.data).padding(bottom: 8),
|
||||||
|
if (widget.data.type == 'question')
|
||||||
|
_PostQuestionHint(data: widget.data).padding(bottom: 8),
|
||||||
|
if (_displayDescription.isNotEmpty || _displayTitle.isNotEmpty)
|
||||||
|
_PostHeadline(
|
||||||
|
title: _displayTitle,
|
||||||
|
description: _displayDescription,
|
||||||
|
data: widget.data,
|
||||||
|
isEnlarge:
|
||||||
|
widget.data.type == 'article' && widget.showFullPost,
|
||||||
|
).padding(bottom: 8),
|
||||||
|
if (widget.data.type == 'article' && !widget.showFullPost)
|
||||||
|
Text('postArticle')
|
||||||
|
.tr()
|
||||||
|
.fontSize(13)
|
||||||
|
.opacity(0.75)
|
||||||
|
.padding(bottom: 8),
|
||||||
|
if ((_displayText.isNotEmpty) &&
|
||||||
|
(widget.showFullPost || widget.data.type != 'article'))
|
||||||
|
_PostContentBody(
|
||||||
|
text: _displayText,
|
||||||
|
data: widget.data,
|
||||||
|
isSelectable: widget.showFullPost,
|
||||||
|
isEnlarge:
|
||||||
|
widget.data.type == 'article' && widget.showFullPost,
|
||||||
|
).padding(bottom: 6),
|
||||||
|
if (widget.data.visibility > 0)
|
||||||
|
_PostVisibilityHint(data: widget.data).padding(
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
if (widget.data.body['content_truncated'] == true)
|
||||||
|
_PostTruncatedHint(data: widget.data).padding(
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
if (widget.data.tags.isNotEmpty)
|
||||||
|
_PostTagsList(data: widget.data).padding(top: 4, bottom: 6),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
if (widget.showViews)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.play_circle, size: 20),
|
||||||
|
const Gap(4),
|
||||||
|
Text('postViews').plural(widget.data.totalViews),
|
||||||
|
],
|
||||||
|
).opacity(0.75),
|
||||||
|
if (_isTranslating)
|
||||||
|
AnimateWidgetExtensions(Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.translate, size: 20),
|
||||||
|
const Gap(4),
|
||||||
|
Text('translating').tr(),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.animate(onPlay: (e) => e.repeat())
|
||||||
|
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
|
||||||
|
.then()
|
||||||
|
.fadeOut(
|
||||||
|
duration: 500.ms,
|
||||||
|
delay: 1000.ms,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
),
|
||||||
|
if (_isTranslated)
|
||||||
|
InkWell(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.translate, size: 20),
|
||||||
|
const Gap(4),
|
||||||
|
Text('translated').tr(),
|
||||||
|
],
|
||||||
|
).opacity(0.75),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_displayText = widget.data.body['content'] ?? '';
|
||||||
|
_displayTitle = widget.data.body['title'] ?? '';
|
||||||
|
_displayDescription =
|
||||||
|
widget.data.body['description'] ?? '';
|
||||||
|
_isTranslated = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (widget.data.repostTo != null)
|
||||||
|
_PostQuoteContent(child: widget.data.repostTo!).padding(
|
||||||
|
top: 4,
|
||||||
|
bottom: widget.data.preload?.attachments?.isNotEmpty ??
|
||||||
|
false
|
||||||
|
? 12
|
||||||
|
: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(
|
||||||
|
bottom: widget.showViews || _isTranslated || _isTranslating
|
||||||
|
? 8
|
||||||
|
: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 12, top: 8),
|
||||||
|
),
|
||||||
|
if (displayableAttachments?.isNotEmpty ?? false)
|
||||||
|
AttachmentList(
|
||||||
|
data: displayableAttachments!,
|
||||||
|
bordered: true,
|
||||||
|
maxHeight: widget.showFullPost ? null : 480,
|
||||||
|
minWidth: attachmentSize,
|
||||||
|
maxWidth: attachmentSize,
|
||||||
|
fit: widget.showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||||
|
padding: EdgeInsets.only(left: 12, right: 12),
|
||||||
|
),
|
||||||
|
if (widget.data.preload?.poll != null)
|
||||||
|
StyledWidget(Container(
|
||||||
|
constraints:
|
||||||
|
BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
|
||||||
|
child: PostPoll(poll: widget.data.preload!.poll!),
|
||||||
|
))
|
||||||
|
.padding(
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
top: 12,
|
||||||
|
bottom: 4,
|
||||||
|
)
|
||||||
|
.center(),
|
||||||
|
if (widget.data.body['content'] != null &&
|
||||||
|
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||||
|
LinkPreviewWidget(
|
||||||
|
text: widget.data.body['content'],
|
||||||
|
).padding(left: 12, right: 4),
|
||||||
|
if (widget.showExpandableComments)
|
||||||
|
_PostCommentIntent(
|
||||||
|
data: widget.data,
|
||||||
|
showAvatar: widget.showAvatar,
|
||||||
|
maxWidth: widget.maxWidth ?? double.infinity,
|
||||||
|
).padding(left: 12, right: 12)
|
||||||
|
else
|
||||||
|
_PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth)
|
||||||
|
.padding(left: 12, right: 12),
|
||||||
|
if (widget.showReactions)
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: (widget.maxWidth ?? double.infinity) + 24,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: _PostReactionList(
|
||||||
|
data: widget.data,
|
||||||
|
padding: EdgeInsets.only(left: 12, right: 12),
|
||||||
|
onChanged: _onChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -308,6 +555,15 @@ class _PostItemState extends State<PostItem> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (widget.showCompactAvatar)
|
||||||
|
_PostAvatar(
|
||||||
|
data: widget.data,
|
||||||
|
isCompact: true,
|
||||||
|
),
|
||||||
|
if (widget.showCompactAvatar) const Gap(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _PostContentHeader(
|
child: _PostContentHeader(
|
||||||
isRelativeDate: !widget.showFullPost,
|
isRelativeDate: !widget.showFullPost,
|
||||||
@@ -315,6 +571,9 @@ class _PostItemState extends State<PostItem> {
|
|||||||
data: widget.data,
|
data: widget.data,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
_PostActionPopup(
|
_PostActionPopup(
|
||||||
data: widget.data,
|
data: widget.data,
|
||||||
isAuthor: isAuthor,
|
isAuthor: isAuthor,
|
||||||
@@ -329,7 +588,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
).padding(bottom: widget.showCompactAvatar ? 4 : 0),
|
||||||
if (widget.data.preload?.thumbnail != null)
|
if (widget.data.preload?.thumbnail != null)
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
@@ -389,15 +648,6 @@ class _PostItemState extends State<PostItem> {
|
|||||||
isEnlarge: widget.data.type == 'article' &&
|
isEnlarge: widget.data.type == 'article' &&
|
||||||
widget.showFullPost,
|
widget.showFullPost,
|
||||||
).padding(bottom: 6),
|
).padding(bottom: 6),
|
||||||
if (widget.data.repostTo != null)
|
|
||||||
_PostQuoteContent(child: widget.data.repostTo!)
|
|
||||||
.padding(
|
|
||||||
bottom:
|
|
||||||
widget.data.preload?.attachments?.isNotEmpty ??
|
|
||||||
false
|
|
||||||
? 12
|
|
||||||
: 0,
|
|
||||||
),
|
|
||||||
if (widget.data.visibility > 0)
|
if (widget.data.visibility > 0)
|
||||||
_PostVisibilityHint(data: widget.data).padding(
|
_PostVisibilityHint(data: widget.data).padding(
|
||||||
vertical: 4,
|
vertical: 4,
|
||||||
@@ -462,9 +712,22 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(
|
).padding(
|
||||||
bottom: widget.showViews ||
|
bottom: (widget.showViews ||
|
||||||
_isTranslated ||
|
_isTranslated ||
|
||||||
_isTranslating
|
_isTranslating) &&
|
||||||
|
(widget.data.repostTo != null ||
|
||||||
|
(widget.data.preload?.attachments
|
||||||
|
?.isNotEmpty ??
|
||||||
|
false))
|
||||||
|
? 8
|
||||||
|
: 0,
|
||||||
|
),
|
||||||
|
if (widget.data.repostTo != null)
|
||||||
|
_PostQuoteContent(child: widget.data.repostTo!)
|
||||||
|
.padding(
|
||||||
|
bottom:
|
||||||
|
(widget.data.preload?.attachments?.isNotEmpty ??
|
||||||
|
false)
|
||||||
? 8
|
? 8
|
||||||
: 0,
|
: 0,
|
||||||
),
|
),
|
||||||
@@ -502,21 +765,30 @@ class _PostItemState extends State<PostItem> {
|
|||||||
if (widget.showExpandableComments)
|
if (widget.showExpandableComments)
|
||||||
_PostCommentIntent(
|
_PostCommentIntent(
|
||||||
data: widget.data,
|
data: widget.data,
|
||||||
|
maxWidth: (widget.maxWidth ?? double.infinity) -
|
||||||
|
(widget.showAvatar ? 72 : 24),
|
||||||
showAvatar: widget.showAvatar,
|
showAvatar: widget.showAvatar,
|
||||||
).padding(left: widget.showAvatar ? 60 : 12, right: 12)
|
).padding(left: widget.showAvatar ? 60 : 12, right: 12)
|
||||||
else
|
else if (widget.showComments)
|
||||||
_PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth)
|
_PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth)
|
||||||
.padding(left: widget.showAvatar ? 60 : 12, right: 12),
|
.padding(left: widget.showAvatar ? 60 : 12, right: 12),
|
||||||
if (widget.showReactions)
|
if (widget.showReactions)
|
||||||
Padding(
|
Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: widget.maxWidth ?? double.infinity,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: _PostReactionList(
|
child: _PostReactionList(
|
||||||
data: widget.data,
|
data: widget.data,
|
||||||
padding:
|
padding: EdgeInsets.only(
|
||||||
EdgeInsets.only(left: widget.showAvatar ? 60 : 12, right: 12),
|
left: widget.showAvatar ? 60 : 12,
|
||||||
|
right: 12,
|
||||||
|
),
|
||||||
onChanged: _onChanged,
|
onChanged: _onChanged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1299,19 +1571,24 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
if (isCompact) {
|
if (isCompact) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text(data.publisher.nick).bold(),
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
data.publisher.nick,
|
||||||
|
maxLines: 1,
|
||||||
|
).bold(),
|
||||||
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Row(
|
Flexible(
|
||||||
children: [
|
child: Text(
|
||||||
Text(
|
|
||||||
isRelativeDate
|
isRelativeDate
|
||||||
? RelativeTime(context)
|
? RelativeTime(context)
|
||||||
.format((data.publishedAt ?? data.createdAt).toLocal())
|
.format((data.publishedAt ?? data.createdAt).toLocal())
|
||||||
: DateFormat('y/M/d HH:mm')
|
: DateFormat('y/M/d HH:mm')
|
||||||
.format((data.publishedAt ?? data.createdAt).toLocal()),
|
.format((data.publishedAt ?? data.createdAt).toLocal()),
|
||||||
).fontSize(13),
|
maxLines: 1,
|
||||||
],
|
overflow: TextOverflow.fade,
|
||||||
).opacity(0.8),
|
).fontSize(13).opacity(0.8),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -1330,7 +1607,10 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('@${data.publisher.name}').fontSize(13),
|
Text(
|
||||||
|
'@${data.publisher.name}',
|
||||||
|
maxLines: 1,
|
||||||
|
).fontSize(13),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
isRelativeDate
|
isRelativeDate
|
||||||
@@ -1338,6 +1618,8 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
.format((data.publishedAt ?? data.createdAt).toLocal())
|
.format((data.publishedAt ?? data.createdAt).toLocal())
|
||||||
: DateFormat('y/M/d HH:mm')
|
: DateFormat('y/M/d HH:mm')
|
||||||
.format((data.publishedAt ?? data.createdAt).toLocal()),
|
.format((data.publishedAt ?? data.createdAt).toLocal()),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
],
|
],
|
||||||
).opacity(0.8),
|
).opacity(0.8),
|
||||||
@@ -1403,10 +1685,19 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_PostAvatar(
|
||||||
|
data: child,
|
||||||
|
isCompact: true,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
_PostContentHeader(
|
_PostContentHeader(
|
||||||
data: child,
|
data: child,
|
||||||
isCompact: true,
|
isCompact: true,
|
||||||
isRelativeDate: isRelativeDate,
|
isRelativeDate: isRelativeDate,
|
||||||
|
),
|
||||||
|
],
|
||||||
).padding(bottom: 4),
|
).padding(bottom: 4),
|
||||||
_PostContentBody(
|
_PostContentBody(
|
||||||
data: child,
|
data: child,
|
||||||
@@ -1594,7 +1885,12 @@ class _PostTruncatedHint extends StatelessWidget {
|
|||||||
class _PostCommentIntent extends StatefulWidget {
|
class _PostCommentIntent extends StatefulWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
final bool showAvatar;
|
final bool showAvatar;
|
||||||
const _PostCommentIntent({required this.data, this.showAvatar = false});
|
final double maxWidth;
|
||||||
|
const _PostCommentIntent({
|
||||||
|
required this.data,
|
||||||
|
this.showAvatar = false,
|
||||||
|
required this.maxWidth,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_PostCommentIntent> createState() => _PostCommentIntentState();
|
State<_PostCommentIntent> createState() => _PostCommentIntentState();
|
||||||
@@ -1633,26 +1929,40 @@ class _PostCommentIntentState extends State<_PostCommentIntent> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: widget.maxWidth),
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (_comments.isNotEmpty)
|
if (_comments.isNotEmpty)
|
||||||
Card(
|
Card(
|
||||||
|
elevation: 4,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: Column(
|
child: Column(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
for (final ele in _comments)
|
for (final ele in _comments)
|
||||||
PostItem(
|
InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: PostItem(
|
||||||
data: ele,
|
data: ele,
|
||||||
showAvatar: false,
|
showAvatar: false,
|
||||||
|
showCompactAvatar: true,
|
||||||
showExpandableComments: true,
|
showExpandableComments: true,
|
||||||
showReactions: false,
|
showReactions: false,
|
||||||
showViews: false,
|
showViews: false,
|
||||||
maxWidth: double.infinity,
|
maxWidth: double.infinity,
|
||||||
).padding(vertical: 8, left: 6),
|
).padding(vertical: 8, left: 6),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'postDetail',
|
||||||
|
pathParameters: {'slug': ele.id.toString()},
|
||||||
|
extra: ele,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).padding(bottom: 8),
|
).padding(vertical: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Transform.flip(
|
Transform.flip(
|
||||||
@@ -1660,7 +1970,8 @@ class _PostCommentIntentState extends State<_PostCommentIntent> {
|
|||||||
child: const Icon(Symbols.comment, size: 20),
|
child: const Icon(Symbols.comment, size: 20),
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text('postCommentsDetailed'.plural(widget.data.metric.replyCount)),
|
Text(
|
||||||
|
'postCommentsDetailed'.plural(widget.data.metric.replyCount)),
|
||||||
if (widget.data.metric.replyCount > 0 && !_isAllLoaded)
|
if (widget.data.metric.replyCount > 0 && !_isAllLoaded)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
@@ -1680,6 +1991,7 @@ class _PostCommentIntentState extends State<_PostCommentIntent> {
|
|||||||
],
|
],
|
||||||
).opacity(0.75).padding(horizontal: widget.showAvatar ? 4 : 0),
|
).opacity(0.75).padding(horizontal: widget.showAvatar ? 4 : 0),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:ui';
|
|||||||
import 'package:croppy/croppy.dart';
|
import 'package:croppy/croppy.dart';
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -491,6 +492,14 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _selectFile() async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(type: FileType.any);
|
||||||
|
if (result == null) return;
|
||||||
|
onAdd(
|
||||||
|
result.files.map((e) => PostWriteMedia.fromFile(e.xFile)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _pasteMedia() async {
|
void _pasteMedia() async {
|
||||||
final imageBytes = await Pasteboard.image;
|
final imageBytes = await Pasteboard.image;
|
||||||
if (imageBytes == null) return;
|
if (imageBytes == null) return;
|
||||||
@@ -605,6 +614,18 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
_selectMedia();
|
_selectMedia();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.file_upload),
|
||||||
|
const Gap(16),
|
||||||
|
Text('addAttachmentFromFiles').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_selectFile();
|
||||||
|
},
|
||||||
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/screens/post/post_editor.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
@@ -17,8 +18,10 @@ import 'package:surface/widgets/loading_indicator.dart';
|
|||||||
class PostMiniEditor extends StatefulWidget {
|
class PostMiniEditor extends StatefulWidget {
|
||||||
final int? postReplyId;
|
final int? postReplyId;
|
||||||
final Function? onPost;
|
final Function? onPost;
|
||||||
|
final Function? onExpand;
|
||||||
|
|
||||||
const PostMiniEditor({super.key, this.postReplyId, this.onPost});
|
const PostMiniEditor(
|
||||||
|
{super.key, this.postReplyId, this.onPost, this.onExpand});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PostMiniEditor> createState() => _PostMiniEditorState();
|
State<PostMiniEditor> createState() => _PostMiniEditorState();
|
||||||
@@ -214,12 +217,16 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postEditor',
|
'postEditor',
|
||||||
|
extra: PostEditorExtra(
|
||||||
|
text: _writeController.contentController.text,
|
||||||
|
),
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
if (widget.postReplyId != null)
|
if (widget.postReplyId != null)
|
||||||
'replying': widget.postReplyId.toString(),
|
'replying': widget.postReplyId.toString(),
|
||||||
'mode': 'stories',
|
'mode': 'stories',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
widget.onExpand?.call();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
|
|||||||
@@ -102,6 +102,6 @@ static void my_application_init(MyApplication* self) {}
|
|||||||
MyApplication* my_application_new() {
|
MyApplication* my_application_new() {
|
||||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||||
"application-id", APPLICATION_ID,
|
"application-id", APPLICATION_ID,
|
||||||
"flags", G_APPLICATION_NON_UNIQUE,
|
"flags", G_APPLICATION_DEFAULT_FLAGS,
|
||||||
nullptr));
|
nullptr));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2.4.2+80
|
version: 2.4.2+83
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:drift/internal/migrations.dart';
|
|||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
import 'schema_v3.dart' as v3;
|
import 'schema_v3.dart' as v3;
|
||||||
|
import 'schema_v4.dart' as v4;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -17,10 +18,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v2.DatabaseAtV2(db);
|
return v2.DatabaseAtV2(db);
|
||||||
case 3:
|
case 3:
|
||||||
return v3.DatabaseAtV3(db);
|
return v3.DatabaseAtV3(db);
|
||||||
|
case 4:
|
||||||
|
return v4.DatabaseAtV4(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2, 3];
|
static const versions = const [1, 2, 3, 4];
|
||||||
}
|
}
|
||||||
|
|||||||
2391
test/drift/my_database/generated/schema_v4.dart
Normal file
2391
test/drift/my_database/generated/schema_v4.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,29 @@ auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
|
|||||||
#include "flutter_window.h"
|
#include "flutter_window.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
|
HANDLE g_hMutex = NULL;
|
||||||
|
|
||||||
|
bool CheckIfAlreadyRunning() {
|
||||||
|
g_hMutex = CreateMutex(NULL, FALSE, L"Global\\SolianDesktop");
|
||||||
|
|
||||||
|
if (g_hMutex == NULL) {
|
||||||
|
return true; // Mutex creation failed
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||||
|
CloseHandle(g_hMutex);
|
||||||
|
return true; // Another instance is running
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||||
|
if (CheckIfAlreadyRunning()) {
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
// Attach to console when present (e.g., 'flutter run') or create a
|
// Attach to console when present (e.g., 'flutter run') or create a
|
||||||
// new console when running with a debugger.
|
// new console when running with a debugger.
|
||||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user