✨ Download attachment
This commit is contained in:
@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
extension SolianExtenions on BuildContext {
|
||||
void showSnackbar(String content) {
|
||||
void showSnackbar(String content, {SnackBarAction? action}) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
|
||||
content: Text(content),
|
||||
action: action,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -33,6 +33,7 @@ const i18nEnglish = {
|
||||
'article': 'Article',
|
||||
'reply': 'Reply',
|
||||
'repost': 'Repost',
|
||||
'openInAlbum': 'Open in album',
|
||||
'openInBrowser': 'Open in browser',
|
||||
'notification': 'Notification',
|
||||
'errorHappened': 'An error occurred',
|
||||
@ -313,4 +314,5 @@ const i18nEnglish = {
|
||||
'themeColorKagamine': 'Kagamine Yellow',
|
||||
'themeColorLuka': 'Luka Pink',
|
||||
'themeColorApplied': 'Global theme color has been applied.',
|
||||
'attachmentSaved': 'Attachment saved to your system album.',
|
||||
};
|
||||
|
@ -33,6 +33,7 @@ const i18nSimplifiedChinese = {
|
||||
'article': '文章',
|
||||
'reply': '回复',
|
||||
'repost': '转帖',
|
||||
'openInAlbum': '在相簿中打开',
|
||||
'openInBrowser': '在浏览器中打开',
|
||||
'notification': '通知',
|
||||
'errorHappened': '发生错误了',
|
||||
@ -290,4 +291,5 @@ const i18nSimplifiedChinese = {
|
||||
'themeColorKagamine': '镜音黄',
|
||||
'themeColorLuka': '流音粉',
|
||||
'themeColorApplied': '全局主题颜色已应用',
|
||||
'attachmentSaved': '附件已保存到系统相册',
|
||||
};
|
||||
|
@ -322,7 +322,7 @@ class AttachmentListEntry extends StatelessWidget {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentListFullScreen(
|
||||
parentId: parentId,
|
||||
attachment: item!,
|
||||
item: item!,
|
||||
),
|
||||
rootNavigator: true,
|
||||
);
|
||||
|
@ -1,19 +1,27 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.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/attachments/attachment_item.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:path/path.dart' show extension;
|
||||
|
||||
class AttachmentListFullScreen extends StatefulWidget {
|
||||
final String parentId;
|
||||
final Attachment attachment;
|
||||
final Attachment item;
|
||||
|
||||
const AttachmentListFullScreen(
|
||||
{super.key, required this.parentId, required this.attachment});
|
||||
{super.key, required this.parentId, required this.item});
|
||||
|
||||
@override
|
||||
State<AttachmentListFullScreen> createState() =>
|
||||
@ -23,6 +31,10 @@ class AttachmentListFullScreen extends StatefulWidget {
|
||||
class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
|
||||
bool _showDetails = true;
|
||||
|
||||
bool _isDownloading = false;
|
||||
bool _isCompletedDownload = false;
|
||||
double? _progressOfDownload = 0;
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
@ -46,13 +58,63 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
|
||||
}
|
||||
|
||||
double _getRatio() {
|
||||
final value = widget.attachment.metadata?['ratio'];
|
||||
final value = widget.item.metadata?['ratio'];
|
||||
if (value == null) return 1;
|
||||
if (value is int) return value.toDouble();
|
||||
if (value is double) return value;
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -60,8 +122,13 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final metaTextStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unFocusColor,
|
||||
);
|
||||
|
||||
return DismissiblePage(
|
||||
key: Key('attachment-dismissible${widget.attachment.id}'),
|
||||
key: Key('attachment-dismissible${widget.item.id}'),
|
||||
direction: DismissiblePageDismissDirection.vertical,
|
||||
onDismissed: () => Navigator.pop(context),
|
||||
dismissThresholds: const {
|
||||
@ -89,7 +156,7 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
|
||||
child: AttachmentItem(
|
||||
parentId: widget.parentId,
|
||||
showHideButton: false,
|
||||
item: widget.attachment,
|
||||
item: widget.item,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
@ -118,38 +185,66 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
|
||||
bottom: math.max(MediaQuery.of(context).padding.bottom, 16),
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: IgnorePointer(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.attachment.account != null)
|
||||
Row(
|
||||
children: [
|
||||
AccountAvatar(
|
||||
content: widget.attachment.account!.avatar,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.item.account != null)
|
||||
Row(
|
||||
children: [
|
||||
IgnorePointer(
|
||||
child: AccountAvatar(
|
||||
content: widget.item.account!.avatar,
|
||||
radius: 19,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'attachmentUploadBy'.tr,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
widget.attachment.account!.nick,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const IgnorePointer(child: SizedBox(width: 8)),
|
||||
Expanded(
|
||||
child: IgnorePointer(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'attachmentUploadBy'.tr,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
widget.item.account!.nick,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.attachment.alt,
|
||||
),
|
||||
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 IgnorePointer(child: SizedBox(height: 4)),
|
||||
IgnorePointer(
|
||||
child: Text(
|
||||
widget.item.alt,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
@ -157,38 +252,35 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Wrap(
|
||||
),
|
||||
const IgnorePointer(child: SizedBox(height: 2)),
|
||||
IgnorePointer(
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (widget.attachment.metadata?['width'] != null &&
|
||||
widget.attachment.metadata?['height'] != null)
|
||||
if (widget.item.metadata?['width'] != null &&
|
||||
widget.item.metadata?['height'] != null)
|
||||
Text(
|
||||
'${widget.attachment.metadata?['width']}x${widget.attachment.metadata?['height']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
'${widget.item.metadata?['width']}x${widget.item.metadata?['height']}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (widget.attachment.metadata?['ratio'] != null)
|
||||
if (widget.item.metadata?['ratio'] != null)
|
||||
Text(
|
||||
'${_getRatio().toPrecision(2)}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
style: metaTextStyle,
|
||||
),
|
||||
Text(
|
||||
_formatBytes(widget.attachment.size),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
)
|
||||
_formatBytes(widget.item.size),
|
||||
style: metaTextStyle,
|
||||
),
|
||||
Text(
|
||||
widget.item.mimetype,
|
||||
style: metaTextStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -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/attachments/attachment_list.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/sized_container.dart';
|
||||
import 'package:timeago/timeago.dart' show format;
|
||||
@ -129,7 +129,7 @@ class _PostItemState extends State<PostItem> {
|
||||
List<Widget> widgets = List.empty(growable: true);
|
||||
|
||||
if (widget.item.tags?.isNotEmpty ?? false) {
|
||||
widgets.add(FeedTagsList(tags: widget.item.tags!));
|
||||
widgets.add(PostTagsList(tags: widget.item.tags!));
|
||||
}
|
||||
if (labels.isNotEmpty) {
|
||||
widgets.add(Text(
|
||||
|
@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:solian/models/feed.dart';
|
||||
import 'package:solian/router.dart';
|
||||
|
||||
class FeedTagsList extends StatelessWidget {
|
||||
class PostTagsList extends StatelessWidget {
|
||||
final List<Tag> tags;
|
||||
|
||||
const FeedTagsList({
|
||||
const PostTagsList({
|
||||
super.key,
|
||||
required this.tags,
|
||||
});
|
Reference in New Issue
Block a user