Download attachment

This commit is contained in:
LittleSheep 2024-08-01 02:10:57 +08:00
parent ecef8dab0c
commit f10393f6d0
14 changed files with 198 additions and 64 deletions

View File

@ -76,6 +76,9 @@ PODS:
- flutter_webrtc (0.11.3): - flutter_webrtc (0.11.3):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleDataTransport (9.4.1): - GoogleDataTransport (9.4.1):
- GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30911.0, >= 2.30908.0) - nanopb (< 2.30911.0, >= 2.30908.0)
@ -169,6 +172,7 @@ DEPENDENCIES:
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`) - livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
@ -223,6 +227,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios" :path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_webrtc: flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios" :path: ".symlinks/plugins/flutter_webrtc/ios"
gal:
:path: ".symlinks/plugins/gal/darwin"
image_picker_ios: image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios" :path: ".symlinks/plugins/image_picker_ios/ios"
livekit_client: livekit_client:
@ -276,6 +282,7 @@ SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1

View File

@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
extension SolianExtenions on BuildContext { extension SolianExtenions on BuildContext {
void showSnackbar(String content) { void showSnackbar(String content, {SnackBarAction? action}) {
ScaffoldMessenger.of(this).showSnackBar(SnackBar( ScaffoldMessenger.of(this).showSnackBar(SnackBar(
content: Text(content), content: Text(content),
action: action,
)); ));
} }

View File

@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View File

@ -33,6 +33,7 @@ const i18nEnglish = {
'article': 'Article', 'article': 'Article',
'reply': 'Reply', 'reply': 'Reply',
'repost': 'Repost', 'repost': 'Repost',
'openInAlbum': 'Open in album',
'openInBrowser': 'Open in browser', 'openInBrowser': 'Open in browser',
'notification': 'Notification', 'notification': 'Notification',
'errorHappened': 'An error occurred', 'errorHappened': 'An error occurred',
@ -313,4 +314,5 @@ const i18nEnglish = {
'themeColorKagamine': 'Kagamine Yellow', 'themeColorKagamine': 'Kagamine Yellow',
'themeColorLuka': 'Luka Pink', 'themeColorLuka': 'Luka Pink',
'themeColorApplied': 'Global theme color has been applied.', 'themeColorApplied': 'Global theme color has been applied.',
'attachmentSaved': 'Attachment saved to your system album.',
}; };

View File

@ -33,6 +33,7 @@ const i18nSimplifiedChinese = {
'article': '文章', 'article': '文章',
'reply': '回复', 'reply': '回复',
'repost': '转帖', 'repost': '转帖',
'openInAlbum': '在相簿中打开',
'openInBrowser': '在浏览器中打开', 'openInBrowser': '在浏览器中打开',
'notification': '通知', 'notification': '通知',
'errorHappened': '发生错误了', 'errorHappened': '发生错误了',
@ -290,4 +291,5 @@ const i18nSimplifiedChinese = {
'themeColorKagamine': '镜音黄', 'themeColorKagamine': '镜音黄',
'themeColorLuka': '流音粉', 'themeColorLuka': '流音粉',
'themeColorApplied': '全局主题颜色已应用', 'themeColorApplied': '全局主题颜色已应用',
'attachmentSaved': '附件已保存到系统相册',
}; };

View File

@ -322,7 +322,7 @@ class AttachmentListEntry extends StatelessWidget {
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentListFullScreen( AttachmentListFullScreen(
parentId: parentId, parentId: parentId,
attachment: item!, item: item!,
), ),
rootNavigator: true, rootNavigator: true,
); );

View File

@ -1,19 +1,27 @@
import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:dio/dio.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:gal/gal.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/platform.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/attachments/attachment_item.dart'; import 'package:solian/widgets/attachments/attachment_item.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:path/path.dart' show extension;
class AttachmentListFullScreen extends StatefulWidget { class AttachmentListFullScreen extends StatefulWidget {
final String parentId; final String parentId;
final Attachment attachment; final Attachment item;
const AttachmentListFullScreen( const AttachmentListFullScreen(
{super.key, required this.parentId, required this.attachment}); {super.key, required this.parentId, required this.item});
@override @override
State<AttachmentListFullScreen> createState() => State<AttachmentListFullScreen> createState() =>
@ -23,6 +31,10 @@ class AttachmentListFullScreen extends StatefulWidget {
class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> { class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
bool _showDetails = true; bool _showDetails = true;
bool _isDownloading = false;
bool _isCompletedDownload = false;
double? _progressOfDownload = 0;
Color get _unFocusColor => Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
@ -46,13 +58,63 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
} }
double _getRatio() { double _getRatio() {
final value = widget.attachment.metadata?['ratio']; final value = widget.item.metadata?['ratio'];
if (value == null) return 1; if (value == null) return 1;
if (value is int) return value.toDouble(); if (value is int) return value.toDouble();
if (value is double) return value; if (value is double) return value;
return 1; return 1;
} }
Future<void> _saveToAlbum() async {
final url = ServiceFinder.buildUrl(
'files',
'/attachments/${widget.item.id}',
);
if (PlatformInfo.isWeb) {
await launchUrlString(url);
return;
}
if (!await Gal.hasAccess(toAlbum: true)) {
if (!await Gal.requestAccess(toAlbum: true)) return;
}
setState(() => _isDownloading = true);
var extName = extension(widget.item.name);
if(extName.isEmpty) extName = '.png';
final imagePath =
'${Directory.systemTemp.path}/${widget.item.uuid}$extName';
await Dio().download(
url,
imagePath,
onReceiveProgress: (count, total) {
setState(() => _progressOfDownload = count / total);
},
);
bool isSuccess = false;
try {
await Gal.putImage(imagePath);
isSuccess = true;
} on GalException catch (e) {
context.showErrorDialog(e.type.message);
}
context.showSnackbar(
'attachmentSaved'.tr,
action: SnackBarAction(
label: 'openInAlbum'.tr,
onPressed: () async => Gal.open(),
),
);
setState(() {
_isDownloading = false;
_isCompletedDownload = isSuccess;
});
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -60,8 +122,13 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final metaTextStyle = TextStyle(
fontSize: 12,
color: _unFocusColor,
);
return DismissiblePage( return DismissiblePage(
key: Key('attachment-dismissible${widget.attachment.id}'), key: Key('attachment-dismissible${widget.item.id}'),
direction: DismissiblePageDismissDirection.vertical, direction: DismissiblePageDismissDirection.vertical,
onDismissed: () => Navigator.pop(context), onDismissed: () => Navigator.pop(context),
dismissThresholds: const { dismissThresholds: const {
@ -89,7 +156,7 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
child: AttachmentItem( child: AttachmentItem(
parentId: widget.parentId, parentId: widget.parentId,
showHideButton: false, showHideButton: false,
item: widget.attachment, item: widget.item,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
), ),
@ -118,38 +185,66 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
bottom: math.max(MediaQuery.of(context).padding.bottom, 16), bottom: math.max(MediaQuery.of(context).padding.bottom, 16),
left: 16, left: 16,
right: 16, right: 16,
child: IgnorePointer(
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (widget.attachment.account != null) if (widget.item.account != null)
Row( Row(
children: [ children: [
AccountAvatar( IgnorePointer(
content: widget.attachment.account!.avatar, child: AccountAvatar(
content: widget.item.account!.avatar,
radius: 19, radius: 19,
), ),
const SizedBox(width: 8), ),
Column( const IgnorePointer(child: SizedBox(width: 8)),
Expanded(
child: IgnorePointer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'attachmentUploadBy'.tr, 'attachmentUploadBy'.tr,
style: Theme.of(context).textTheme.bodySmall, style:
Theme.of(context).textTheme.bodySmall,
), ),
Text( Text(
widget.attachment.account!.nick, widget.item.account!.nick,
style: Theme.of(context).textTheme.bodyMedium, style:
Theme.of(context).textTheme.bodyMedium,
), ),
], ],
), ),
),
),
IconButton(
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -2,
),
icon: !_isDownloading
? !_isCompletedDownload
? const Icon(Icons.save_alt)
: const Icon(Icons.download_done)
: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _progressOfDownload,
strokeWidth: 3,
),
),
onPressed:
_isDownloading ? null : () => _saveToAlbum(),
),
], ],
), ),
const SizedBox(height: 4), const IgnorePointer(child: SizedBox(height: 4)),
Text( IgnorePointer(
widget.attachment.alt, child: Text(
widget.item.alt,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
@ -157,39 +252,36 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
const SizedBox(height: 2), ),
Wrap( const IgnorePointer(child: SizedBox(height: 2)),
IgnorePointer(
child: Wrap(
spacing: 6, spacing: 6,
children: [ children: [
if (widget.attachment.metadata?['width'] != null && if (widget.item.metadata?['width'] != null &&
widget.attachment.metadata?['height'] != null) widget.item.metadata?['height'] != null)
Text( Text(
'${widget.attachment.metadata?['width']}x${widget.attachment.metadata?['height']}', '${widget.item.metadata?['width']}x${widget.item.metadata?['height']}',
style: TextStyle( style: metaTextStyle,
fontSize: 12,
color: _unFocusColor,
), ),
), if (widget.item.metadata?['ratio'] != null)
if (widget.attachment.metadata?['ratio'] != null)
Text( Text(
'${_getRatio().toPrecision(2)}', '${_getRatio().toPrecision(2)}',
style: TextStyle( style: metaTextStyle,
fontSize: 12,
color: _unFocusColor,
),
), ),
Text( Text(
_formatBytes(widget.attachment.size), _formatBytes(widget.item.size),
style: TextStyle( style: metaTextStyle,
fontSize: 12,
color: _unFocusColor,
), ),
) Text(
], widget.item.mimetype,
style: metaTextStyle,
), ),
], ],
), ),
), ),
],
),
), ),
) )
.animate(target: _showDetails ? 1 : 0) .animate(target: _showDetails ? 1 : 0)

View File

@ -8,7 +8,7 @@ import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart'; import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/markdown_text_content.dart'; import 'package:solian/widgets/markdown_text_content.dart';
import 'package:solian/widgets/feed/feed_tags.dart'; import 'package:solian/widgets/posts/post_tags.dart';
import 'package:solian/widgets/posts/post_quick_action.dart'; import 'package:solian/widgets/posts/post_quick_action.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:timeago/timeago.dart' show format; import 'package:timeago/timeago.dart' show format;
@ -129,7 +129,7 @@ class _PostItemState extends State<PostItem> {
List<Widget> widgets = List.empty(growable: true); List<Widget> widgets = List.empty(growable: true);
if (widget.item.tags?.isNotEmpty ?? false) { if (widget.item.tags?.isNotEmpty ?? false) {
widgets.add(FeedTagsList(tags: widget.item.tags!)); widgets.add(PostTagsList(tags: widget.item.tags!));
} }
if (labels.isNotEmpty) { if (labels.isNotEmpty) {
widgets.add(Text( widgets.add(Text(

View File

@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:solian/models/feed.dart'; import 'package:solian/models/feed.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
class FeedTagsList extends StatelessWidget { class PostTagsList extends StatelessWidget {
final List<Tag> tags; final List<Tag> tags;
const FeedTagsList({ const PostTagsList({
super.key, super.key,
required this.tags, required this.tags,
}); });

View File

@ -13,6 +13,7 @@ import firebase_core
import firebase_messaging import firebase_messaging
import flutter_secure_storage_macos import flutter_secure_storage_macos
import flutter_webrtc import flutter_webrtc
import gal
import livekit_client import livekit_client
import macos_window_utils import macos_window_utils
import media_kit_libs_macos_video import media_kit_libs_macos_video
@ -38,6 +39,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))

View File

@ -321,6 +321,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
dio:
dependency: "direct main"
description:
name: dio
sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714
url: "https://pub.dev"
source: hosted
version: "5.5.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
dismissible_page: dismissible_page:
dependency: "direct main" dependency: "direct main"
description: description:
@ -672,6 +688,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
gal:
dependency: "direct main"
description:
name: gal
sha256: "54c9b72528efce7c66234f3b6dd01cb0304fd8af8196de15571d7bdddb940977"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
get: get:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -62,6 +62,8 @@ dependencies:
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
easy_debounce: ^2.0.3 easy_debounce: ^2.0.3
provider: ^6.1.2 provider: ^6.1.2
gal: ^2.3.0
dio: ^5.5.0+1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -13,6 +13,7 @@
#include <flutter_acrylic/flutter_acrylic_plugin.h> #include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h> #include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <gal/gal_plugin_c_api.h>
#include <livekit_client/live_kit_plugin.h> #include <livekit_client/live_kit_plugin.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.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 <media_kit_video/media_kit_video_plugin_c_api.h>
@ -39,6 +40,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
FlutterWebRTCPluginRegisterWithRegistrar( FlutterWebRTCPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
GalPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GalPluginCApi"));
LiveKitPluginRegisterWithRegistrar( LiveKitPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LiveKitPlugin")); registry->GetRegistrarForPlugin("LiveKitPlugin"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(

View File

@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_acrylic flutter_acrylic
flutter_secure_storage_windows flutter_secure_storage_windows
flutter_webrtc flutter_webrtc
gal
livekit_client livekit_client
media_kit_libs_windows_video media_kit_libs_windows_video
media_kit_video media_kit_video