Compare commits

...

11 Commits

Author SHA1 Message Date
619c90cdd9 Setting attachment thumbnail 2024-12-26 00:02:25 +08:00
168d51c9fe 📝 Add api docs 2024-12-25 00:48:25 +08:00
d4b831f98e Copy, linking attachment RID 2024-12-25 00:48:19 +08:00
4d96a15c31 🐛 Fix context menu mis placed on device which showing the side navigation 2024-12-24 23:07:47 +08:00
06dd3e092a 🚀 Launch 2.1.1+39 2024-12-23 23:08:07 +08:00
82fe9e287a 🐛 Bug fixes on special days 2024-12-23 23:02:47 +08:00
dc1c285de1 🍱 Add more special days 2024-12-23 22:55:03 +08:00
5a3313e94f Days countdown 2024-12-23 22:42:10 +08:00
61032c84f1 🐛 Scale down user image on ios notification extensions 2024-12-23 22:02:29 +08:00
36a5b8fb39 🐛 Bug fixes on something 2024-12-23 21:55:07 +08:00
3eda464e03 🐛 Fix search post did not triggered 2024-12-22 20:28:53 +08:00
28 changed files with 970 additions and 461 deletions

View File

@ -0,0 +1,30 @@
meta {
name: Developer Notify All Users
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/id/dev/notify/all
body: json
auth: bearer
}
auth:bearer {
token: {{atk}}
}
body:json {
{
"client_id": "{{third_client_id}}",
"client_secret":"{{third_client_tk}}",
"type": "general",
"subject": "Merry Christmas!",
"subtitle": "一条来自 Solar Network 团队的信息",
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
"metadata": {
"image": "6EqsYQwmFRCkbmhR"
},
"priority": 10
}
}

9
api/bruno.json Normal file
View File

