Compare commits
10 Commits
11a3d8f39b
...
8943f089f2
Author | SHA1 | Date | |
---|---|---|---|
8943f089f2 | |||
ba405770ed | |||
c25ae591b9 | |||
da5f3a24e7 | |||
718c715cae | |||
964210cbe4 | |||
bb5a10c4c4 | |||
0814c17407 | |||
d2ae4f3292 | |||
7e42d95904 |
@ -4,6 +4,10 @@ PODS:
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
@ -15,9 +19,6 @@ PODS:
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
@ -29,12 +30,13 @@ DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
@ -46,6 +48,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_native_event_loop:
|
||||
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
|
||||
media_kit_video:
|
||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||
package_info_plus:
|
||||
@ -56,8 +62,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
@ -69,12 +73,13 @@ SPEC CHECKSUMS:
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18
|
||||
media_kit_video: 26c5b265a4094a2df3e8d41e6724d9b964c13151
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
|
||||
video_player_avfoundation: 2b4384f3b157206b5e150a0083cdc0c905d260d3
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||
webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"solian": "Solian",
|
||||
"explore": "Explore",
|
||||
"chat": "Chat",
|
||||
"account": "Account",
|
||||
"signIn": "Sign In",
|
||||
"signInCaption": "Sign in to create post, start a realm, message your friend and more!",
|
||||
@ -12,16 +13,26 @@
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"action": "Action",
|
||||
"cancel": "Cancel",
|
||||
"report": "Report",
|
||||
"reaction": "Reaction",
|
||||
"reactVerb": "React",
|
||||
"post": "Post",
|
||||
"postVerb": "Post",
|
||||
"comment": "Comment",
|
||||
"attachment": "Attachment",
|
||||
"attachmentAdd": "Add new attachment",
|
||||
"pickPhoto": "Gallery photo",
|
||||
"takePhoto": "Capture photo",
|
||||
"pickVideo": "Gallery video",
|
||||
"takeVideo": "Record video",
|
||||
"newMoment": "Record a moment",
|
||||
"newComment": "Leave a comment",
|
||||
"postIdentityNotify": "You will create this post as",
|
||||
"postContentPlaceholder": "What's happened?!",
|
||||
"postDeleteConfirm": "Are you sure you want to delete this post? This operation cannot be revert!"
|
||||
"postDeleteConfirm": "Are you sure you want to delete this post? This operation cannot be revert!",
|
||||
"postEditNotify": "You are about editing a post that already published.",
|
||||
"reactionAdded": "Your reaction has been added.",
|
||||
"reactionRemoved": "Your reaction has been removed.",
|
||||
"chatMessagePlaceholder": "Write a message..."
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"solian": "索链",
|
||||
"explore": "探索",
|
||||
"chat": "聊天",
|
||||
"account": "账号",
|
||||
"signIn": "登陆",
|
||||
"signInCaption": "登陆以发表帖子、文章、创建领域、和你的朋友聊天,以及获取更多功能!",
|
||||
@ -12,16 +13,26 @@
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"action": "操作",
|
||||
"cancel": "取消",
|
||||
"report": "举报",
|
||||
"reaction": "反应",
|
||||
"reactVerb": "作出反应",
|
||||
"post": "帖子",
|
||||
"postVerb": "发表",
|
||||
"comment": "评论",
|
||||
"attachment": "附件",
|
||||
"attachmentAdd": "附加新附件",
|
||||
"pickPhoto": "相册照片",
|
||||
"takePhoto": "拍摄照片",
|
||||
"pickVideo": "相册视频",
|
||||
"takeVideo": "拍摄视频",
|
||||
"newMoment": "记录时刻",
|
||||
"newComment": "留下评论",
|
||||
"postIdentityNotify": "你将会以该身份发表本帖子",
|
||||
"postContentPlaceholder": "发生什么事了?!",
|
||||
"postDeleteConfirm": "你确定要删除这篇帖子吗?这意味着这个帖子将永远被我们丢弃在硬盘海中!该操作不可被反转!"
|
||||
"postDeleteConfirm": "你确定要删除这篇帖子吗?这意味着这个帖子将永远被我们丢弃在硬盘海中!该操作不可被反转!",
|
||||
"postEditNotify": "你正在修改一个已经发布了的帖子。",
|
||||
"reactionAdded": "你的反应已被添加。",
|
||||
"reactionRemoved": "你的反应已被移除。",
|
||||
"chatMessagePlaceholder": "发条消息……"
|
||||
}
|
@ -5,8 +5,10 @@ import 'package:solian/providers/navigation.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/utils/timeago.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:solian/utils/video_player.dart';
|
||||
|
||||
void main() {
|
||||
initVideo();
|
||||
initTimeAgo();
|
||||
|
||||
runApp(const SolianApp());
|
||||
|
59
lib/models/account.dart
Normal file
59
lib/models/account.dart
Normal file
@ -0,0 +1,59 @@
|
||||
class Account {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String name;
|
||||
String nick;
|
||||
String avatar;
|
||||
String banner;
|
||||
String description;
|
||||
String emailAddress;
|
||||
int powerLevel;
|
||||
int externalId;
|
||||
|
||||
Account({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.name,
|
||||
required this.nick,
|
||||
required this.avatar,
|
||||
required this.banner,
|
||||
required this.description,
|
||||
required this.emailAddress,
|
||||
required this.powerLevel,
|
||||
required this.externalId,
|
||||
});
|
||||
|
||||
factory Account.fromJson(Map<String, dynamic> json) => Account(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
name: json["name"],
|
||||
nick: json["nick"],
|
||||
avatar: json["avatar"],
|
||||
banner: json["banner"],
|
||||
description: json["description"],
|
||||
emailAddress: json["email_address"],
|
||||
powerLevel: json["power_level"],
|
||||
externalId: json["external_id"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"name": name,
|
||||
"nick": nick,
|
||||
"avatar": avatar,
|
||||
"banner": banner,
|
||||
"description": description,
|
||||
"email_address": emailAddress,
|
||||
"power_level": powerLevel,
|
||||
"external_id": externalId,
|
||||
};
|
||||
}
|
63
lib/models/channel.dart
Normal file
63
lib/models/channel.dart
Normal file
@ -0,0 +1,63 @@
|
||||
class Channel {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String alias;
|
||||
String name;
|
||||
String description;
|
||||
dynamic members;
|
||||
dynamic messages;
|
||||
dynamic calls;
|
||||
int type;
|
||||
int accountId;
|
||||
int realmId;
|
||||
|
||||
Channel({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.alias,
|
||||
required this.name,
|
||||
required this.description,
|
||||
this.members,
|
||||
this.messages,
|
||||
this.calls,
|
||||
required this.type,
|
||||
required this.accountId,
|
||||
required this.realmId,
|
||||
});
|
||||
|
||||
factory Channel.fromJson(Map<String, dynamic> json) => Channel(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
alias: json["alias"],
|
||||
name: json["name"],
|
||||
description: json["description"],
|
||||
members: json["members"],
|
||||
messages: json["messages"],
|
||||
calls: json["calls"],
|
||||
type: json["type"],
|
||||
accountId: json["account_id"],
|
||||
realmId: json["realm_id"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"alias": alias,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"members": members,
|
||||
"messages": messages,
|
||||
"calls": calls,
|
||||
"type": type,
|
||||
"account_id": accountId,
|
||||
"realm_id": realmId,
|
||||
};
|
||||
}
|
115
lib/models/message.dart
Normal file
115
lib/models/message.dart
Normal file
@ -0,0 +1,115 @@
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
|
||||
class Message {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String content;
|
||||
dynamic metadata;
|
||||
int type;
|
||||
List<Attachment>? attachments;
|
||||
Channel? channel;
|
||||
Sender sender;
|
||||
int? replyId;
|
||||
Message? replyTo;
|
||||
int channelId;
|
||||
int senderId;
|
||||
|
||||
Message({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.content,
|
||||
required this.metadata,
|
||||
required this.type,
|
||||
this.attachments,
|
||||
this.channel,
|
||||
required this.sender,
|
||||
required this.replyId,
|
||||
required this.replyTo,
|
||||
required this.channelId,
|
||||
required this.senderId,
|
||||
});
|
||||
|
||||
factory Message.fromJson(Map<String, dynamic> json) => Message(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
content: json["content"],
|
||||
metadata: json["metadata"],
|
||||
type: json["type"],
|
||||
attachments: List<Attachment>.from(json["attachments"]?.map((x) => Attachment.fromJson(x)) ?? List.empty()),
|
||||
channel: Channel.fromJson(json["channel"]),
|
||||
sender: Sender.fromJson(json["sender"]),
|
||||
replyId: json["reply_id"],
|
||||
replyTo: json["reply_to"] != null ? Message.fromJson(json["reply_to"]) : null,
|
||||
channelId: json["channel_id"],
|
||||
senderId: json["sender_id"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"content": content,
|
||||
"metadata": metadata,
|
||||
"type": type,
|
||||
"attachments": List<dynamic>.from(attachments?.map((x) => x.toJson()) ?? List.empty()),
|
||||
"channel": channel?.toJson(),
|
||||
"sender": sender.toJson(),
|
||||
"reply_id": replyId,
|
||||
"reply_to": replyTo?.toJson(),
|
||||
"channel_id": channelId,
|
||||
"sender_id": senderId,
|
||||
};
|
||||
}
|
||||
|
||||
class Sender {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
Account account;
|
||||
int channelId;
|
||||
int accountId;
|
||||
int notify;
|
||||
|
||||
Sender({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.account,
|
||||
required this.channelId,
|
||||
required this.accountId,
|
||||
required this.notify,
|
||||
});
|
||||
|
||||
factory Sender.fromJson(Map<String, dynamic> json) => Sender(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
account: Account.fromJson(json["account"]),
|
||||
channelId: json["channel_id"],
|
||||
accountId: json["account_id"],
|
||||
notify: json["notify"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"account": account.toJson(),
|
||||
"channel_id": channelId,
|
||||
"account_id": accountId,
|
||||
"notify": notify,
|
||||
};
|
||||
}
|
16
lib/models/reaction.dart
Normal file
16
lib/models/reaction.dart
Normal file
@ -0,0 +1,16 @@
|
||||
class ReactInfo {
|
||||
final String icon;
|
||||
final int attitude;
|
||||
|
||||
ReactInfo({required this.icon, required this.attitude});
|
||||
}
|
||||
|
||||
final Map<String, ReactInfo> reactions = {
|
||||
'thumb_up': ReactInfo(icon: '👍', attitude: 1),
|
||||
'thumb_down': ReactInfo(icon: '👎', attitude: 2),
|
||||
'just_okay': ReactInfo(icon: '😅', attitude: 0),
|
||||
'cry': ReactInfo(icon: '😭', attitude: 0),
|
||||
'confuse': ReactInfo(icon: '🧐', attitude: 0),
|
||||
'retard': ReactInfo(icon: '🤪', attitude: 0),
|
||||
'clap': ReactInfo(icon: '👏', attitude: 1),
|
||||
};
|
@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
@ -8,9 +7,7 @@ import 'package:oauth2/oauth2.dart' as oauth2;
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
|
||||
class AuthProvider {
|
||||
AuthProvider() {
|
||||
pickClient();
|
||||
}
|
||||
AuthProvider();
|
||||
|
||||
final deviceEndpoint =
|
||||
getRequestUri('passport', '/api/notifications/subscribe');
|
||||
@ -26,7 +23,10 @@ class AuthProvider {
|
||||
static const storageKey = "identity";
|
||||
static const profileKey = "profiles";
|
||||
|
||||
/// Before use this variable to make request
|
||||
/// **MAKE SURE YOU HAVE CALL THE isAuthorized() METHOD**
|
||||
oauth2.Client? client;
|
||||
|
||||
DateTime? lastRefreshedAt;
|
||||
|
||||
Future<bool> pickClient() async {
|
||||
@ -83,9 +83,9 @@ class AuthProvider {
|
||||
|
||||
Future<void> refreshToken() async {
|
||||
if (client != null) {
|
||||
final credentials = await client?.credentials.refresh(
|
||||
final credentials = await client!.credentials.refresh(
|
||||
identifier: clientId, secret: clientSecret, basicAuth: false);
|
||||
client = oauth2.Client(credentials!,
|
||||
client = oauth2.Client(credentials,
|
||||
identifier: clientId, secret: clientSecret);
|
||||
storage.write(key: storageKey, value: credentials.toJson());
|
||||
}
|
||||
@ -106,14 +106,15 @@ class AuthProvider {
|
||||
Future<bool> isAuthorized() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
if (await storage.containsKey(key: storageKey)) {
|
||||
if (client != null) {
|
||||
if (lastRefreshedAt == null ||
|
||||
lastRefreshedAt!
|
||||
.add(const Duration(minutes: 3))
|
||||
.isBefore(DateTime.now())) {
|
||||
await refreshToken();
|
||||
lastRefreshedAt = DateTime.now();
|
||||
}
|
||||
if (client == null) {
|
||||
await pickClient();
|
||||
}
|
||||
if (lastRefreshedAt == null ||
|
||||
DateTime.now()
|
||||
.subtract(const Duration(minutes: 3))
|
||||
.isAfter(lastRefreshedAt!)) {
|
||||
await refreshToken();
|
||||
lastRefreshedAt = DateTime.now();
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/screens/account.dart';
|
||||
import 'package:solian/screens/chat/chat.dart';
|
||||
import 'package:solian/screens/chat/index.dart';
|
||||
import 'package:solian/screens/explore.dart';
|
||||
import 'package:solian/screens/posts/comment_editor.dart';
|
||||
import 'package:solian/screens/posts/moment_editor.dart';
|
||||
@ -13,6 +15,16 @@ final router = GoRouter(
|
||||
name: 'explore',
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
builder: (context, state) => const ChatIndexScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat/:channel',
|
||||
name: 'chat.channel',
|
||||
builder: (context, state) => ChatScreen(alias: state.pathParameters['channel'] as String),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
|
129
lib/screens/chat/chat.dart
Normal file
129
lib/screens/chat/chat.dart
Normal file
@ -0,0 +1,129 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/message.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
import 'package:solian/widgets/chat/message.dart';
|
||||
import 'package:solian/widgets/chat/message_editor.dart';
|
||||
import 'package:solian/widgets/indent_wrapper.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final String alias;
|
||||
|
||||
const ChatScreen({super.key, required this.alias});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
}
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
Channel? _channelMeta;
|
||||
|
||||
final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0);
|
||||
|
||||
final http.Client _client = http.Client();
|
||||
|
||||
Future<void> fetchMetadata(BuildContext context) async {
|
||||
var uri = getRequestUri('messaging', '/api/channels/${widget.alias}');
|
||||
var res = await _client.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final result = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
setState(() => _channelMeta = Channel.fromJson(result));
|
||||
} else {
|
||||
var message = utf8.decode(res.bodyBytes);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Something went wrong... $message")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchMessages(int pageKey, BuildContext context) async {
|
||||
final auth = context.read<AuthProvider>();
|
||||
if (!await auth.isAuthorized()) return;
|
||||
|
||||
final offset = pageKey;
|
||||
const take = 5;
|
||||
|
||||
var uri = getRequestUri(
|
||||
'messaging',
|
||||
'/api/channels/${widget.alias}/messages?take=$take&offset=$offset',
|
||||
);
|
||||
|
||||
var res = await auth.client!.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
|
||||
final items = result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty();
|
||||
final isLastPage = (result.count - pageKey) < take;
|
||||
if (isLastPage || result.data == null) {
|
||||
_pagingController.appendLastPage(items);
|
||||
} else {
|
||||
final nextPageKey = pageKey + items.length;
|
||||
_pagingController.appendPage(items, nextPageKey);
|
||||
}
|
||||
} else {
|
||||
_pagingController.error = utf8.decode(res.bodyBytes);
|
||||
}
|
||||
}
|
||||
|
||||
bool getMessageMergeable(Message? a, Message? b) {
|
||||
if (a == null || b == null) return false;
|
||||
if (a.senderId != b.senderId) return false;
|
||||
return a.createdAt.difference(b.createdAt).inMinutes <= 5;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(Duration.zero, () {
|
||||
fetchMetadata(context);
|
||||
});
|
||||
|
||||
_pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context));
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IndentWrapper(
|
||||
hideDrawer: true,
|
||||
title: _channelMeta?.name ?? "Loading...",
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PagedListView<int, Message>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Message>(
|
||||
itemBuilder: (context, item, index) {
|
||||
bool isMerged = false, hasMerged = false;
|
||||
if (index > 0) {
|
||||
isMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item);
|
||||
}
|
||||
if (index + 1 < (_pagingController.itemList?.length ?? 0)) {
|
||||
hasMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]);
|
||||
}
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: !isMerged ? 8 : 0,
|
||||
bottom: !hasMerged ? 8 : 0,
|
||||
left: 12,
|
||||
right: 12,
|
||||
),
|
||||
child: ChatMessage(item: item, underMerged: isMerged),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ChatMessageEditor(channel: widget.alias),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
82
lib/screens/chat/index.dart
Normal file
82
lib/screens/chat/index.dart
Normal file
@ -0,0 +1,82 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
import 'package:solian/widgets/indent_wrapper.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class ChatIndexScreen extends StatefulWidget {
|
||||
const ChatIndexScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ChatIndexScreen> createState() => _ChatIndexScreenState();
|
||||
}
|
||||
|
||||
class _ChatIndexScreenState extends State<ChatIndexScreen> {
|
||||
List<Channel> _channels = List.empty();
|
||||
|
||||
Future<void> fetchChannels(BuildContext context) async {
|
||||
final auth = context.read<AuthProvider>();
|
||||
if (!await auth.isAuthorized()) return;
|
||||
|
||||
var uri = getRequestUri('messaging', '/api/channels/me/available');
|
||||
|
||||
var res = await auth.client!.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final result = jsonDecode(utf8.decode(res.bodyBytes)) as List<dynamic>;
|
||||
setState(() {
|
||||
_channels = result.map((x) => Channel.fromJson(x)).toList();
|
||||
});
|
||||
} else {
|
||||
var message = utf8.decode(res.bodyBytes);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Something went wrong... $message")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(Duration.zero, () {
|
||||
fetchChannels(context);
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IndentWrapper(
|
||||
title: AppLocalizations.of(context)!.chat,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => fetchChannels(context),
|
||||
child: ListView.builder(
|
||||
itemCount: _channels.length,
|
||||
itemBuilder: (context, index) {
|
||||
final element = _channels[index];
|
||||
return ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.indigo,
|
||||
child: Icon(Icons.tag, color: Colors.white),
|
||||
),
|
||||
title: Text(element.name),
|
||||
subtitle: Text(element.description),
|
||||
onTap: () {
|
||||
router.pushNamed(
|
||||
'chat.channel',
|
||||
pathParameters: {
|
||||
'channel': element.alias,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -19,8 +19,7 @@ class ExploreScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ExploreScreenState extends State<ExploreScreen> {
|
||||
final PagingController<int, Post> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
final PagingController<int, Post> _pagingController = PagingController(firstPageKey: 0);
|
||||
|
||||
final http.Client _client = http.Client();
|
||||
|
||||
@ -28,15 +27,12 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
final offset = pageKey;
|
||||
const take = 5;
|
||||
|
||||
var uri =
|
||||
getRequestUri('interactive', '/api/feed?take=$take&offset=$offset');
|
||||
var uri = getRequestUri('interactive', '/api/feed?take=$take&offset=$offset');
|
||||
|
||||
var res = await _client.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final result =
|
||||
PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
|
||||
final items =
|
||||
result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty();
|
||||
final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
|
||||
final items = result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty();
|
||||
final isLastPage = (result.count - pageKey) < take;
|
||||
if (isLastPage || result.data == null) {
|
||||
_pagingController.appendLastPage(items);
|
||||
@ -77,8 +73,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: PagedListView<int, Post>.separated(
|
||||
pagingController: _pagingController,
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(thickness: 0.3),
|
||||
separatorBuilder: (context, index) => const Divider(thickness: 0.3),
|
||||
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||
itemBuilder: (context, item, index) => GestureDetector(
|
||||
child: PostItem(
|
||||
|
@ -12,14 +12,14 @@ import 'package:solian/widgets/indent_wrapper.dart';
|
||||
import 'package:solian/widgets/posts/attachment_editor.dart';
|
||||
|
||||
class CommentPostArguments {
|
||||
final Post related;
|
||||
final Post? related;
|
||||
final Post? editing;
|
||||
|
||||
CommentPostArguments({required this.related, this.editing});
|
||||
CommentPostArguments({this.related, this.editing});
|
||||
}
|
||||
|
||||
class CommentEditorScreen extends StatefulWidget {
|
||||
final Post related;
|
||||
final Post? related;
|
||||
final Post? editing;
|
||||
|
||||
const CommentEditorScreen({super.key, required this.related, this.editing});
|
||||
@ -50,9 +50,10 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
|
||||
final auth = context.read<AuthProvider>();
|
||||
if (!await auth.isAuthorized()) return;
|
||||
|
||||
final relatedDataset = '${widget.related.modelType}s';
|
||||
final alias = widget.related?.alias ?? 'not-found';
|
||||
final relatedDataset = '${widget.related?.modelType ?? 'comment'}s';
|
||||
final uri = widget.editing == null
|
||||
? getRequestUri('interactive', '/api/p/$relatedDataset/${widget.related.alias}/comments')
|
||||
? getRequestUri('interactive', '/api/p/$relatedDataset/$alias/comments')
|
||||
: getRequestUri('interactive', '/api/p/comments/${widget.editing!.id}');
|
||||
|
||||
final req = Request(widget.editing == null ? "POST" : "PUT", uri);
|
||||
@ -78,6 +79,12 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
|
||||
void cancelEditing() {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (widget.editing != null) {
|
||||
@ -93,6 +100,20 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final auth = context.read<AuthProvider>();
|
||||
|
||||
final editingBanner = MaterialBanner(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20),
|
||||
leading: const Icon(Icons.edit_note),
|
||||
backgroundColor: const Color(0xFFE0E0E0),
|
||||
dividerColor: const Color.fromARGB(1, 0, 0, 0),
|
||||
content: Text(AppLocalizations.of(context)!.postEditNotify),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
onPressed: () => cancelEditing(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return IndentWrapper(
|
||||
hideDrawer: true,
|
||||
title: AppLocalizations.of(context)!.newComment,
|
||||
@ -145,6 +166,7 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
widget.editing != null ? editingBanner : Container(),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
|
@ -69,6 +69,12 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
|
||||
void cancelEditing() {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (widget.editing != null) {
|
||||
@ -84,6 +90,20 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final auth = context.read<AuthProvider>();
|
||||
|
||||
final editingBanner = MaterialBanner(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20),
|
||||
leading: const Icon(Icons.edit_note),
|
||||
backgroundColor: const Color(0xFFE0E0E0),
|
||||
dividerColor: const Color.fromARGB(1, 0, 0, 0),
|
||||
content: Text(AppLocalizations.of(context)!.postEditNotify),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
onPressed: () => cancelEditing(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return IndentWrapper(
|
||||
hideDrawer: true,
|
||||
title: AppLocalizations.of(context)!.newMoment,
|
||||
@ -121,8 +141,7 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
||||
const Divider(thickness: 0.3),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
autofocus: true,
|
||||
@ -130,12 +149,12 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: _textController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText:
|
||||
AppLocalizations.of(context)!.postContentPlaceholder,
|
||||
hintText: AppLocalizations.of(context)!.postContentPlaceholder,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
widget.editing != null ? editingBanner : Container(),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
|
@ -44,6 +44,7 @@ class _PostScreenState extends State<PostScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IndentWrapper(
|
||||
noSafeArea: true,
|
||||
hideDrawer: true,
|
||||
title: AppLocalizations.of(context)!.post,
|
||||
child: FutureBuilder(
|
||||
|
7
lib/utils/video_player.dart
Normal file
7
lib/utils/video_player.dart
Normal file
@ -0,0 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
void initVideo() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
MediaKit.ensureInitialized();
|
||||
}
|
28
lib/widgets/chat/content.dart
Normal file
28
lib/widgets/chat/content.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:solian/models/message.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class ChatMessageContent extends StatelessWidget {
|
||||
final Message item;
|
||||
|
||||
const ChatMessageContent({super.key, required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Markdown(
|
||||
selectable: true,
|
||||
data: item.content,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(0),
|
||||
onTapLink: (text, href, title) async {
|
||||
if (href == null) return;
|
||||
await launchUrlString(
|
||||
href,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
73
lib/widgets/chat/message.dart
Normal file
73
lib/widgets/chat/message.dart
Normal file
@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:solian/models/message.dart';
|
||||
import 'package:solian/widgets/chat/content.dart';
|
||||
import 'package:solian/widgets/posts/content/attachment.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
class ChatMessage extends StatelessWidget {
|
||||
final Message item;
|
||||
final bool underMerged;
|
||||
|
||||
const ChatMessage({super.key, required this.item, required this.underMerged});
|
||||
|
||||
Widget renderAttachment() {
|
||||
if (item.attachments != null && item.attachments!.isNotEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: AttachmentList(items: item.attachments!, provider: 'messaging'),
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final contentPart = Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 2),
|
||||
child: ChatMessageContent(item: item),
|
||||
);
|
||||
|
||||
final userinfoPart = Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
item.sender.account.nick,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(timeago.format(item.createdAt))
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (underMerged) {
|
||||
return Row(
|
||||
children: [
|
||||
const SizedBox(width: 40),
|
||||
Expanded(child: contentPart),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage: NetworkImage(item.sender.account.avatar),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
userinfoPart,
|
||||
contentPart,
|
||||
renderAttachment(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
94
lib/widgets/chat/message_editor.dart
Normal file
94
lib/widgets/chat/message_editor.dart
Normal file
@ -0,0 +1,94 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/models/message.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
|
||||
class ChatMessageEditor extends StatefulWidget {
|
||||
final String channel;
|
||||
final Message? editing;
|
||||
|
||||
const ChatMessageEditor({super.key, required this.channel, this.editing});
|
||||
|
||||
@override
|
||||
State<ChatMessageEditor> createState() => _ChatMessageEditorState();
|
||||
}
|
||||
|
||||
class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
||||
final _textController = TextEditingController();
|
||||
|
||||
bool _isSubmitting = false;
|
||||
|
||||
Future<void> sendMessage(BuildContext context) async {
|
||||
if (_isSubmitting) return;
|
||||
|
||||
final auth = context.read<AuthProvider>();
|
||||
if (!await auth.isAuthorized()) return;
|
||||
|
||||
final uri = widget.editing == null
|
||||
? getRequestUri('messaging', '/api/channels/${widget.channel}/messages')
|
||||
: getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.editing!.id}');
|
||||
|
||||
final req = Request(widget.editing == null ? "POST" : "PUT", uri);
|
||||
req.headers['Content-Type'] = 'application/json';
|
||||
req.body = jsonEncode(<String, dynamic>{
|
||||
'content': _textController.value.text,
|
||||
});
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
var res = await Response.fromStream(await auth.client!.send(req));
|
||||
if (res.statusCode != 200) {
|
||||
var message = utf8.decode(res.bodyBytes);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Something went wrong... $message")),
|
||||
);
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_textController.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(width: 0.3, color: Color(0xffdedede)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
maxLines: null,
|
||||
autofocus: true,
|
||||
autocorrect: true,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: AppLocalizations.of(context)!.chatMessagePlaceholder,
|
||||
),
|
||||
onSubmitted: (_) => sendMessage(context),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)),
|
||||
onPressed: !_isSubmitting ? () => sendMessage(context) : null,
|
||||
child: const Icon(Icons.send),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -3,18 +3,29 @@ import 'package:solian/widgets/navigation_drawer.dart';
|
||||
|
||||
class LayoutWrapper extends StatelessWidget {
|
||||
final Widget? child;
|
||||
final Widget? floatingActionButton;
|
||||
final List<Widget>? appBarActions;
|
||||
final bool? noSafeArea;
|
||||
final String title;
|
||||
|
||||
const LayoutWrapper({super.key, this.child, required this.title});
|
||||
const LayoutWrapper({
|
||||
super.key,
|
||||
this.child,
|
||||
required this.title,
|
||||
this.floatingActionButton,
|
||||
this.appBarActions,
|
||||
this.noSafeArea,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = child ?? Container();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(title), actions: appBarActions),
|
||||
floatingActionButton: floatingActionButton,
|
||||
drawer: const SolianNavigationDrawer(),
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: SafeArea(
|
||||
child: child ?? Container(),
|
||||
),
|
||||
body: (noSafeArea ?? false) ? content : SafeArea(child: content),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:solian/widgets/common_wrapper.dart';
|
||||
import 'package:solian/widgets/navigation_drawer.dart';
|
||||
|
||||
class IndentWrapper extends StatelessWidget {
|
||||
final Widget? child;
|
||||
final Widget? floatingActionButton;
|
||||
final List<Widget>? appBarActions;
|
||||
class IndentWrapper extends LayoutWrapper {
|
||||
final bool? hideDrawer;
|
||||
final bool? noSafeArea;
|
||||
final String title;
|
||||
|
||||
const IndentWrapper({super.key, this.child, required this.title, this.floatingActionButton, this.appBarActions, this.hideDrawer, this.noSafeArea});
|
||||
const IndentWrapper({
|
||||
super.key,
|
||||
super.child,
|
||||
required super.title,
|
||||
super.floatingActionButton,
|
||||
super.appBarActions,
|
||||
this.hideDrawer,
|
||||
super.noSafeArea,
|
||||
}) : super();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -41,6 +41,13 @@ class _SolianNavigationDrawerState extends State<SolianNavigationDrawer> {
|
||||
),
|
||||
"explore",
|
||||
),
|
||||
(
|
||||
NavigationDrawerDestination(
|
||||
icon: const Icon(Icons.send),
|
||||
label: Text(AppLocalizations.of(context)!.chat),
|
||||
),
|
||||
"chat",
|
||||
),
|
||||
(
|
||||
NavigationDrawerDestination(
|
||||
icon: const Icon(Icons.account_circle),
|
||||
|
@ -3,7 +3,6 @@ import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
@ -17,8 +16,7 @@ class AttachmentEditor extends StatefulWidget {
|
||||
final List<Attachment> current;
|
||||
final void Function(List<Attachment> data) onUpdate;
|
||||
|
||||
const AttachmentEditor(
|
||||
{super.key, required this.current, required this.onUpdate});
|
||||
const AttachmentEditor({super.key, required this.current, required this.onUpdate});
|
||||
|
||||
@override
|
||||
State<AttachmentEditor> createState() => _AttachmentEditorState();
|
||||
@ -27,7 +25,7 @@ class AttachmentEditor extends StatefulWidget {
|
||||
class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
bool isSubmitting = false;
|
||||
bool _isSubmitting = false;
|
||||
|
||||
List<Attachment> _attachments = List.empty(growable: true);
|
||||
|
||||
@ -35,19 +33,22 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => AttachmentEditorMethodPopup(
|
||||
pickImage: () => pickImageToUpload(context),
|
||||
pickImage: () => pickImageToUpload(context, ImageSource.gallery),
|
||||
takeImage: () => pickImageToUpload(context, ImageSource.camera),
|
||||
pickVideo: () => pickVideoToUpload(context, ImageSource.gallery),
|
||||
takeVideo: () => pickVideoToUpload(context, ImageSource.camera),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pickImageToUpload(BuildContext context) async {
|
||||
Future<void> pickImageToUpload(BuildContext context, ImageSource source) async {
|
||||
final auth = context.read<AuthProvider>();
|
||||
if (!await auth.isAuthorized()) return;
|
||||
|
||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
final image = await _imagePicker.pickImage(source: source);
|
||||
if (image == null) return;
|
||||
|
||||
setState(() => isSubmitting = true);
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
final file = File(image.path);
|
||||
final hashcode = await calculateSha256(file);
|
||||
@ -63,7 +64,34 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
SnackBar(content: Text("Something went wrong... $err")),
|
||||
);
|
||||
} finally {
|
||||
setState(() => isSubmitting = false);
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pickVideoToUpload(BuildContext context, ImageSource source) async {
|
||||
final auth = context.read<AuthProvider>();
|
||||
if (!await auth.isAuthorized()) return;
|
||||
|
||||
final image = await _imagePicker.pickVideo(source: source);
|
||||
if (image == null) return;
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
final file = File(image.path);
|
||||
final hashcode = await calculateSha256(file);
|
||||
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
try {
|
||||
await uploadAttachment(file, hashcode);
|
||||
} catch (err) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Something went wrong... $err")),
|
||||
);
|
||||
} finally {
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +105,6 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
req.fields['hashcode'] = hashcode;
|
||||
|
||||
var res = await auth.client!.send(req);
|
||||
print(res);
|
||||
if (res.statusCode == 200) {
|
||||
var result = Attachment.fromJson(
|
||||
jsonDecode(utf8.decode(await res.stream.toBytes()))["info"],
|
||||
@ -89,8 +116,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disposeAttachment(
|
||||
BuildContext context, Attachment item, int index) async {
|
||||
Future<void> disposeAttachment(BuildContext context, Attachment item, int index) async {
|
||||
final auth = context.read<AuthProvider>();
|
||||
|
||||
final req = MultipartRequest(
|
||||
@ -98,7 +124,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
getRequestUri('interactive', '/api/attachments/${item.id}'),
|
||||
);
|
||||
|
||||
setState(() => isSubmitting = true);
|
||||
setState(() => _isSubmitting = true);
|
||||
var res = await auth.client!.send(req);
|
||||
if (res.statusCode == 200) {
|
||||
setState(() => _attachments.removeAt(index));
|
||||
@ -109,7 +135,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
SnackBar(content: Text("Something went wrong... $err")),
|
||||
);
|
||||
}
|
||||
setState(() => isSubmitting = false);
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
|
||||
Future<String> calculateSha256(File file) async {
|
||||
@ -139,17 +165,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
if (bytes == 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
final dm = decimals < 0 ? 0 : decimals;
|
||||
final sizes = [
|
||||
'Bytes',
|
||||
'KiB',
|
||||
'MiB',
|
||||
'GiB',
|
||||
'TiB',
|
||||
'PiB',
|
||||
'EiB',
|
||||
'ZiB',
|
||||
'YiB'
|
||||
];
|
||||
final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
final i = (math.log(bytes) / math.log(k)).floor().toInt();
|
||||
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||
}
|
||||
@ -186,9 +202,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data == true) {
|
||||
return TextButton(
|
||||
onPressed: isSubmitting
|
||||
? null
|
||||
: () => viewAttachMethods(context),
|
||||
onPressed: _isSubmitting ? null : () => viewAttachMethods(context),
|
||||
style: TextButton.styleFrom(shape: const CircleBorder()),
|
||||
child: const Icon(Icons.add_circle),
|
||||
);
|
||||
@ -200,7 +214,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
],
|
||||
),
|
||||
),
|
||||
isSubmitting ? const LinearProgressIndicator() : Container(),
|
||||
_isSubmitting ? const LinearProgressIndicator() : Container(),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: _attachments.length,
|
||||
@ -232,8 +246,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: const Icon(Icons.delete),
|
||||
onPressed: () =>
|
||||
disposeAttachment(context, element, index),
|
||||
onPressed: () => disposeAttachment(context, element, index),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -249,8 +262,17 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
|
||||
|
||||
class AttachmentEditorMethodPopup extends StatelessWidget {
|
||||
final Function pickImage;
|
||||
final Function takeImage;
|
||||
final Function pickVideo;
|
||||
final Function takeVideo;
|
||||
|
||||
const AttachmentEditorMethodPopup({super.key, required this.pickImage});
|
||||
const AttachmentEditorMethodPopup({
|
||||
super.key,
|
||||
required this.pickImage,
|
||||
required this.takeImage,
|
||||
required this.pickVideo,
|
||||
required this.takeVideo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -282,14 +304,58 @@ class AttachmentEditorMethodPopup extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add_photo_alternate,
|
||||
color: Colors.indigo),
|
||||
const Icon(Icons.add_photo_alternate, color: Colors.indigo),
|
||||
const SizedBox(height: 8),
|
||||
Text(AppLocalizations.of(context)!.pickPhoto),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => takeImage(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.camera_alt, color: Colors.indigo),
|
||||
const SizedBox(height: 8),
|
||||
Text(AppLocalizations.of(context)!.takePhoto),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => pickVideo(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.camera, color: Colors.indigo),
|
||||
const SizedBox(height: 8),
|
||||
Text(AppLocalizations.of(context)!.pickVideo),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => takeVideo(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.video_call, color: Colors.indigo),
|
||||
const SizedBox(height: 8),
|
||||
Text(AppLocalizations.of(context)!.takeVideo),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,29 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AttachmentScreen extends StatelessWidget {
|
||||
final String tag;
|
||||
final String url;
|
||||
final String? tag;
|
||||
|
||||
const AttachmentScreen({super.key, required this.tag, required this.url});
|
||||
const AttachmentScreen({super.key, this.tag, required this.url});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final image = SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: InteractiveViewer(
|
||||
boundaryMargin: const EdgeInsets.all(128),
|
||||
minScale: 0.1,
|
||||
maxScale: 16.0,
|
||||
child: Image.network(url, fit: BoxFit.contain),
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: GestureDetector(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: InteractiveViewer(
|
||||
boundaryMargin: const EdgeInsets.all(128),
|
||||
minScale: 0.1,
|
||||
maxScale: 16.0,
|
||||
child: Hero(
|
||||
tag: tag,
|
||||
child: Image.network(url, fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: tag != null ? Hero(tag: tag!, child: image) : image,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:markdown/markdown.dart' as markdown;
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
import 'package:solian/widgets/posts/content/attachment.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class ArticleContent extends StatelessWidget {
|
||||
@ -53,13 +54,17 @@ class ArticleContent extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
imageBuilder: (url, _, __) {
|
||||
Uri uri;
|
||||
if (url.toString().startsWith("/api/attachments")) {
|
||||
return Image.network(
|
||||
getRequestUri('interactive', url.toString())
|
||||
.toString());
|
||||
uri = getRequestUri('interactive', url.toString());
|
||||
} else {
|
||||
return Image.network(url.toString());
|
||||
uri = url;
|
||||
}
|
||||
|
||||
return AttachmentItem(
|
||||
type: 1,
|
||||
url: uri.toString(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -1,47 +1,58 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
|
||||
import 'package:solian/widgets/posts/attachment_screen.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AttachmentItem extends StatefulWidget {
|
||||
final Attachment item;
|
||||
final int type;
|
||||
final String url;
|
||||
final String? tag;
|
||||
final String? badge;
|
||||
|
||||
const AttachmentItem({super.key, required this.item, this.badge});
|
||||
const AttachmentItem({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.url,
|
||||
this.tag,
|
||||
this.badge,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AttachmentItem> createState() => _AttachmentItemState();
|
||||
}
|
||||
|
||||
class _AttachmentItemState extends State<AttachmentItem> {
|
||||
String getTag() => 'attachment-${widget.item.fileId}';
|
||||
String getTag() => 'attachment-${widget.tag ?? const Uuid().v4()}';
|
||||
|
||||
Uri getFileUri() =>
|
||||
getRequestUri('interactive', '/api/attachments/o/${widget.item.fileId}');
|
||||
|
||||
VideoPlayerController? _vpController;
|
||||
ChewieController? _chewieController;
|
||||
late final _videoPlayer = Player(
|
||||
configuration: PlayerConfiguration(
|
||||
title: "Attachment #${getTag()}",
|
||||
logLevel: MPVLogLevel.error,
|
||||
),
|
||||
);
|
||||
late final _videoController = VideoController(_videoPlayer);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const borderRadius = Radius.circular(16);
|
||||
const borderRadius = Radius.circular(8);
|
||||
final tag = getTag();
|
||||
|
||||
Widget content;
|
||||
|
||||
if (widget.item.type == 1) {
|
||||
if (widget.type == 1) {
|
||||
content = GestureDetector(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(borderRadius),
|
||||
child: Hero(
|
||||
tag: getTag(),
|
||||
tag: tag,
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.network(
|
||||
getFileUri().toString(),
|
||||
widget.url,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
@ -62,38 +73,25 @@ class _AttachmentItemState extends State<AttachmentItem> {
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) {
|
||||
return AttachmentScreen(
|
||||
tag: getTag(),
|
||||
url: getFileUri().toString(),
|
||||
tag: tag,
|
||||
url: widget.url,
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_vpController = VideoPlayerController.networkUrl(getFileUri());
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _vpController!,
|
||||
_videoPlayer.open(
|
||||
Media(widget.url),
|
||||
play: false,
|
||||
);
|
||||
|
||||
content = FutureBuilder(
|
||||
future: () async {
|
||||
await _vpController?.initialize();
|
||||
return true;
|
||||
}(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.all(borderRadius),
|
||||
child: Chewie(
|
||||
controller: _chewieController!,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
content = ClipRRect(
|
||||
borderRadius: const BorderRadius.all(borderRadius),
|
||||
child: Video(
|
||||
controller: _videoController,
|
||||
key: Key(getTag()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -112,16 +110,18 @@ class _AttachmentItemState extends State<AttachmentItem> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_vpController?.dispose();
|
||||
_chewieController?.dispose();
|
||||
_videoPlayer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class AttachmentList extends StatelessWidget {
|
||||
final List<Attachment> items;
|
||||
final String provider;
|
||||
|
||||
const AttachmentList({super.key, required this.items});
|
||||
const AttachmentList({super.key, required this.items, required this.provider});
|
||||
|
||||
Uri getFileUri(String fileId) => getRequestUri(provider, '/api/attachments/o/$fileId');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -140,7 +140,11 @@ class AttachmentList extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: AttachmentItem(
|
||||
item: item, badge: items.length <= 1 ? null : badge),
|
||||
type: item.type,
|
||||
tag: item.fileId,
|
||||
url: getFileUri(item.fileId).toString(),
|
||||
badge: items.length <= 1 ? null : badge,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class MomentContent extends StatelessWidget {
|
||||
final Post item;
|
||||
@ -16,6 +17,13 @@ class MomentContent extends StatelessWidget {
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(0),
|
||||
onTapLink: (text, href, title) async {
|
||||
if (href == null) return;
|
||||
await launchUrlString(
|
||||
href,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/widgets/posts/comment_list.dart';
|
||||
import 'package:solian/widgets/posts/content/article.dart';
|
||||
import 'package:solian/widgets/posts/content/attachment.dart';
|
||||
import 'package:solian/widgets/posts/content/moment.dart';
|
||||
import 'package:solian/widgets/posts/item_action.dart';
|
||||
import 'package:solian/widgets/posts/reaction_list.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
class PostItem extends StatefulWidget {
|
||||
final Post item;
|
||||
final bool? brief;
|
||||
final Function? onUpdate;
|
||||
final Function? onDelete;
|
||||
|
||||
const PostItem({super.key, required this.item, this.brief, this.onUpdate});
|
||||
const PostItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.brief,
|
||||
this.onUpdate,
|
||||
this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PostItem> createState() => _PostItemState();
|
||||
@ -30,6 +41,36 @@ class _PostItemState extends State<PostItem> {
|
||||
);
|
||||
}
|
||||
|
||||
void viewComments(BuildContext context) {
|
||||
final PagingController<int, Post> commentPaging =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
CommentListHeader(
|
||||
related: widget.item,
|
||||
paging: commentPaging,
|
||||
),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
CommentList(
|
||||
related: widget.item,
|
||||
dataset: '${widget.item.modelType}s',
|
||||
paging: commentPaging,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget renderContent() {
|
||||
switch (widget.item.modelType) {
|
||||
case 'article':
|
||||
@ -46,17 +87,56 @@ class _PostItemState extends State<PostItem> {
|
||||
widget.item.attachments!.isNotEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: AttachmentList(items: widget.item.attachments!),
|
||||
child: AttachmentList(items: widget.item.attachments!, provider: 'interactive'),
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
Widget renderReactions() {
|
||||
return Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.only(top: 8, left: 4, right: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.comment),
|
||||
label: Text(widget.item.commentCount.toString()),
|
||||
tooltip: AppLocalizations.of(context)!.comment,
|
||||
onPressed: () => viewComments(context),
|
||||
),
|
||||
const VerticalDivider(thickness: 0.3, indent: 8, endIndent: 8),
|
||||
Expanded(
|
||||
child: ReactionList(
|
||||
item: widget.item,
|
||||
reactionList: reactionList,
|
||||
onReact: (symbol, changes) {
|
||||
setState(() {
|
||||
if (!reactionList!.containsKey(symbol)) {
|
||||
reactionList![symbol] = 0;
|
||||
}
|
||||
reactionList![symbol] += changes;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getAuthorDescribe() => widget.item.author.description.isNotEmpty
|
||||
? widget.item.author.description
|
||||
: 'No description yet.';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
reactionList = widget.item.reactionList;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final headingParts = [
|
||||
@ -75,89 +155,96 @@ class _PostItemState extends State<PostItem> {
|
||||
),
|
||||
];
|
||||
|
||||
Widget content;
|
||||
|
||||
if (widget.brief ?? true) {
|
||||
return GestureDetector(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage: NetworkImage(widget.item.author.avatar),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...headingParts,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, right: 12, top: 4),
|
||||
child: renderContent(),
|
||||
),
|
||||
renderAttachments(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onLongPress: () {
|
||||
viewActions(context);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return GestureDetector(
|
||||
content = Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage: NetworkImage(widget.item.author.avatar),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage: NetworkImage(widget.item.author.avatar),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...headingParts,
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 12, right: 12, top: 4),
|
||||
child: renderContent(),
|
||||
),
|
||||
renderAttachments(),
|
||||
renderReactions(),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...headingParts,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
getAuthorDescribe(),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
child: Divider(thickness: 0.3),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: renderContent(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: renderAttachments(),
|
||||
)
|
||||
],
|
||||
),
|
||||
onLongPress: () {
|
||||
viewActions(context);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
content = Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage: NetworkImage(widget.item.author.avatar),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...headingParts,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
getAuthorDescribe(),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
child: Divider(thickness: 0.3),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: renderContent(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: renderAttachments(),
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: renderReactions(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
child: content,
|
||||
onLongPress: () {
|
||||
viewActions(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,45 @@ import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:solian/screens/posts/comment_editor.dart';
|
||||
import 'package:solian/widgets/posts/item_deletion.dart';
|
||||
|
||||
class PostItemAction extends StatelessWidget {
|
||||
final Post item;
|
||||
final Function? onUpdate;
|
||||
final Function? onDelete;
|
||||
|
||||
const PostItemAction({super.key, required this.item, this.onUpdate});
|
||||
const PostItemAction({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.onUpdate,
|
||||
this.onDelete,
|
||||
});
|
||||
|
||||
void viewEditor() async {
|
||||
bool ok = false;
|
||||
switch (item.modelType) {
|
||||
case 'article':
|
||||
ok = await router.pushNamed(
|
||||
'posts.articles.editor',
|
||||
extra: item,
|
||||
) as bool;
|
||||
case 'moment':
|
||||
ok = await router.pushNamed(
|
||||
'posts.moments.editor',
|
||||
extra: item,
|
||||
) as bool;
|
||||
case 'comment':
|
||||
ok = await router.pushNamed(
|
||||
'posts.comments.editor',
|
||||
extra: CommentPostArguments(editing: item),
|
||||
) as bool;
|
||||
}
|
||||
|
||||
if (ok == true && onUpdate != null) {
|
||||
onUpdate!();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -32,21 +64,12 @@ class PostItemAction extends StatelessWidget {
|
||||
child: FutureBuilder(
|
||||
future: auth.getProfiles(),
|
||||
builder: (context, snapshot) {
|
||||
print(snapshot);
|
||||
if (snapshot.hasData) {
|
||||
final authorizedItems = [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit),
|
||||
title: Text(AppLocalizations.of(context)!.edit),
|
||||
onTap: () {
|
||||
router
|
||||
.pushNamed('posts.moments.editor', extra: item)
|
||||
.then((did) {
|
||||
if (did == true && onUpdate != null) {
|
||||
onUpdate!();
|
||||
}
|
||||
});
|
||||
},
|
||||
onTap: () => viewEditor(),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
@ -59,7 +82,7 @@ class PostItemAction extends StatelessWidget {
|
||||
item: item,
|
||||
dataset: dataset,
|
||||
onDelete: (did) {
|
||||
if(did == true && onUpdate != null) onUpdate!();
|
||||
if (did == true && onDelete != null) onDelete!();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
145
lib/widgets/posts/reaction_action.dart
Normal file
145
lib/widgets/posts/reaction_action.dart
Normal file
@ -0,0 +1,145 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/models/reaction.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
Future<void> doReact(
|
||||
String dataset,
|
||||
int id,
|
||||
String symbol,
|
||||
int attitude,
|
||||
final void Function(String symbol, int num) onReact,
|
||||
BuildContext context,
|
||||
) async {
|
||||
final auth = context.read<AuthProvider>();
|
||||
if (!await auth.isAuthorized()) return;
|
||||
|
||||
var uri = getRequestUri(
|
||||
'interactive',
|
||||
'/api/p/$dataset/$id/react',
|
||||
);
|
||||
|
||||
var res = await auth.client!.post(
|
||||
uri,
|
||||
headers: <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(<String, dynamic>{
|
||||
'symbol': symbol,
|
||||
'attitude': attitude,
|
||||
}),
|
||||
);
|
||||
|
||||
if (res.statusCode == 201) {
|
||||
onReact(symbol, 1);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.reactionAdded),
|
||||
),
|
||||
);
|
||||
} else if (res.statusCode == 204) {
|
||||
onReact(symbol, -1);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.reactionRemoved),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final message = utf8.decode(res.bodyBytes);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Something went wrong... $message")),
|
||||
);
|
||||
}
|
||||
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
class ReactionActionPopup extends StatefulWidget {
|
||||
final String dataset;
|
||||
final int id;
|
||||
final void Function(String symbol, int num) onReact;
|
||||
|
||||
const ReactionActionPopup({
|
||||
super.key,
|
||||
required this.dataset,
|
||||
required this.id,
|
||||
required this.onReact,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReactionActionPopup> createState() => _ReactionActionPopupState();
|
||||
}
|
||||
|
||||
class _ReactionActionPopupState extends State<ReactionActionPopup> {
|
||||
bool _isSubmitting = false;
|
||||
|
||||
Future<void> doWidgetReact(
|
||||
String symbol,
|
||||
int attitude,
|
||||
BuildContext context,
|
||||
) async {
|
||||
if (_isSubmitting) return;
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
await doReact(
|
||||
widget.dataset,
|
||||
widget.id,
|
||||
symbol,
|
||||
attitude,
|
||||
widget.onReact,
|
||||
context,
|
||||
);
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reactEntries = reactions.entries.toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.reaction,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
_isSubmitting ? const LinearProgressIndicator() : Container(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: reactions.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var info = reactEntries[index];
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
await doWidgetReact(info.key, info.value.attitude, context);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(info.value.icon),
|
||||
subtitle: Text(
|
||||
":${info.key}:",
|
||||
style: const TextStyle(fontFamily: "monospace"),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
90
lib/widgets/posts/reaction_list.dart
Normal file
90
lib/widgets/posts/reaction_list.dart
Normal file
@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/models/reaction.dart';
|
||||
import 'package:solian/widgets/posts/reaction_action.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class ReactionList extends StatefulWidget {
|
||||
final Post item;
|
||||
final Map<String, dynamic>? reactionList;
|
||||
final void Function(String symbol, int num) onReact;
|
||||
|
||||
const ReactionList({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.reactionList,
|
||||
required this.onReact,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReactionList> createState() => _ReactionListState();
|
||||
}
|
||||
|
||||
class _ReactionListState extends State<ReactionList> {
|
||||
bool _isSubmitting = false;
|
||||
|
||||
void viewReactMenu(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => ReactionActionPopup(
|
||||
dataset: '${widget.item.modelType}s',
|
||||
id: widget.item.id,
|
||||
onReact: widget.onReact,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> doWidgetReact(
|
||||
String symbol,
|
||||
int attitude,
|
||||
BuildContext context,
|
||||
) async {
|
||||
if (_isSubmitting) return;
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
await doReact(
|
||||
'${widget.item.modelType}s',
|
||||
widget.item.id,
|
||||
symbol,
|
||||
attitude,
|
||||
widget.onReact,
|
||||
context,
|
||||
);
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const density = VisualDensity(horizontal: -4, vertical: -2);
|
||||
|
||||
final reactEntries = widget.reactionList?.entries ?? List.empty();
|
||||
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
...reactEntries.map((x) {
|
||||
final info = reactions[x.key];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ActionChip(
|
||||
avatar: Text(info!.icon),
|
||||
label: Text(x.value.toString()),
|
||||
tooltip: ':${x.key}:',
|
||||
visualDensity: density,
|
||||
onPressed: _isSubmitting
|
||||
? null
|
||||
: () => doWidgetReact(x.key, info.attitude, context),
|
||||
),
|
||||
);
|
||||
}),
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.add_reaction, color: Colors.teal),
|
||||
label: Text(AppLocalizations.of(context)!.reactVerb),
|
||||
visualDensity: density,
|
||||
onPressed: () => viewReactMenu(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <media_kit_video/media_kit_video_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
@ -18,6 +19,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
|
||||
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
|
||||
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
|
||||
|
@ -5,11 +5,13 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
media_kit_libs_linux
|
||||
media_kit_video
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
media_kit_native_event_loop
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
@ -7,22 +7,22 @@ import Foundation
|
||||
|
||||
import file_selector_macos
|
||||
import flutter_secure_storage_macos
|
||||
import media_kit_libs_macos_video
|
||||
import media_kit_video
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
import screen_brightness_macos
|
||||
import url_launcher_macos
|
||||
import video_player_avfoundation
|
||||
import wakelock_plus
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
}
|
||||
|
@ -4,6 +4,10 @@ PODS:
|
||||
- flutter_secure_storage_macos (6.1.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
- media_kit_libs_macos_video (1.0.4):
|
||||
- FlutterMacOS
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
- FlutterMacOS
|
||||
- media_kit_video (0.0.1):
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.0.1):
|
||||
@ -15,9 +19,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
|
||||
@ -25,12 +26,13 @@ DEPENDENCIES:
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
|
||||
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
|
||||
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@ -40,6 +42,10 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
media_kit_libs_macos_video:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
|
||||
media_kit_native_event_loop:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos
|
||||
media_kit_video:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
|
||||
package_info_plus:
|
||||
@ -50,8 +56,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
video_player_avfoundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin
|
||||
wakelock_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
|
||||
@ -59,12 +63,13 @@ SPEC CHECKSUMS:
|
||||
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
|
||||
flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
media_kit_video: f5bdcbfaef003c02251e50d44bb741aa96fb8a1e
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
video_player_avfoundation: 2b4384f3b157206b5e150a0083cdc0c905d260d3
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
|
||||
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
|
||||
|
160
pubspec.lock
160
pubspec.lock
@ -41,14 +41,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
chewie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: chewie
|
||||
sha256: e53da939709efb9aad0f3d72a69a8d05f889168b7a138af60ce78bab5c94b135
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -89,22 +81,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
version: "1.0.8"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -199,10 +183,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: "31c12de79262b5431c5492e9c89948aa789158435f707d3519a7fdef6af28af7"
|
||||
sha256: "04c4722cc36ec5af38acc38ece70d22d3c2123c61305d555750a091517bbe504"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.22+1"
|
||||
version: "0.6.23"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -285,14 +269,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.2.4"
|
||||
html:
|
||||
hive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
|
||||
name: hive
|
||||
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.4"
|
||||
version: "2.2.3"
|
||||
hive_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hive_flutter
|
||||
sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -321,34 +313,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "1f498d086203360cca099d20ffea2963f48c39ce91bdd8a3b6d4a045786b02c8"
|
||||
sha256: fe9ee64ccb8d599a5dfb0e21cc6652232c610bcf667af4e79b9eb175cc30a7a5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
version: "1.1.0"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "844c6da4e4f2829dffdab97816bca09d0e0977e8dcef7450864aba4e07967a58"
|
||||
sha256: "8e75431a62b7feb4fd55cb4a5c6f0ac4564460ec5dc09f9c4a0d50a5ce7c4cb9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.9+6"
|
||||
version: "0.8.10"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "6a1704fdd75022272e7e7a897a9068e9c2ff3cd6a66820bf3ded810633eac954"
|
||||
sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.4"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "917a5cadd67d052554cfb258595e54217de53fac5b52939426e26319a02e6297"
|
||||
sha256: f4a6f62be96d6fd268f32a6bf8ef444cd8e3fff64d16923c6e6fe55e0c84a761
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.9+2"
|
||||
version: "0.8.10"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -470,13 +462,69 @@ packages:
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
media_kit:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit
|
||||
sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.10+1"
|
||||
media_kit_libs_android_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_android_video
|
||||
sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.6"
|
||||
media_kit_libs_ios_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_ios_video
|
||||
sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
media_kit_libs_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_linux
|
||||
sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
media_kit_libs_macos_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_macos_video
|
||||
sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
media_kit_libs_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_libs_video
|
||||
sha256: "3688e0c31482074578652bf038ce6301a5d21e1eda6b54fc3117ffeb4bdba067"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
media_kit_libs_windows_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_windows_video
|
||||
sha256: "7bace5f35d9afcc7f9b5cdadb7541d2191a66bb3fc71bfa11c1395b3360f6122"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
media_kit_native_event_loop:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_native_event_loop
|
||||
sha256: a605cf185499d14d58935b8784955a92a4bf0ff4e19a23de3d17a9106303930e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
media_kit_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -846,10 +894,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d"
|
||||
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -859,7 +907,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
|
||||
@ -874,46 +922,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
sha256: db6a72d8f4fd155d0189845678f55ad2fd54b02c10dcafd11c068dbb631286c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.6"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: "821cff3446bbde255e8d03c12fe1f9810c69fee2c26c394545b13d824ba63c2e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.13"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: "00c49b1d68071341397cf760b982c1e26ed9232464c8506ee08378a5cca5070d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.7"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.2"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
sha256: "41245cef5ef29c4585dbabcbcbe9b209e34376642c7576cabf11b4ad9289d6e4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -48,13 +48,15 @@ dependencies:
|
||||
timeago: ^3.6.1
|
||||
flutter_carousel_widget: ^2.2.0
|
||||
media_kit_video: ^1.2.4
|
||||
chewie: ^1.8.1
|
||||
video_player: ^2.8.6
|
||||
flutter_secure_storage: ^9.0.0
|
||||
oauth2: ^2.0.2
|
||||
webview_flutter: ^4.7.0
|
||||
crypto: ^3.0.3
|
||||
image_picker: ^1.0.8
|
||||
uuid: ^4.4.0
|
||||
media_kit: ^1.1.10+1
|
||||
media_kit_libs_video: ^1.0.4
|
||||
hive_flutter: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
||||
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||
ScreenBrightnessWindowsPluginRegisterWithRegistrar(
|
||||
|
@ -5,12 +5,14 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
media_kit_libs_windows_video
|
||||
media_kit_video
|
||||
screen_brightness_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
media_kit_native_event_loop
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
Loading…
x
Reference in New Issue
Block a user