Skip to content

Commit

Permalink
feat:detail thumbnail (#176)
Browse files Browse the repository at this point in the history
* feat:detail thumbnail

* push

* Add legacy external storage support and save image functionality

* Add localization for save button

---------

Co-authored-by: MiaoMint <miaomint@0u0.ren>
  • Loading branch information
appdevelpo and MiaoMint committed Feb 7, 2024
1 parent 64a694a commit 68d8519
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 17 deletions.
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<application
android:label="Miru"
android:name="${applicationName}"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
Expand Down
4 changes: 3 additions & 1 deletion assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
"previous": "Previous",
"show-all": "Show all",
"delete": "Delete",
"delete-all": "Delete all"
"delete-all": "Delete all",
"save": "Save",
"save-success": "Save success"
},
"home": {
"continue-watching": "Continue",
Expand Down
4 changes: 3 additions & 1 deletion assets/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"previous": "上一个",
"show-all": "显示全部",
"delete": "删除",
"delete-all": "删除全部"
"delete-all": "删除全部",
"save": "保存",
"save-success": "保存成功"
},
"home": {
"continue-watching": "继续观看",
Expand Down
26 changes: 12 additions & 14 deletions lib/views/pages/detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,6 @@ class _DetailPageState extends State<DetailPage> {
child: ProgressRing(),
);
}