@ -0,0 +1,9 @@
{
"version": "1",
"name": "Solar Network",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@ -0,0 +1,8 @@
vars {
endpoint: https://api.sn.solsynth.dev
third_client_id: alphabot
}
vars:secret [
atk,
third_client_tk
]

View File

@ -281,16 +281,22 @@
"one": "{} attachment", "one": "{} attachment",
"other": "{} attachments" "other": "{} attachments"
}, },
"fieldAttachmentRandomId": "Random ID",
"addAttachmentFromAlbum": "Add from album", "addAttachmentFromAlbum": "Add from album",
"addAttachmentFromClipboard": "Paste file", "addAttachmentFromClipboard": "Paste file",
"addAttachmentFromCameraPhoto": "Take photo", "addAttachmentFromCameraPhoto": "Take photo",
"addAttachmentFromCameraVideo": "Take video", "addAttachmentFromCameraVideo": "Take video",
"addAttachmentFromRandomId": "Link via RID",
"attachmentPastedImage": "Pasted Image", "attachmentPastedImage": "Pasted Image",
"attachmentInsertLink": "Insert Link", "attachmentInsertLink": "Insert Link",
"attachmentSetAsPostThumbnail": "Set as post thumbnail", "attachmentSetAsPostThumbnail": "Set as post thumbnail",
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail", "attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
"attachmentSetThumbnail": "Set thumbnail", "attachmentSetThumbnail": "Set thumbnail",
"attachmentCopyRandomId": "Copy RID",
"attachmentUpload": "Upload", "attachmentUpload": "Upload",
"attachmentInputDialog": "Upload attachments",
"attachmentInputUseRandomId": "Use Random ID",
"attachmentInputNew": "New Upload",
"notification": "Notification", "notification": "Notification",
"notificationUnreadCount": { "notificationUnreadCount": {
"zero": "All notifications read", "zero": "All notifications read",
@ -378,9 +384,26 @@
"dailyCheckNegativeHint5Description": "Lost connection at a crucial moment", "dailyCheckNegativeHint5Description": "Lost connection at a crucial moment",
"dailyCheckNegativeHint6": "Going out", "dailyCheckNegativeHint6": "Going out",
"dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain", "dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain",
"happyBirthday": "Happy birthday, {}!", "celebrateBirthday": "Happy birthday, {}!",
"celebrateMerryXmas": "Merry christmas, {}", "celebrateMerryXmas": "Merry christmas, {}",
"celebrateNewYear": "Happy new year, {}", "celebrateNewYear": "Happy new year, {}",
"celebrateValentineDay": "Today is valentine's day, {}!",
"celebrateLaborDay": "Today is labor day, {}.",
"celebrateMotherDay": "Today is mother's day, {}.",
"celebrateChildrenDay": "Today is children's day, {}!",
"celebrateFatherDay": "Today is father's day, {}.",
"celebrateHalloween": "Happy halloween, {}!",
"celebrateThanksgiving": "Today is thanksgiving day, {}!",
"pendingBirthday": "Birthday in {}",
"pendingMerryXmas": "Christmas in {}",
"pendingNewYear": "New year in {}",
"pendingValentineDay": "Valentine's day in {}",
"pendingLaborDay": "Labor day in {}",
"pendingMotherDay": "Mother's day in {}",
"pendingChildrenDay": "Children's day in {}",
"pendingFatherDay": "Father's day in {}",
"pendingHalloween": "Halloween in {}",
"pendingThanksgiving": "Thanksgiving day in {}",
"friendNew": "Add Friend", "friendNew": "Add Friend",
"friendRequests": "Friend Requests", "friendRequests": "Friend Requests",
"friendRequestsDescription": { "friendRequestsDescription": {
@ -488,5 +511,7 @@
"postCategoryNews": "News", "postCategoryNews": "News",
"postCategoryKnowledge": "Knowledge", "postCategoryKnowledge": "Knowledge",
"postCategoryLiterature": "Literature", "postCategoryLiterature": "Literature",
"postCategoryUncategorized": "Uncategorized" "postCategoryFunny": "Funny",
"postCategoryUncategorized": "Uncategorized",
"waitingForUpload": "Waiting for upload"
} }

View File

@ -279,16 +279,22 @@
"one": "{} 个附件", "one": "{} 个附件",
"other": "{} 个附件" "other": "{} 个附件"
}, },
"fieldAttachmentRandomId": "访问 ID",
"addAttachmentFromAlbum": "从相册中添加附件", "addAttachmentFromAlbum": "从相册中添加附件",
"addAttachmentFromClipboard": "粘贴附件", "addAttachmentFromClipboard": "粘贴附件",
"addAttachmentFromCameraPhoto": "拍摄照片", "addAttachmentFromCameraPhoto": "拍摄照片",
"addAttachmentFromCameraVideo": "拍摄视频", "addAttachmentFromCameraVideo": "拍摄视频",
"addAttachmentFromRandomId": "通过访问 ID 链接",
"attachmentPastedImage": "粘贴的图片", "attachmentPastedImage": "粘贴的图片",
"attachmentInsertLink": "插入连接", "attachmentInsertLink": "插入连接",
"attachmentSetAsPostThumbnail": "设置为帖子缩略图", "attachmentSetAsPostThumbnail": "设置为帖子缩略图",
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图", "attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
"attachmentSetThumbnail": "设置缩略图", "attachmentSetThumbnail": "设置缩略图",
"attachmentCopyRandomId": "复制访问 ID",
"attachmentUpload": "上传", "attachmentUpload": "上传",
"attachmentInputDialog": "上传附件",
"attachmentInputUseRandomId": "使用访问 ID",
"attachmentInputNew": "新上传附件",
"notification": "通知", "notification": "通知",
"notificationUnreadCount": { "notificationUnreadCount": {
"zero": "无未读通知", "zero": "无未读通知",
@ -376,9 +382,26 @@
"dailyCheckNegativeHint5Description": "关键时刻断网", "dailyCheckNegativeHint5Description": "关键时刻断网",
"dailyCheckNegativeHint6": "出门", "dailyCheckNegativeHint6": "出门",
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨", "dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
"happyBirthday": "生日快乐,{}", "celebrateBirthday": "生日快乐,{}",
"celebrateMerryXmas": "圣诞快乐,{}", "celebrateMerryXmas": "圣诞快乐,{}",
"celebrateNewYear": "新年快乐,{}", "celebrateNewYear": "新年快乐,{}",
"celebrateValentineDay": "今天是情人节,{}",
"celebrateLaborDay": "今天是劳动节,{}。",
"celebrateMotherDay": "今天是母亲节,{}。",
"celebrateChildrenDay": "今天是儿童节,{}",
"celebrateFatherDay": "今天是父亲节,{}。",
"celebrateHalloween": "快乐在圣诞节,{}",
"celebrateThanksgiving": "今天是感恩节,{}",
"pendingBirthday": "{} 过生日",
"pendingMerryXmas": "{} 过圣诞节",
"pendingNewYear": "{} 跨年",
"pendingValentineDay": "{} 过情人节",
"pendingLaborDay": "{} 过劳动节",
"pendingMotherDay": "{} 过母亲节",
"pendingChildrenDay": "{} 过儿童节",
"pendingFatherDay": "{} 过父亲节",
"pendingHalloween": "{} 过圣诞节",
"pendingThanksgiving": "{} 过感恩节",
"friendNew": "添加好友", "friendNew": "添加好友",
"friendRequests": "好友请求", "friendRequests": "好友请求",
"friendRequestsDescription": { "friendRequestsDescription": {
@ -486,5 +509,7 @@
"postCategoryNews": "新闻", "postCategoryNews": "新闻",
"postCategoryKnowledge": "知识", "postCategoryKnowledge": "知识",
"postCategoryLiterature": "文学", "postCategoryLiterature": "文学",
"postCategoryUncategorized": "未分类" "postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分类",
"waitingForUpload": "等待上传"
} }

View File

@ -376,9 +376,26 @@
"dailyCheckNegativeHint5Description": "關鍵時刻斷網", "dailyCheckNegativeHint5Description": "關鍵時刻斷網",
"dailyCheckNegativeHint6": "出門", "dailyCheckNegativeHint6": "出門",
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
"happyBirthday": "生日快樂,{}", "celebrateBirthday": "生日快樂,{}",
"celebrateMerryXmas": "聖誕快樂,{}", "celebrateMerryXmas": "聖誕快樂,{}",
"celebrateNewYear": "新年快樂,{}", "celebrateNewYear": "新年快樂,{}",
"celebrateValentineDay": "今天是情人節,{}",
"celebrateLaborDay": "今天是勞動節,{}。",
"celebrateMotherDay": "今天是母親節,{}。",
"celebrateChildrenDay": "今天是兒童節,{}",
"celebrateFatherDay": "今天是父親節,{}。",
"celebrateHalloween": "快樂在聖誕節,{}",
"celebrateThanksgiving": "今天是感恩節,{}",
"pendingBirthday": "{} 過生日",
"pendingMerryXmas": "{} 過聖誕節",
"pendingNewYear": "{} 跨年",
"pendingValentineDay": "{} 過情人節",
"pendingLaborDay": "{} 過勞動節",
"pendingMotherDay": "{} 過母親節",
"pendingChildrenDay": "{} 過兒童節",
"pendingFatherDay": "{} 過父親節",
"pendingHalloween": "{} 過聖誕節",
"pendingThanksgiving": "{} 過感恩節",
"friendNew": "添加好友", "friendNew": "添加好友",
"friendRequests": "好友請求", "friendRequests": "好友請求",
"friendRequestsDescription": { "friendRequestsDescription": {
@ -486,5 +503,6 @@
"postCategoryNews": "新聞", "postCategoryNews": "新聞",
"postCategoryKnowledge": "知識", "postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學", "postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類" "postCategoryUncategorized": "未分類"
} }

View File

@ -376,9 +376,26 @@
"dailyCheckNegativeHint5Description": "關鍵時刻斷網", "dailyCheckNegativeHint5Description": "關鍵時刻斷網",
"dailyCheckNegativeHint6": "出門", "dailyCheckNegativeHint6": "出門",
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
"happyBirthday": "生日快樂,{}", "celebrateBirthday": "生日快樂,{}",
"celebrateMerryXmas": "聖誕快樂,{}", "celebrateMerryXmas": "聖誕快樂,{}",
"celebrateNewYear": "新年快樂,{}", "celebrateNewYear": "新年快樂,{}",
"celebrateValentineDay": "今天是情人節,{}",
"celebrateLaborDay": "今天是勞動節,{}。",
"celebrateMotherDay": "今天是母親節,{}。",
"celebrateChildrenDay": "今天是兒童節,{}",
"celebrateFatherDay": "今天是父親節,{}。",
"celebrateHalloween": "快樂在聖誕節,{}",
"celebrateThanksgiving": "今天是感恩節,{}",
"pendingBirthday": "{} 過生日",
"pendingMerryXmas": "{} 過聖誕節",
"pendingNewYear": "{} 跨年",
"pendingValentineDay": "{} 過情人節",
"pendingLaborDay": "{} 過勞動節",
"pendingMotherDay": "{} 過母親節",
"pendingChildrenDay": "{} 過兒童節",
"pendingFatherDay": "{} 過父親節",
"pendingHalloween": "{} 過聖誕節",
"pendingThanksgiving": "{} 過感恩節",
"friendNew": "新增好友", "friendNew": "新增好友",
"friendRequests": "好友請求", "friendRequests": "好友請求",
"friendRequestsDescription": { "friendRequestsDescription": {
@ -486,5 +503,6 @@
"postCategoryNews": "新聞", "postCategoryNews": "新聞",
"postCategoryKnowledge": "知識", "postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學", "postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類" "postCategoryUncategorized": "未分類"
} }

View File

@ -173,7 +173,7 @@ PODS:
- in_app_review (2.0.0): - in_app_review (2.0.0):
- Flutter - Flutter
- Kingfisher (8.1.3) - Kingfisher (8.1.3)
- livekit_client (2.3.2): - livekit_client (2.3.3):
- Flutter - Flutter
- flutter_webrtc - flutter_webrtc
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
@ -386,7 +386,7 @@ SPEC CHECKSUMS:
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
livekit_client: 6108dad8b77db3142bafd4c630f471d0a54335cd livekit_client: 02cf2cc4357a655af12ccee70ff5596ae4e6feef
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e

View File

@ -80,7 +80,11 @@ class NotificationService: UNNotificationServiceExtension {
let metadataCopy = metadata as? [String: String] ?? [:] let metadataCopy = metadata as? [String: String] ?? [:]
let avatarUrl = getAttachmentUrl(for: avatarIdentifier) let avatarUrl = getAttachmentUrl(for: avatarIdentifier)
KingfisherManager.shared.retrieveImage(with: URL(string: avatarUrl)!, completionHandler: { result in
let targetSize = 640
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
KingfisherManager.shared.retrieveImage(with: URL(string: avatarUrl)!, options: [.processor(scaleProcessor)], completionHandler: { result in
var image: Data? var image: Data?
switch result { switch result {
case .success(let value): case .success(let value):

View File

@ -30,6 +30,7 @@ import 'package:surface/providers/post.dart';
import 'package:surface/providers/relationship.dart'; import 'package:surface/providers/relationship.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/theme.dart'; import 'package:surface/providers/theme.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
@ -148,6 +149,9 @@ class SolianApp extends StatelessWidget {
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
// Additional helper layer
Provider(create: (ctx) => SpecialDayProvider(ctx)),
], ],
child: _AppDelegate(), child: _AppDelegate(),
), ),
@ -265,6 +269,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
// 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
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.initializeUserAgent(); await sn.initializeUserAgent();
await sn.setConfigWithNative();
if (!mounted) return; if (!mounted) return;
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
await ua.initialize(); await ua.initialize();

View File

@ -215,4 +215,18 @@ class SnAttachmentProvider {
return place; return place;
} }
Future<SnAttachment> updateOne(
int id,
String alt, {
required Map<String, dynamic> metadata,
bool isMature = false,
}) async {
final resp = await _sn.client.put('/cgi/uc/attachments/$id', data: {
'alt': alt,
'metadata': metadata,
'is_mature': isMature,
});
return SnAttachment.fromJson(resp.data);
}
} }

View File

@ -68,9 +68,8 @@ class SnNetworkProvider {
_config.initialize().then((_) { _config.initialize().then((_) {
_prefs = _config.prefs; _prefs = _config.prefs;
client.options.baseUrl = _config.serverUrl; client.options.baseUrl = _config.serverUrl;
if (!context.mounted) return;
_home.saveWidgetData("nex_server_url", client.options.baseUrl);
}); });
} }
static Future<Dio> createOffContextClient() async { static Future<Dio> createOffContextClient() async {
@ -109,6 +108,10 @@ class SnNetworkProvider {
return client; return client;
} }
Future<void> setConfigWithNative() async {
_home.saveWidgetData("nex_server_url", client.options.baseUrl);
}
static Future<String> _getUserAgent() async { static Future<String> _getUserAgent() async {
final String platformInfo; final String platformInfo;
if (kIsWeb) { if (kIsWeb) {

View File

@ -0,0 +1,136 @@
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/userinfo.dart';
// Stored as key: month, day
const Map<String, (int, int)> kSpecialDays = {
// Birthday is dynamically generated according to the user's profile
'NewYear': (1, 1),
'ValentineDay': (2, 14),
'LaborDay': (5, 1),
'MotherDay': (5, 11),
'ChildrenDay': (6, 1),
'FatherDay': (8, 8),
'Halloween': (10, 31),
'Thanksgiving': (11, 28),
'MerryXmas': (12, 25),
};
const Map<String, String> kSpecialDaysSymbol = {
'Birthday': '🎂',
'NewYear': '🎉',
'MerryXmas': '🎄',
'ValentineDay': '💑',
'LaborDay': '🏋️',
'MotherDay': '👩',
'ChildrenDay': '👶',
'FatherDay': '👨',
'Halloween': '🎃',
'Thanksgiving': '🎅',
};
class SpecialDayProvider {
late final UserProvider _user;
SpecialDayProvider(BuildContext context) {
_user = context.read<UserProvider>();
}
List<String> getSpecialDays() {
final now = DateTime.now().toLocal();
final birthday = _user.user?.profile?.birthday?.toLocal();
final isBirthday = birthday != null && birthday.day == now.day && birthday.month == now.month;
return [
if (isBirthday) 'Birthday',
...kSpecialDays.keys.where(
(key) => kSpecialDays[key]!.$1 == now.month && kSpecialDays[key]!.$2 == now.day,
),
];
}
(String, DateTime)? getLastSpecialDay() {
final now = DateTime.now().toLocal();
final birthday = _user.user?.profile?.birthday?.toLocal();
final Map<String, (int, int)> specialDays = {
if (birthday != null) 'Birthday': (birthday.month, birthday.day),
...kSpecialDays,
};
DateTime? lastDate;
String? lastEvent;
for (final entry in specialDays.entries) {
final eventName = entry.key;
final (month, day) = entry.value;
var specialDayThisYear = DateTime(now.year, month, day);
var specialDayLastYear = DateTime(now.year - 1, month, day);
if (specialDayThisYear.isBefore(now)) {
if (lastDate == null || specialDayThisYear.isAfter(lastDate)) {
lastDate = specialDayThisYear;
lastEvent = eventName;
}
} else if (specialDayLastYear.isBefore(now)) {
if (lastDate == null || specialDayLastYear.isAfter(lastDate)) {
lastDate = specialDayLastYear;
lastEvent = eventName;
}
}
}
if (lastEvent != null && lastDate != null) {
return (lastEvent, lastDate);
}
return null;
}
(String, DateTime)? getNextSpecialDay() {
final now = DateTime.now().toLocal();
final birthday = _user.user?.profile?.birthday?.toLocal();
// Stored as key: month, day
final Map<String, (int, int)> specialDays = {
if (birthday != null) 'Birthday': (birthday.month, birthday.day),
...kSpecialDays,
};
DateTime? closestDate;
String? closestEvent;
for (final entry in specialDays.entries) {
final eventName = entry.key;
final (month, day) = entry.value;
// Calculate the special day's DateTime in the current year
var specialDay = DateTime(now.year, month, day);
// If the special day has already passed this year, consider it for the next year
if (specialDay.isBefore(now)) {
specialDay = DateTime(now.year + 1, month, day);
}
// Check if this special day is closer than the previously found one
if (closestDate == null || specialDay.isBefore(closestDate)) {
closestDate = specialDay;
closestEvent = eventName;
}
}
if (closestEvent != null && closestDate != null) {
return (closestEvent, closestDate);
}
// No special day found
return null;
}
double getSpecialDayProgress(DateTime last, DateTime next) {
final totalDuration = next.difference(last).inSeconds.toDouble();
final elapsedDuration = DateTime.now().difference(last).inSeconds.toDouble();
return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
}
}

View File

@ -31,10 +31,10 @@ class UserProvider extends ChangeNotifier {
final value = _config.prefs.getString(kAtkStoreKey); final value = _config.prefs.getString(kAtkStoreKey);
isAuthorized = value != null; isAuthorized = value != null;
notifyListeners(); notifyListeners();
refreshUser().then((value) { refreshUser().then((value) async {
if (value != null) { if (value != null) {
log('Logged in as @${value.name}'); log('Logged in as @${value.name}');
_home.saveWidgetData('user', value.toJson()); log('Atk: ${await atk}');
} }
}); });
} }

View File

@ -13,7 +13,7 @@ class HomeWidgetProvider {
Future<void> initialize() async { Future<void> initialize() async {
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return; if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return;
if (!kIsWeb && Platform.isIOS) { if (Platform.isIOS) {
await HomeWidget.setAppGroupId("group.solsynth.solian"); await HomeWidget.setAppGroupId("group.solsynth.solian");
} }
} }

View File

@ -22,8 +22,9 @@ const Map<String, IconData> kCategoryIcons = {
'sports': Symbols.sports_soccer, 'sports': Symbols.sports_soccer,
'music': Symbols.music_note, 'music': Symbols.music_note,
'news': Symbols.newspaper, 'news': Symbols.newspaper,
'knowledge': Symbols.book, 'knowledge': Symbols.library_books,
'literature': Symbols.book, 'literature': Symbols.book,
'funny': Symbols.attractions,
}; };
class ExploreScreen extends StatefulWidget { class ExploreScreen extends StatefulWidget {
@ -184,12 +185,12 @@ class _ExploreScreenState extends State<ExploreScreen> {
preferredSize: const Size.fromHeight(50), preferredSize: const Size.fromHeight(50),
child: SizedBox( child: SizedBox(
height: 50, height: 50,
child: ListView.builder( child: SingleChildScrollView(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: _categories.length, padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12),
itemBuilder: (context, idx) { child: Row(
final ele = _categories[idx]; mainAxisAlignment: MainAxisAlignment.center,
children: _categories.map((ele) {
return StyledWidget(ChoiceChip( return StyledWidget(ChoiceChip(
avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark), avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark),
label: Text( label: Text(
@ -203,7 +204,8 @@ class _ExploreScreenState extends State<ExploreScreen> {
_refreshPosts(); _refreshPosts();
}, },
)).padding(horizontal: 4); )).padding(horizontal: 4);
}, }).toList(),
),
), ),
), ),
), ),

View File

@ -11,11 +11,13 @@ import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/widget.dart'; import 'package:surface/providers/widget.dart';
import 'package:surface/types/check_in.dart'; import 'package:surface/types/check_in.dart';
@ -79,8 +81,8 @@ class _HomeScreenState extends State<HomeScreen> {
child: Column( child: Column(
mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start, mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [ children: [
_HomeDashSpecialDayWidget().padding(bottom: 8, horizontal: 8),
_HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)), _HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)),
_HomeDashSpecialDayWidget().padding(horizontal: 8),
StaggeredGrid.extent( StaggeredGrid.extent(
maxCrossAxisExtent: 280, maxCrossAxisExtent: 280,
mainAxisSpacing: 8, mainAxisSpacing: 8,
@ -156,36 +158,59 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
final today = DateTime.now(); final dayz = context.watch<SpecialDayProvider>();
final birthday = ua.user?.profile?.birthday?.toLocal();
final isBirthday = birthday != null && birthday.day == today.day && birthday.month == today.month;
final days = dayz.getSpecialDays();
if (days.isNotEmpty) {
return Column( return Column(
spacing: 8, spacing: 8,
children: days.map((ele) {
return Card(
child: ListTile(
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
subtitle: Text(
DateFormat('y/M/d').format(DateTime.now().copyWith(
month: kSpecialDays[ele]!.$1,
day: kSpecialDays[ele]!.$2,
)),
),
),
).padding(bottom: 8);
}).toList());
}
final nextOne = dayz.getNextSpecialDay();
final lastOne = dayz.getLastSpecialDay();
if (nextOne != null && lastOne != null) {
var (name, date) = nextOne;
date = date.add(Duration(days: 1));
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
final diff = nextOne.$2.add(-const Duration(days: 1)).difference(lastOne.$2);
return Card(
child: ListTile(
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
title: Text('pending$name').tr(args: [RelativeTime(context).format(date)]),
subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (isBirthday) Text('${diff.inDays}d · ${(progress * 100).toStringAsFixed(2)}%'),
Card( const Gap(8),
child: ListTile( Expanded(
leading: Text('🎂').fontSize(24), child: LinearProgressIndicator(
title: Text('happyBirthday').tr(args: [ua.user?.nick ?? 'user']), value: progress,
), borderRadius: BorderRadius.circular(8),
).padding(bottom: 8),
if (today.month == 12 && today.day == 25)
Card(
child: ListTile(
leading: Text('🎄').fontSize(24),
title: Text('celebrateMerryXmas').tr(args: [ua.user?.nick ?? 'user']),
),
),
if (today.month == 1 && today.day == 1)
Card(
child: ListTile(
leading: Text('🎉').fontSize(24),
title: Text('celebrateNewYear').tr(args: [ua.user?.nick ?? 'user']),
), ),
), ),
], ],
); ),
),
).padding(bottom: 8);
}
return const SizedBox.shrink();
} }
} }
@ -493,9 +518,7 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
setState(() => _isBusy = true); setState(() => _isBusy = true);
try { try {
final pt = context.read<SnPostContentProvider>(); final pt = context.read<SnPostContentProvider>();
final home = context.read<HomeWidgetProvider>();
_posts = await pt.listRecommendations(); _posts = await pt.listRecommendations();
home.saveWidgetData('post_featured', _posts!.first.toJson());
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);

View File

@ -96,38 +96,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
); );
} }
final _imagePicker = ImagePicker();
void _takeMedia(bool isVideo) async {
final result = isVideo
? await _imagePicker.pickVideo(source: ImageSource.camera)
: await _imagePicker.pickImage(source: ImageSource.camera);
if (result == null) return;
_writeController.addAttachments([
PostWriteMedia.fromFile(result),
]);
}
void _selectMedia() async {
final result = await _imagePicker.pickMultipleMedia();
if (result.isEmpty) return;
_writeController.addAttachments(
result.map((e) => PostWriteMedia.fromFile(e)),
);
}
void _pasteMedia() async {
final imageBytes = await Pasteboard.image;
if (imageBytes == null) return;
_writeController.addAttachments([
PostWriteMedia.fromBytes(
imageBytes,
'attachmentPastedImage'.tr(),
PostWriteMediaType.image,
),
]);
}
@override @override
void dispose() { void dispose() {
_writeController.dispose(); _writeController.dispose();
@ -435,64 +403,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
child: Row( child: Row(
children: [ children: [
PopupMenuButton( AddPostMediaButton(
icon: Icon( onAdd: (items) {
Symbols.add_photo_alternate, setState(() {
color: Theme.of(context).colorScheme.primary, _writeController.addAttachments(items);
), });
itemBuilder: (context) => [
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.photo_camera),
const Gap(16),
Text('addAttachmentFromCameraPhoto').tr(),
],
),
onTap: () {
_takeMedia(false);
}, },
), ),
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.videocam),
const Gap(16),
Text('addAttachmentFromCameraVideo').tr(),
],
),
onTap: () {
_takeMedia(true);
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.photo_library),
const Gap(16),
Text('addAttachmentFromAlbum').tr(),
],
),
onTap: () {
_selectMedia();
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.content_paste),
const Gap(16),
Text('addAttachmentFromClipboard').tr(),
],
),
onTap: () {
_pasteMedia();
},
),
],
),
], ],
), ),
), ),

