diff --git a/lib/pages/community.dart b/lib/pages/community.dart index 6b877fc..0f14cc2 100644 --- a/lib/pages/community.dart +++ b/lib/pages/community.dart @@ -13,6 +13,7 @@ import '../util/intl.dart'; import '../util/text_color.dart'; import '../widgets/badge.dart'; import '../widgets/bottom_modal.dart'; +import '../widgets/fullscreenable_image.dart'; import '../widgets/markdown_text.dart'; class CommunityPage extends HookWidget { @@ -251,16 +252,20 @@ class _CommunityOverview extends StatelessWidget { Container( width: 83, height: 83, - child: CachedNetworkImage( - imageUrl: community.icon, - imageBuilder: (context, imageProvider) => Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - image: DecorationImage( - fit: BoxFit.cover, - image: imageProvider, + child: FullscreenableImage( + url: community.icon, + child: CachedNetworkImage( + imageUrl: community.icon, + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.cover, + image: imageProvider, + ), ), ), + errorWidget: (_, __, ___) => Icon(Icons.warning), ), ), ), @@ -271,7 +276,13 @@ class _CommunityOverview extends StatelessWidget { return Stack(children: [ if (community.banner != null) - CachedNetworkImage(imageUrl: community.banner), + FullscreenableImage( + url: community.banner, + child: CachedNetworkImage( + imageUrl: community.banner, + errorWidget: (_, __, ___) => Container(), + ), + ), SafeArea( child: Padding( padding: const EdgeInsets.only(top: 45), diff --git a/lib/pages/instance.dart b/lib/pages/instance.dart index 7d3f3a0..d8ec37f 100644 --- a/lib/pages/instance.dart +++ b/lib/pages/instance.dart @@ -11,6 +11,7 @@ import '../util/goto.dart'; import '../util/text_color.dart'; import '../widgets/badge.dart'; import '../widgets/bottom_modal.dart'; +import '../widgets/fullscreenable_image.dart'; import '../widgets/markdown_text.dart'; import 'communities_list.dart'; import 'users_list.dart'; @@ -159,17 +160,29 @@ class InstancePage extends HookWidget { flexibleSpace: FlexibleSpaceBar( background: Stack(children: [ if (site.site.banner != null) - CachedNetworkImage(imageUrl: site.site.banner), + FullscreenableImage( + url: site.site.banner, + child: CachedNetworkImage( + imageUrl: site.site.banner, + errorWidget: (_, __, ___) => Container(), + ), + ), SafeArea( child: Center( child: Column( children: [ Padding( padding: const EdgeInsets.only(top: 40), - child: CachedNetworkImage( + child: FullscreenableImage( + url: site.site.icon, + child: CachedNetworkImage( width: 100, height: 100, - imageUrl: site.site.icon), + imageUrl: site.site.icon, + errorWidget: (_, __, ___) => + Icon(Icons.warning), + ), + ), ), Text(site.site.name, style: theme.textTheme.headline6), @@ -325,6 +338,8 @@ class _AboutTab extends HookWidget { height: 50, width: 50, imageUrl: e.icon, + errorWidget: (_, __, ___) => + SizedBox(width: 50, height: 50), imageBuilder: (context, imageProvider) => Container( decoration: BoxDecoration( shape: BoxShape.circle, @@ -373,6 +388,8 @@ class _AboutTab extends HookWidget { height: 50, width: 50, imageUrl: e.avatar, + errorWidget: (_, __, ___) => + SizedBox(width: 50, height: 50), imageBuilder: (context, imageProvider) => Container( decoration: BoxDecoration( shape: BoxShape.circle, diff --git a/lib/pages/media_view.dart b/lib/pages/media_view.dart new file mode 100644 index 0000000..9cb42ec --- /dev/null +++ b/lib/pages/media_view.dart @@ -0,0 +1,118 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:esys_flutter_share/esys_flutter_share.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:photo_view/photo_view.dart'; + +import '../widgets/bottom_modal.dart'; + +class MediaViewPage extends HookWidget { + final String url; + + const MediaViewPage(this.url); + + @override + Widget build(BuildContext context) { + final showButtons = useState(true); + final isZoomedOut = useState(true); + + useEffect(() { + if (showButtons.value) { + SystemChrome.setEnabledSystemUIOverlays([ + SystemUiOverlay.bottom, + SystemUiOverlay.top, + ]); + } else { + SystemChrome.setEnabledSystemUIOverlays([]); + } + return null; + }, [showButtons.value]); + + useEffect( + () => () => SystemChrome.setEnabledSystemUIOverlays([ + SystemUiOverlay.bottom, + SystemUiOverlay.top, + ]), + []); + + share() { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + builder: (context) => BottomModal( + child: Column( + children: [ + ListTile( + leading: Icon(Icons.link), + title: Text('Share link'), + onTap: () { + Navigator.of(context).pop(); + Share.text('Share image url', url, 'text/plain'); + }, + ), + ListTile( + leading: Icon(Icons.image), + title: Text('Share file'), + onTap: () { + Navigator.of(context).pop(); + // TODO: share file + }, + ), + ], + ), + ), + ); + } + + return Scaffold( + extendBodyBehindAppBar: true, + extendBody: true, + appBar: showButtons.value + ? AppBar( + backgroundColor: Colors.black38, + shadowColor: Colors.transparent, + leading: CloseButton(), + actions: [ + IconButton( + icon: Icon(Icons.share), + tooltip: 'share', + onPressed: share, + ), + IconButton( + icon: Icon(Icons.file_download), + tooltip: 'download', + onPressed: () {}, + ), + ], + ) + : null, + body: Dismissible( + direction: DismissDirection.vertical, + onDismissed: (_) => Navigator.of(context).pop(), + key: const Key('media view'), + background: Container(color: Colors.black), + dismissThresholds: { + DismissDirection.vertical: 0, + }, + confirmDismiss: (direction) => Future.value(isZoomedOut.value), + resizeDuration: null, + child: GestureDetector( + onTapUp: (details) => showButtons.value = !showButtons.value, + child: PhotoView( + scaleStateChangedCallback: (value) { + isZoomedOut.value = value == PhotoViewScaleState.zoomedOut || + value == PhotoViewScaleState.initial; + }, + minScale: PhotoViewComputedScale.contained, + initialScale: PhotoViewComputedScale.contained, + imageProvider: CachedNetworkImageProvider(url), + heroAttributes: PhotoViewHeroAttributes(tag: url), + loadingBuilder: (context, event) => + Center(child: CircularProgressIndicator()), + ), + ), + ), + ); + } +} diff --git a/lib/pages/users_list.dart b/lib/pages/users_list.dart index 16b5259..332b43c 100644 --- a/lib/pages/users_list.dart +++ b/lib/pages/users_list.dart @@ -48,6 +48,8 @@ class UsersListPage extends StatelessWidget { height: 50, width: 50, imageUrl: users[i].avatar, + errorWidget: (_, __, ___) => + SizedBox(height: 50, width: 50), imageBuilder: (context, imageProvider) => Container( decoration: BoxDecoration( shape: BoxShape.circle, diff --git a/lib/widgets/comment.dart b/lib/widgets/comment.dart index 1ae29d6..206c9ec 100644 --- a/lib/widgets/comment.dart +++ b/lib/widgets/comment.dart @@ -200,6 +200,7 @@ class Comment extends StatelessWidget { ), ), ), + errorWidget: (_, __, ___) => Container(), ), ), ), diff --git a/lib/widgets/fullscreenable_image.dart b/lib/widgets/fullscreenable_image.dart new file mode 100644 index 0000000..5f23506 --- /dev/null +++ b/lib/widgets/fullscreenable_image.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import '../pages/media_view.dart'; + +class FullscreenableImage extends StatelessWidget { + final String url; + final Widget child; + + const FullscreenableImage({ + Key key, + @required this.url, + @required this.child, + }) : super(key: key); + + _onTap(BuildContext c) { + Navigator.of(c).push(MaterialPageRoute( + builder: (context) => MediaViewPage(url), + )); + } + + @override + Widget build(BuildContext context) => InkWell( + onTap: () => _onTap(context), + child: Hero( + tag: url, + child: child, + ), + ); +} diff --git a/lib/widgets/markdown_text.dart b/lib/widgets/markdown_text.dart index e0062ba..d3b4af2 100644 --- a/lib/widgets/markdown_text.dart +++ b/lib/widgets/markdown_text.dart @@ -4,6 +4,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:markdown/markdown.dart' as md; import '../url_launcher.dart'; +import 'fullscreenable_image.dart'; class MarkdownText extends StatelessWidget { final String instanceUrl; @@ -26,13 +27,16 @@ class MarkdownText extends StatelessWidget { ), ))); }, - imageBuilder: (uri, title, alt) => CachedNetworkImage( - imageUrl: uri.toString(), - errorWidget: (context, url, error) => Row( - children: [ - Icon(Icons.warning), - Text("couldn't load image, ${error.toString()}") - ], + imageBuilder: (uri, title, alt) => FullscreenableImage( + url: uri.toString(), + child: CachedNetworkImage( + imageUrl: uri.toString(), + errorWidget: (context, url, error) => Row( + children: [ + Icon(Icons.warning), + Text("couldn't load image, ${error.toString()}") + ], + ), ), ), ); diff --git a/lib/widgets/post.dart b/lib/widgets/post.dart index 4ffcd96..40614a9 100644 --- a/lib/widgets/post.dart +++ b/lib/widgets/post.dart @@ -12,6 +12,7 @@ import '../url_launcher.dart'; import '../util/api_extensions.dart'; import '../util/goto.dart'; import 'bottom_modal.dart'; +import 'fullscreenable_image.dart'; import 'markdown_text.dart'; enum MediaType { @@ -25,7 +26,13 @@ MediaType whatType(String url) { if (url == null) return MediaType.other; // TODO: make detection more nuanced - if (url.endsWith('.jpg') || url.endsWith('.png') || url.endsWith('.gif')) { + if (url.endsWith('.jpg') || + url.endsWith('.jpeg') || + url.endsWith('.png') || + url.endsWith('.gif') || + url.endsWith('.webp') || + url.endsWith('.bmp') || + url.endsWith('.wbpm')) { return MediaType.image; } return MediaType.other; @@ -40,13 +47,6 @@ class Post extends StatelessWidget { // == ACTIONS == - void _openFullImage() { - // TODO: fullscreen media view - print('OPEN FULL IMAGE'); - } - - // POST ACTIONS - void _savePost() { print('SAVE POST'); } @@ -370,10 +370,11 @@ class Post extends StatelessWidget { Widget postImage() { assert(post.url != null); - return InkWell( - onTap: _openFullImage, + return FullscreenableImage( + url: post.url, child: CachedNetworkImage( imageUrl: post.url, + errorWidget: (_, __, ___) => Icon(Icons.warning), progressIndicatorBuilder: (context, url, progress) => CircularProgressIndicator(value: progress.progress), ), diff --git a/lib/widgets/user_profile.dart b/lib/widgets/user_profile.dart index 346205d..f5afc3a 100644 --- a/lib/widgets/user_profile.dart +++ b/lib/widgets/user_profile.dart @@ -96,6 +96,7 @@ class UserProfile extends HookWidget { if (userViewSnap.data?.banner != null) CachedNetworkImage( imageUrl: userViewSnap.data.banner, + errorWidget: (_, __, ___) => Container(), ) else Container( @@ -154,6 +155,7 @@ class UserProfile extends HookWidget { borderRadius: BorderRadius.all(Radius.circular(12)), child: CachedNetworkImage( imageUrl: userViewSnap.data.avatar, + errorWidget: (_, __, ___) => Container(), ), ), ), diff --git a/pubspec.lock b/pubspec.lock index 1b60dcf..ca46e23 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -471,6 +471,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.9.0" + photo_view: + dependency: "direct main" + description: + name: photo_view + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.2" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e22a9d3..f511dea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ environment: sdk: '>=2.7.0 <3.0.0' dependencies: + photo_view: ^0.10.2 url_launcher: ^5.5.1 markdown: ^2.1.8 flutter_markdown: ^0.4.3 @@ -51,7 +52,6 @@ dev_dependencies: mobx_codegen: ^1.1.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec - # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is