return Stack(
children: [
Animate(
Expand Down Expand Up @@ -281,19 +280,17 @@ class _DetailPageState extends State<DetailPage> {
children: [
if (c.detail!.cover != null)
if (constraints.maxWidth > 600) ...[
Hero(
tag: c.heroTag ?? '',
child: Container(
width: 230,
height: double.infinity,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: CacheNetWorkImagePic(
c.detail?.cover ?? '',
headers: c.detail?.headers,
),
Container(
width: 230,
height: double.infinity,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: CacheNetWorkImagePic(
c.detail?.cover ?? '',
headers: c.detail?.headers,
canFullScreen: true,
),
),
const SizedBox(width: 30),
Expand Down Expand Up @@ -386,6 +383,7 @@ class _DetailPageState extends State<DetailPage> {
child: CacheNetWorkImagePic(
url,
height: 200,
canFullScreen: true,
),
);
},
Expand Down
208 changes: 207 additions & 1 deletion lib/views/widgets/cache_network_image.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import 'dart:io';

import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
import 'package:extended_image/extended_image.dart';
import 'package:file_picker/file_picker.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluent;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:miru_app/utils/i18n.dart';
import 'package:miru_app/views/widgets/messenger.dart';
import 'package:miru_app/views/widgets/platform_widget.dart';

class CacheNetWorkImagePic extends StatelessWidget {
const CacheNetWorkImagePic(
Expand All @@ -11,13 +21,15 @@ class CacheNetWorkImagePic extends StatelessWidget {
this.height,
this.fallback,
this.headers,
this.canFullScreen = false,
});
final String url;
final BoxFit fit;
final double? width;
final double? height;
final Widget? fallback;
final Map<String, String>? headers;
final bool canFullScreen;

_errorBuild() {
if (fallback != null) {
Expand All @@ -28,13 +40,207 @@ class CacheNetWorkImagePic extends StatelessWidget {

@override
Widget build(BuildContext context) {
return CachedNetworkImage(
final image = CachedNetworkImage(
imageUrl: url,
httpHeaders: headers,
fit: fit,
width: width,
height: height,
errorWidget: (context, url, error) => _errorBuild(),
);

if (canFullScreen) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
final thumnailPage = _ThumnailPage(
image: CachedNetworkImageProvider(
url,
headers: headers,
),
);
if (Platform.isAndroid) {
Get.to(thumnailPage);
return;
}
fluent.showDialog(
context: context,
builder: (_) => thumnailPage,
);
},
child: image,
),
);
}

return image;
}
}

class _ThumnailPage extends StatefulWidget {
const _ThumnailPage({
required this.image,
});
final CachedNetworkImageProvider image;

@override
State<_ThumnailPage> createState() => _ThumnailPageState();
}

class _ThumnailPageState extends State<_ThumnailPage> {
final menuController = fluent.FlyoutController();
final contextAttachKey = GlobalKey();

@override
dispose() {
menuController.dispose();
super.dispose();
}

_saveImage() async {
final url = widget.image.url;
final fileName = url.split('/').last;
final res = await Dio().get(
url,
options: Options(
responseType: ResponseType.bytes,
headers: widget.image.headers,
),
);
if (Platform.isAndroid) {
final result = await ImageGallerySaver.saveImage(
res.data,
name: fileName,
);
if (mounted) {
final msg = result['isSuccess'] == true
? "common.save-success"
: result['errorMessage'];
showPlatformSnackbar(
context: context,
content: msg,
);
}
return;
}
// 打开目录选择对话框file_picker

final path = await FilePicker.platform.saveFile(
type: FileType.image,
fileName: fileName,
);
if (path == null) {
return;
}
// 保存
File(path).writeAsBytesSync(res.data);
}

Widget _buildContent(BuildContext context) {
return Center(
child: ExtendedImageSlidePage(
slideAxis: SlideAxis.both,
slideType: SlideType.onlyImage,
slidePageBackgroundHandler: (offset, pageSize) {
final color = Platform.isAndroid
? Theme.of(context).scaffoldBackgroundColor
: fluent.FluentTheme.of(context).scaffoldBackgroundColor;
return color.withOpacity(0);
},
child: ExtendedImage(
image: widget.image,
fit: BoxFit.contain,
mode: ExtendedImageMode.gesture,
initGestureConfigHandler: (state) {
return GestureConfig(
minScale: 0.9,
animationMinScale: 0.7,
maxScale: 3.0,
animationMaxScale: 3.5,
speed: 1.0,
inertialSpeed: 100.0,
initialScale: 1.0,
inPageView: true,
reverseMousePointerScrollDirection: true,
initialAlignment: InitialAlignment.center,
);
},
),
),
);
}

Widget _buildAndroid(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: GestureDetector(
child: _buildContent(context),
onLongPress: () {
showModalBottomSheet(
context: context,
showDragHandle: true,
useSafeArea: true,
builder: (_) => SizedBox(
height: 100,
child: Column(
children: [
ListTile(
leading: const Icon(Icons.save),
title: Text('common.save'.i18n),
onTap: () {
Navigator.of(context).pop();
_saveImage();
},
),
],
),
),
);
},
),
);
}

Widget _buildDesktop(BuildContext context) {
return GestureDetector(
onSecondaryTapUp: (d) {
final targetContext = contextAttachKey.currentContext;
if (targetContext == null) return;
final box = targetContext.findRenderObject() as RenderBox;
final position = box.localToGlobal(
d.localPosition,
ancestor: Navigator.of(context).context.findRenderObject(),
);
menuController.showFlyout(
position: position,
builder: (context) {
return fluent.MenuFlyout(items: [
fluent.MenuFlyoutItem(
leading: const Icon(fluent.FluentIcons.save),
text: Text('common.save'.i18n),
onPressed: () {
fluent.Flyout.of(context).close();
_saveImage();
},
),
]);
},
);
},
child: fluent.FlyoutTarget(
key: contextAttachKey,
controller: menuController,
child: _buildContent(context),
),
);
}

@override
Widget build(BuildContext context) {
return PlatformBuildWidget(
androidBuilder: _buildAndroid,
desktopBuilder: _buildDesktop,
);
}
}
1 change: 1 addition & 0 deletions lib/views/widgets/detail/detail_appbar_flexible_space.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class _DetailAppbarflexibleSpaceState extends State<DetailAppbarflexibleSpace> {
c.data.value?.cover ?? '',
fit: BoxFit.cover,
headers: c.detail?.headers,
canFullScreen: true,
),
),
),
Expand Down
1 change: 1 addition & 0 deletions lib/views/widgets/detail/detail_overview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class DetailOverView extends StatelessWidget {
child: CacheNetWorkImagePic(
url,
height: 160,
canFullScreen: true,
),
);
},
Expand Down
8 changes: 8 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.3"
image_gallery_saver:
dependency: "direct main"
description:
name: image_gallery_saver
sha256: "0aba74216a4d9b0561510cb968015d56b701ba1bd94aace26aacdd8ae5761816"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
intl:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ dependencies:
crypto: ^3.0.3
extended_image: ^8.2.0
flutter_hls_parser: ^2.0.1
image_gallery_saver: ^2.0.3

dev_dependencies:
flutter_test:
Expand Down

0 comments on commit 68d8519

Please sign in to comment.