View File

@ -99,11 +99,16 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
], ],
).padding(horizontal: 24, vertical: 16), ).padding(horizontal: 24, vertical: 16),
).then((_) { ).then((_) {
_posts.clear(); _refreshPosts();
_fetchPosts();
}); });
} }
Future<void> _refreshPosts() {
_postCount = null;
_posts.clear();
return _fetchPosts();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const labelShadows = <Shadow>[ const labelShadows = <Shadow>[
@ -144,8 +149,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
setState(() => _posts[idx] = data); setState(() => _posts[idx] = data);
}, },
onDeleted: () { onDeleted: () {
_posts.clear(); _refreshPosts();
_fetchPosts();
}, },
), ),
onTap: () { onTap: () {
@ -176,10 +180,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
_searchTerm = value; _searchTerm = value;
}, },
onSubmitted: (value) { onSubmitted: (value) {
setState(() => _posts.clear());
_searchTerm = value; _searchTerm = value;
_fetchPosts(); _refreshPosts();
}, },
), ),
if (_lastTook != null) if (_lastTook != null)

View File

@ -0,0 +1,114 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/widgets/dialog.dart';
class AttachmentInputDialog extends StatefulWidget {
final String? title;
const AttachmentInputDialog({super.key, required this.title});
@override
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
}
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
final _randomIdController = TextEditingController();
XFile? _thumbnailFile;
void _pickImage() async {
final picker = ImagePicker();
final result = await picker.pickImage(source: ImageSource.gallery);
if (result == null) return;
setState(() => _thumbnailFile = result);
}
bool _isBusy = false;
void _finishUp() async {
if (_isBusy) return;
setState(() => _isBusy = true);
final attach = context.read<SnAttachmentProvider>();
if (_randomIdController.text.isNotEmpty) {
try {
final attachment = await attach.getOne(_randomIdController.text);
if (!mounted) return;
Navigator.pop(context, attachment);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
} else if (_thumbnailFile != null) {
try {
final attachment = await attach.directUploadOne(
(await _thumbnailFile!.readAsBytes()).buffer.asUint8List(),
_thumbnailFile!.path,
'interactive',
null,
);
if (!mounted) return;
Navigator.pop(context, attachment);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.title ?? 'attachmentInputDialog').tr(),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('attachmentInputUseRandomId').tr().fontSize(14),
const Gap(8),
TextField(
controller: _randomIdController,
decoration: InputDecoration(
labelText: 'fieldAttachmentRandomId'.tr(),
border: const OutlineInputBorder(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(24),
Text('attachmentInputNew').tr().fontSize(14),
Card(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: const Icon(Symbols.add_photo_alternate),
trailing: const Icon(Symbols.chevron_right),
title: Text('addAttachmentFromAlbum').tr(),
subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
onTap: () {
_pickImage();
},
),
),
],
),
actions: [
TextButton(
child: Text('dialogDismiss').tr(),
onPressed: _isBusy ? null : () {
Navigator.pop(context);
},
),
TextButton(
onPressed: _isBusy ? null : () => _finishUp(),
child: Text('dialogConfirm').tr(),
),
],
);
}
}

