Compare commits

...

10 Commits

Author SHA1 Message Date
8943f089f2 Basic message editor 2024-04-17 23:24:09 +08:00
ba405770ed Message showing 2024-04-17 23:00:53 +08:00
c25ae591b9 ⬆️ Use new video player 2024-04-17 20:57:25 +08:00
da5f3a24e7 More attachment source 2024-04-16 23:23:34 +08:00
718c715cae Editing notify 2024-04-16 22:45:22 +08:00
964210cbe4 Chat channels index 2024-04-16 22:29:58 +08:00
bb5a10c4c4 Popup comments 2024-04-16 20:36:47 +08:00
0814c17407 More and more reactions 2024-04-15 23:47:44 +08:00
d2ae4f3292 🐛 Bug fixes and translation 2024-04-15 23:40:36 +08:00
7e42d95904 Reactions 2024-04-15 23:08:32 +08:00
40 changed files with 1548 additions and 328 deletions

View File

@ -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

View File

@ -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..."
}

View File

@ -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": "发条消息……"
}

View File

@ -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
View 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
View 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
View 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
View 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),
};

View File

@ -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 {

View File

@ -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
View 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),
],
),
);
}
}

View 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,
},
);
},
);
},
),
),
);
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -0,0 +1,7 @@
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
void initVideo() {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
}

View 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,
);
},
);
}
}

View 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(),
],
),
),
],
);
}
}
}

View 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),
)
],
),
);
}
}

View File

@ -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),
);
}
}

View File

@ -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) {

View File

@ -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),

View File

@ -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),
],
),
),
),
],
),
),

View File

@ -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);

View File

@ -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(),
);
},
),
],

View File

@ -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,
),
);
},
);

View File

@ -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,
);
},
);
}
}

View File

@ -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);
},
);
}
}

View File

@ -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!();
},
),
);

View 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"),
),
),
);
},
),
),
],
);
}
}

View 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),
),
],
);
}
}

View File

@ -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);

View File

@ -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)

View File

@ -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"))
}

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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(

View File

@ -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)