View File

@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/attachment/attachment_list.dart'; import 'package:surface/widgets/attachment/attachment_list.dart';
import 'package:surface/widgets/context_menu.dart';
import 'package:surface/widgets/link_preview.dart'; import 'package:surface/widgets/link_preview.dart';
import 'package:surface/widgets/markdown_content.dart'; import 'package:surface/widgets/markdown_content.dart';
import 'package:swipe_to/swipe_to.dart'; import 'package:swipe_to/swipe_to.dart';
@ -53,7 +54,7 @@ class ChatMessage extends StatelessWidget {
swipeSensitivity: 20, swipeSensitivity: 20,
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null, onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null, onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null,
child: ContextMenuRegion( child: ContextMenuArea(
contextMenu: ContextMenu( contextMenu: ContextMenu(
entries: [ entries: [
MenuHeader(text: "eventResourceTag".tr(args: ['#${data.id}'])), MenuHeader(text: "eventResourceTag".tr(args: ['#${data.id}'])),

View File

@ -123,40 +123,6 @@ class ChatMessageInputState extends State<ChatMessageInput> {
} }
final List<PostWriteMedia> _attachments = List.empty(growable: true); final List<PostWriteMedia> _attachments = List.empty(growable: true);
final _imagePicker = ImagePicker();
void _takeMedia(bool isVideo) async {
final result = isVideo
? await _imagePicker.pickVideo(source: ImageSource.camera)
: await _imagePicker.pickImage(source: ImageSource.camera);
if (result == null) return;
_attachments.add(
PostWriteMedia.fromFile(result),
);
setState(() {});
}
void _selectMedia() async {
final result = await _imagePicker.pickMultipleMedia();
if (result.isEmpty) return;
_attachments.addAll(
result.map((e) => PostWriteMedia.fromFile(e)),
);
setState(() {});
}
void _pasteMedia() async {
final imageBytes = await Pasteboard.image;
if (imageBytes == null) return;
_attachments.add(
PostWriteMedia.fromBytes(
imageBytes,
'attachmentPastedImage'.tr(),
PostWriteMediaType.image,
),
);
setState(() {});
}
@override @override
void dispose() { void dispose() {
@ -294,64 +260,13 @@ class ChatMessageInputState extends State<ChatMessageInput> {
), ),
), ),
const Gap(8), const Gap(8),
PopupMenuButton( AddPostMediaButton(
icon: Icon( onAdd: (items) {
Symbols.add_photo_alternate, setState(() {
color: Theme.of(context).colorScheme.primary, _attachments.addAll(items);
), });
itemBuilder: (context) => [
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.photo_camera),
const Gap(16),
Text('addAttachmentFromCameraPhoto').tr(),
],
),
onTap: () {
_takeMedia(false);
}, },
), ),
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.videocam),
const Gap(16),
Text('addAttachmentFromCameraVideo').tr(),
],
),
onTap: () {
_takeMedia(true);
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.photo_library),
const Gap(16),
Text('addAttachmentFromAlbum').tr(),
],
),
onTap: () {
_selectMedia();
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.content_paste),
const Gap(16),
Text('addAttachmentFromClipboard').tr(),
],
),
onTap: () {
_pasteMedia();
},
),
],
),
IconButton( IconButton(
onPressed: _isBusy ? null : _sendMessage, onPressed: _isBusy ? null : _sendMessage,
icon: Icon( icon: Icon(

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:responsive_framework/responsive_framework.dart';
class ContextMenuArea extends StatelessWidget {
final ContextMenu contextMenu;
final Widget child;
final ValueChanged<dynamic>? onItemSelected;
const ContextMenuArea({
super.key,
required this.contextMenu,
required this.child,
this.onItemSelected,
});
@override
Widget build(BuildContext context) {
Offset mousePosition = Offset.zero;
return Listener(
onPointerDown: (event) {
mousePosition = event.position;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
if (!isCollapseDrawer) {
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
// Leave padding for side navigation
mousePosition = isExpandDrawer
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
}
},
child: GestureDetector(
onLongPress: () => _showMenu(context, mousePosition),
onSecondaryTap: () => _showMenu(context, mousePosition),
child: child,
),
);
}
void _showMenu(BuildContext context, Offset mousePosition) async {
final menu = contextMenu.copyWith(position: contextMenu.position ?? mousePosition);
final value = await showContextMenu(context, contextMenu: menu);
onItemSelected?.call(value);
}
}

View File

@ -6,15 +6,23 @@ import 'package:dismissible_page/dismissible_page.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:flutter/services.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.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/controllers/post_write_controller.dart'; import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_input.dart';
import 'package:surface/widgets/attachment/attachment_zoom.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart';
import 'package:surface/widgets/context_menu.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/universal_image.dart';
class PostMediaPendingList extends StatelessWidget { class PostMediaPendingList extends StatelessWidget {
final PostWriteMedia? thumbnail; final PostWriteMedia? thumbnail;
@ -70,6 +78,32 @@ class PostMediaPendingList extends StatelessWidget {
} }
} }
Future<void> _setThumbnail(BuildContext context, int idx) async {
if (idx == -1) {
// Thumbnail only can set on video or audio. And thumbnail of the post must be an image, so it's not possible to set thumbnail on the post thumbnail.
return;
} else if (attachments[idx].attachment == null) {
return;
}
final thumbnail = await showDialog<SnAttachment?>(
context: context,
builder: (context) => AttachmentInputDialog(
title: 'attachmentSetThumbnail'.tr(),
),
);
if (thumbnail == null) return;
if (!context.mounted) return;
final attach = context.read<SnAttachmentProvider>();
final newAttach = await attach.updateOne(attachments[idx].attachment!.id, thumbnail.alt, metadata: {
...attachments[idx].attachment!.metadata,
'thumbnail': thumbnail.rid,
});
onUpdate!(idx, PostWriteMedia(newAttach));
}
Future<void> _deleteAttachment(BuildContext context, int idx) async { Future<void> _deleteAttachment(BuildContext context, int idx) async {
final media = idx == -1 ? thumbnail! : attachments[idx]; final media = idx == -1 ? thumbnail! : attachments[idx];
if (media.attachment == null) return; if (media.attachment == null) return;
@ -87,9 +121,17 @@ class PostMediaPendingList extends StatelessWidget {
} }
} }
ContextMenu _buildContextMenu(BuildContext context, int idx, PostWriteMedia media) { ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
return ContextMenu( return ContextMenu(
entries: [ entries: [
if (media.attachment != null && media.type == PostWriteMediaType.video)
MenuItem(
label: 'attachmentSetThumbnail'.tr(),
icon: Symbols.image,
onSelected: () {
_setThumbnail(context, idx);
},
),
if (media.attachment == null && onUpload != null) if (media.attachment == null && onUpload != null)
MenuItem( MenuItem(
label: 'attachmentUpload'.tr(), label: 'attachmentUpload'.tr(),
@ -97,7 +139,10 @@ class PostMediaPendingList extends StatelessWidget {
onSelected: () { onSelected: () {
onUpload!(idx); onUpload!(idx);
}), }),
if (media.attachment != null && onPostSetThumbnail != null && idx != -1) if (media.attachment != null &&
media.type == PostWriteMediaType.image &&
onPostSetThumbnail != null &&
idx != -1)
MenuItem( MenuItem(
label: 'attachmentSetAsPostThumbnail'.tr(), label: 'attachmentSetAsPostThumbnail'.tr(),
icon: Symbols.gallery_thumbnail, icon: Symbols.gallery_thumbnail,
@ -105,7 +150,7 @@ class PostMediaPendingList extends StatelessWidget {
onPostSetThumbnail!(idx); onPostSetThumbnail!(idx);
}, },
) )
else if (media.attachment != null && onPostSetThumbnail != null) else if (media.attachment != null && media.type == PostWriteMediaType.image && onPostSetThumbnail != null)
MenuItem( MenuItem(
label: 'attachmentUnsetAsPostThumbnail'.tr(), label: 'attachmentUnsetAsPostThumbnail'.tr(),
icon: Symbols.cancel, icon: Symbols.cancel,
@ -138,6 +183,14 @@ class PostMediaPendingList extends StatelessWidget {
icon: Symbols.crop, icon: Symbols.crop,
onSelected: () => _cropImage(context, idx), onSelected: () => _cropImage(context, idx),
), ),
if (media.attachment != null)
MenuItem(
label: 'attachmentCopyRandomId'.tr(),
icon: Symbols.content_copy,
onSelected: () {
Clipboard.setData(ClipboardData(text: media.attachment!.rid));
},
),
if (media.attachment != null && onRemove != null) if (media.attachment != null && onRemove != null)
MenuItem( MenuItem(
label: 'delete'.tr(), label: 'delete'.tr(),
@ -168,48 +221,17 @@ class PostMediaPendingList extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final sn = context.read<SnNetworkProvider>();
return Container( return Container(
constraints: const BoxConstraints(maxHeight: 120), constraints: const BoxConstraints(maxHeight: 120),
child: Row( child: Row(
children: [ children: [
const Gap(8), const Gap(8),
if (thumbnail != null) if (thumbnail != null)
ContextMenuRegion( ContextMenuArea(
contextMenu: _buildContextMenu(context, -1, thumbnail!), contextMenu: _createContextMenu(context, -1, thumbnail!),
child: Container( child: _PostMediaPendingItem(media: thumbnail!),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 1,
child: switch (thumbnail!.type) {
PostWriteMediaType.image => Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: LayoutBuilder(builder: (context, constraints) {
return Image(
image: thumbnail!.getImageProvider(
context,
width: (constraints.maxWidth * devicePixelRatio).round(),
height: (constraints.maxHeight * devicePixelRatio).round(),
)!,
fit: BoxFit.contain,
);
}),
),
_ => Container(
color: Theme.of(context).colorScheme.surface,
child: const Icon(Symbols.docs).center(),
),
},
),
),
),
), ),
if (thumbnail != null) if (thumbnail != null)
const VerticalDivider(width: 1, thickness: 1).padding( const VerticalDivider(width: 1, thickness: 1).padding(
@ -224,9 +246,33 @@ class PostMediaPendingList extends StatelessWidget {
itemCount: attachments.length, itemCount: attachments.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final media = attachments[idx]; final media = attachments[idx];
return ContextMenuRegion( return ContextMenuArea(
contextMenu: _buildContextMenu(context, idx, media), contextMenu: _createContextMenu(context, idx, media),
child: Container( child: _PostMediaPendingItem(media: media),
);
},
),
),
],
),
);
}
}
class _PostMediaPendingItem extends StatelessWidget {
final PostWriteMedia media;
const _PostMediaPendingItem({
required this.media,
});
@override
Widget build(BuildContext context) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final sn = context.read<SnNetworkProvider>();
return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
@ -252,6 +298,12 @@ class PostMediaPendingList extends StatelessWidget {
); );
}), }),
), ),
PostWriteMediaType.video => Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: media.attachment?.metadata['thumbnail'] != null
? AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment?.metadata['thumbnail']))
: const Icon(Symbols.videocam).center(),
),
_ => Container( _ => Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: const Icon(Symbols.docs).center(), child: const Icon(Symbols.docs).center(),
@ -259,13 +311,165 @@ class PostMediaPendingList extends StatelessWidget {
}, },
), ),
), ),
),
); );
}
}
class AddPostMediaButton extends StatelessWidget {
final Function(Iterable<PostWriteMedia>) onAdd;
const AddPostMediaButton({super.key, required this.onAdd});
void _takeMedia(bool isVideo) async {
final picker = ImagePicker();
final result = isVideo
? await picker.pickVideo(source: ImageSource.camera)
: await picker.pickImage(source: ImageSource.camera);
if (result == null) return;
onAdd([PostWriteMedia.fromFile(result)]);
}
void _selectMedia() async {
final picker = ImagePicker();
final result = await picker.pickMultipleMedia();
if (result.isEmpty) return;
onAdd(
result.map((e) => PostWriteMedia.fromFile(e)),
);
}
void _pasteMedia() async {
final imageBytes = await Pasteboard.image;
if (imageBytes == null) return;
onAdd([
PostWriteMedia.fromBytes(
imageBytes,
'attachmentPastedImage'.tr(),
PostWriteMediaType.image,
),
]);
}
void _linkRandomId(BuildContext context) async {
final randomIdController = TextEditingController();
final randomId = await showDialog<String?>(
context: context,
builder: (context) => AlertDialog(
title: Text('addAttachmentFromRandomId').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: randomIdController,
decoration: InputDecoration(
labelText: 'fieldAttachmentRandomId'.tr(),
border: const UnderlineInputBorder(),
),
),
const Gap(8),
],
),
actions: [
TextButton(
child: Text('dialogDismiss').tr(),
onPressed: () {
Navigator.pop(context);
}, },
), ),
TextButton(
child: Text('dialogConfirm').tr(),
onPressed: () {
Navigator.pop(context, randomIdController.text);
},
), ),
], ],
), ),
); );
WidgetsBinding.instance.addPostFrameCallback((_) {
randomIdController.dispose();
});
if (randomId == null || randomId.isEmpty) return;
if (!context.mounted) return;
final attach = context.read<SnAttachmentProvider>();
final attachment = await attach.getOne(randomId);
onAdd([
PostWriteMedia(attachment),
]);
}
@override
Widget build(BuildContext context) {
return PopupMenuButton(
icon: Icon(
Symbols.add_photo_alternate,
color: Theme.of(context).colorScheme.primary,
),
itemBuilder: (context) => [
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.photo_camera),
const Gap(16),
Text('addAttachmentFromCameraPhoto').tr(),
],
),
onTap: () {
_takeMedia(false);
},
),
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.videocam),
const Gap(16),
Text('addAttachmentFromCameraVideo').tr(),
],
),
onTap: () {
_takeMedia(true);
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.photo_library),
const Gap(16),
Text('addAttachmentFromAlbum').tr(),
],
),
onTap: () {
_selectMedia();
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.link),
const Gap(16),
Text('addAttachmentFromRandomId').tr(),
],
),
onTap: () {
_linkRandomId(context);
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.content_paste),
const Gap(16),
Text('addAttachmentFromClipboard').tr(),
],
),
onTap: () {
_pasteMedia();
},
),
],
);
} }
} }

View File

@ -282,20 +282,6 @@ class _PostCategoriesFieldState extends State<PostCategoriesField> {
: null, : null,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onChanged: (value) {
for (final divider in kTagsDividers) {
if (value.endsWith(divider)) {
final tagValue = value.substring(0, value.length - 1);
if (tagValue.isEmpty) return;
if (!_currentCategories.contains(tagValue)) {
setState(() => _currentCategories.add(tagValue));
}
controller.clear();
widget.onUpdate(_currentCategories);
break;
}
}
},
onSubmitted: (_) { onSubmitted: (_) {
onSubmitted(); onSubmitted();
}, },

View File

@ -753,10 +753,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_udid name: flutter_udid
sha256: be464dc5b1fb7ee894f6a32d65c086ca5e177fdcf9375ac08d77495b98150f84 sha256: "166bee5989a58c66b8b62000ea65edccc7c8167bbafdbb08022638db330dd030"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "4.0.0"
flutter_web_plugins: flutter_web_plugins:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -766,10 +766,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: "430859fb5b763d7556d06ef287cfca582e17d9a2dc36da26017f25a5c0b2523e" sha256: "0e138a0a3bf6830c29c8439b17be0e222d0de27fa72f24e6aee4d34de72f22ef"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.4" version: "0.12.5"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -934,10 +934,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: fa8141602fde3f7e2f81dbf043613eb44dfa325fa0bcf93c0f142c9f7a2c193e sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.12+18" version: "0.8.12+19"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@ -1086,10 +1086,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: livekit_client name: livekit_client
sha256: "7802b5de1cae2ee3439db730d24d31c6dcbce173c5e6db2fc5774039a290bc2d" sha256: a3ff529fe6745ee40cdedcd021d81c4a6ad946dd495e782596f2856eeeabc739
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.3"
logging: logging:
dependency: transitive dependency: transitive
description: description:

View File

@ -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.1.1+38 version: 2.1.1+39
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -80,7 +80,7 @@ dependencies:
firebase_core: ^3.8.0 firebase_core: ^3.8.0
firebase_messaging: ^15.1.5 firebase_messaging: ^15.1.5
firebase_analytics: ^11.3.5 firebase_analytics: ^11.3.5
flutter_udid: ^3.0.0 flutter_udid: ^4.0.0
media_kit: ^1.1.11 media_kit: ^1.1.11
media_kit_video: ^1.2.5 media_kit_video: ^1.2.5
media_kit_libs_video: ^1.0.5 media_kit_libs_video: ^1.0.5

View File

@ -1,4 +1,6 @@
<!DOCTYPE html><html><head> <!DOCTYPE html>
<html lang="en" oncontextmenu="event.preventDefault();">
<head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.
@ -31,9 +33,8 @@
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"> <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
name="viewport">
<style id="splash-screen-style"> <style id="splash-screen-style">
@ -109,22 +110,24 @@
</script> </script>
</head> </head>
<body> <body>
<picture id="splash-branding"> <picture id="splash-branding">
<source srcset="splash/img/branding-1x.png 1x, splash/img/branding-2x.png 2x, splash/img/branding-3x.png 3x, splash/img/branding-4x.png 4x" media="(prefers-color-scheme: light)"> <source srcset="splash/img/branding-1x.png 1x, splash/img/branding-2x.png 2x, splash/img/branding-3x.png 3x, splash/img/branding-4x.png 4x"
<source srcset="splash/img/branding-dark-1x.png 1x, splash/img/branding-dark-2x.png 2x, splash/img/branding-dark-3x.png 3x, splash/img/branding-dark-4x.png 4x" media="(prefers-color-scheme: dark)"> media="(prefers-color-scheme: light)">
<source srcset="splash/img/branding-dark-1x.png 1x, splash/img/branding-dark-2x.png 2x, splash/img/branding-dark-3x.png 3x, splash/img/branding-dark-4x.png 4x"
media="(prefers-color-scheme: dark)">
<img class="bottom" aria-hidden="true" src="splash/img/branding-1x.png" alt=""> <img class="bottom" aria-hidden="true" src="splash/img/branding-1x.png" alt="">
</picture> </picture>
<picture id="splash"> <picture id="splash">
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)"> <source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x"
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)"> media="(prefers-color-scheme: light)">
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x"
media="(prefers-color-scheme: dark)">
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt=""> <img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
</picture> </picture>
<script src="flutter_bootstrap.js" async=""></script>
</body>
<script src="flutter_bootstrap.js" async=""></script> </html>
</body></html>