From f7dbf71759a6c7bd1ae9130d070e3c8ec0b9f7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B8=D0=BC=20=D0=97=D0=B0=D0=BD=D0=B8=D0=B1?= =?UTF-8?q?=D0=B5=D0=BA=D0=BE=D0=B2?= Date: Fri, 4 Mar 2022 02:22:11 +0300 Subject: [PATCH] Posts and images sharing --- lib/app/auth/auth.dart | 178 ++++++++++++----------- lib/app/content/content.dart | 25 +++- lib/app/image-gallery/image-gallery.dart | 26 +++- lib/app/post/post-controls.dart | 68 ++++++--- lib/core/common/menu.dart | 4 + lib/core/common/retry-network-image.dart | 7 +- pubspec.lock | 52 ++++++- pubspec.yaml | 8 +- 8 files changed, 245 insertions(+), 123 deletions(-) diff --git a/lib/app/auth/auth.dart b/lib/app/auth/auth.dart index c129c37..0d613bf 100644 --- a/lib/app/auth/auth.dart +++ b/lib/app/auth/auth.dart @@ -26,106 +26,108 @@ class _AppAuthPageState extends State { return Scaffold( appBar: AppBar(title: const Text('Аворизация')), body: Center( - child: Padding( - padding: const EdgeInsets.all(36.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - height: 45.0, - child: Align( - alignment: Alignment.centerLeft, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 150), - opacity: _error ? 1 : 0, - child: Text( - _errorMessage ?? 'Неверное имя пользователя или пароль', - style: style.copyWith( - color: Theme.of(context).errorColor, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(36.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 45.0, + child: Align( + alignment: Alignment.centerLeft, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 150), + opacity: _error ? 1 : 0, + child: Text( + _errorMessage ?? 'Неверное имя пользователя или пароль', + style: style.copyWith( + color: Theme.of(context).errorColor, + ), ), ), ), ), - ), - TextFormField( - controller: _usernameEditingController, - obscureText: false, - validator: (value) => (value?.isEmpty ?? true) - ? 'Введите имя пользователя' - : null, - decoration: const InputDecoration( - contentPadding: EdgeInsets.fromLTRB(10.0, 15.0, 10.0, 15.0), - hintText: 'Имя пользователя', + TextFormField( + controller: _usernameEditingController, + obscureText: false, + validator: (value) => (value?.isEmpty ?? true) + ? 'Введите имя пользователя' + : null, + decoration: const InputDecoration( + contentPadding: EdgeInsets.fromLTRB(10.0, 15.0, 10.0, 15.0), + hintText: 'Имя пользователя', + ), ), - ), - const SizedBox(height: 25.0), - TextFormField( - controller: _passwordEditingController, - obscureText: true, - validator: (value) => - (value?.isEmpty ?? true) ? 'Введите пароль' : null, - decoration: const InputDecoration( - contentPadding: EdgeInsets.fromLTRB(10.0, 15.0, 10.0, 15.0), - hintText: 'Пароль', + const SizedBox(height: 25.0), + TextFormField( + controller: _passwordEditingController, + obscureText: true, + validator: (value) => + (value?.isEmpty ?? true) ? 'Введите пароль' : null, + decoration: const InputDecoration( + contentPadding: EdgeInsets.fromLTRB(10.0, 15.0, 10.0, 15.0), + hintText: 'Пароль', + ), ), - ), - const SizedBox(height: 35.0), - Container( - width: double.infinity, - child: OutlinedButton( - onPressed: () async { - if (_formKey.currentState?.validate() ?? false) { - FocusScope.of(context).requestFocus(FocusNode()); - setState(() => _loading = true); - try { - await Auth().login( - _usernameEditingController.value.text, - _passwordEditingController.value.text); - _loading = false; - AppPages.appBottomBarPage.add(AppBottomBarPage.MAIN); - } catch (err) { - if (err is InvalidUsernameOrPasswordException) { - _errorMessage = - 'Неверное имя пользователя или пароль'; - } else if (err is RateLimitException) { - _errorMessage = 'Превышен лимит обращений'; - } else if (err is InvalidStatusCodeException) { - _errorMessage = - 'Что то пошло не так, повторите позже'; - } - setState(() { - _error = true; + const SizedBox(height: 35.0), + Container( + width: double.infinity, + child: OutlinedButton( + onPressed: () async { + if (_formKey.currentState?.validate() ?? false) { + FocusScope.of(context).requestFocus(FocusNode()); + setState(() => _loading = true); + try { + await Auth().login( + _usernameEditingController.value.text, + _passwordEditingController.value.text); _loading = false; - }); + AppPages.appBottomBarPage.add(AppBottomBarPage.MAIN); + } catch (err) { + if (err is InvalidUsernameOrPasswordException) { + _errorMessage = + 'Неверное имя пользователя или пароль'; + } else if (err is RateLimitException) { + _errorMessage = 'Превышен лимит обращений'; + } else if (err is InvalidStatusCodeException) { + _errorMessage = + 'Что то пошло не так, повторите позже'; + } + setState(() { + _error = true; + _loading = false; + }); + } } - } - }, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 150), - child: IndexedStack( - key: ValueKey(_loading ? 1 : 0), - index: _loading ? 1 : 0, - children: [ - Center(child: Text('Войти', style: style)), - const Center( - child: SizedBox( - child: CircularProgressIndicator( - strokeWidth: 1.5, + }, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + child: IndexedStack( + key: ValueKey(_loading ? 1 : 0), + index: _loading ? 1 : 0, + children: [ + Center(child: Text('Войти', style: style)), + const Center( + child: SizedBox( + child: CircularProgressIndicator( + strokeWidth: 1.5, + ), + height: 16, + width: 16, ), - height: 16, - width: 16, - ), - ) - ], + ) + ], + ), ), ), ), - ), - const SizedBox(height: 15.0), - ], + const SizedBox(height: 15.0), + ], + ), ), ), ), diff --git a/lib/app/content/content.dart b/lib/app/content/content.dart index b508cc4..5695a37 100644 --- a/lib/app/content/content.dart +++ b/lib/app/content/content.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:share_plus/share_plus.dart'; import '../../core/common/clipboard.dart'; import '../../core/common/menu.dart'; @@ -100,7 +101,8 @@ class AppContentLoader { } } - _filteredFutures = _futures.where((it) => it != null).map((it) => it!).toList(); + _filteredFutures = + _futures.where((it) => it != null).map((it) => it!).toList(); if (_undefinedSizeImages.isEmpty) { if (_filteredFutures.isNotEmpty) { @@ -341,8 +343,26 @@ class _AppContentState extends State { onSelect: () { SaveFile.downloadAndSave( context, image.prettyImageLink ?? image.value); - }), + }) ]); + final checkMenu = () async { + final exist = await widget.loader.images[index].existInCache(); + if (!exist || menu.items.any((it) => it.text == "Поделиться")) { + return; + } + + menu.addItem(MenuItem( + text: "Поделиться", + onSelect: () async { + final image = widget.loader.images[index]; + final file = await image.loadFromDiskCache(); + if (file != null) { + Share.shareFiles([file.path]); + } + }, + )); + }; + checkMenu(); return GestureDetector( onLongPress: () { menu.openUnderTap(pos); @@ -360,6 +380,7 @@ class _AppContentState extends State { child: AppSafeImage( imageProvider: widget.loader.images[index], onInfo: (e) { + checkMenu(); if (widget.loader.onImageInfo(e, image)) { if (mounted) setState(() {}); } diff --git a/lib/app/image-gallery/image-gallery.dart b/lib/app/image-gallery/image-gallery.dart index 975c8d8..2d4f900 100644 --- a/lib/app/image-gallery/image-gallery.dart +++ b/lib/app/image-gallery/image-gallery.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:vector_math/vector_math_64.dart' as vector; import '../../core/common/menu.dart'; @@ -303,17 +304,30 @@ class _ImageGalleryState extends State SnackBarHelper.show(context, 'Дождитесь загрузки изображения'); return; } - final rawBytes = - await (_activeImage.imageProvider as AppNetworkImageWithRetry) - .loadFromDiskCache(); - final url = - (_activeImage.imageProvider as AppNetworkImageWithRetry).url; + final image = + _activeImage.imageProvider as AppNetworkImageWithRetry; + final rawBytes = await image.loadContentsFromDiskCache(); + final url = image.url; if (rawBytes != null) { await SaveFile.save(context, url, rawBytes); } else { SnackBarHelper.show(context, 'Не удалось загрузить изображение'); } - }) + }), + MenuItem( + text: "Поделиться", + onSelect: () async { + if (_activeImage.info == null) { + SnackBarHelper.show(context, 'Дождитесь загрузки изображения'); + return; + } + final image = _activeImage.imageProvider as AppNetworkImageWithRetry; + final file = await image.loadFromDiskCache(); + if (file != null) { + Share.shareFiles([file.path]); + } + }, + ) ]); return PopupMenuButton( diff --git a/lib/app/post/post-controls.dart b/lib/app/post/post-controls.dart index a67b97b..938a03d 100644 --- a/lib/app/post/post-controls.dart +++ b/lib/app/post/post-controls.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:share_plus/share_plus.dart'; import '../../core/api/api.dart'; import '../../core/api/types.dart'; import '../../core/auth/auth.dart'; import '../../core/common/snack-bar.dart'; import '../../core/parsers/types/module.dart'; +import '../../core/preferences/preferences.dart'; class AppPostControls extends StatefulWidget { final Post post; @@ -78,10 +80,14 @@ class _AppPostControlsState extends State { } } + get _link { + return 'http://${Preferences().host}/post/${widget.post.id}'; + } + @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -101,40 +107,66 @@ class _AppPostControlsState extends State { ), if (_auth.authorized) Padding( - padding: const EdgeInsets.only(left: 15), + padding: const EdgeInsets.only(left: 10), child: InkWell( onTap: _toggleFavorite, - child: widget.post.favorite - ? Icon( - Icons.star, - color: Theme.of(context).colorScheme.secondary, - size: 22, - ) - : Icon(Icons.star_border, size: 22), + borderRadius: BorderRadius.circular(30), + child: Padding( + padding: const EdgeInsets.all(5), + child: widget.post.favorite + ? Icon( + Icons.star, + color: Theme.of(context).colorScheme.secondary, + size: 22, + ) + : Icon(Icons.star_border, size: 22), + ), ), - ) + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: InkWell( + onTap: () async { + await Share.share(_link); + }, + borderRadius: BorderRadius.circular(30), + child: Padding( + padding: const EdgeInsets.all(5), + child: Icon(Icons.share, size: 22), + ), + ), + ) ]), if (_auth.authorized) Row(children: [ if (widget.post.canVote) Padding( - padding: EdgeInsets.only(right: 10), + padding: EdgeInsets.only(right: 5), child: InkWell( onTap: () => _vote(VoteType.UP), - child: widget.post.votedUp - ? Icon(Icons.mood, color: Colors.green[600], size: 22) - : Icon(Icons.mood, size: 22), + borderRadius: BorderRadius.circular(30), + child: Padding( + padding: const EdgeInsets.all(5), + child: widget.post.votedUp + ? Icon(Icons.mood, color: Colors.green[600], size: 22) + : Icon(Icons.mood, size: 22), + ), ), ), Text(widget.post.rating?.toString() ?? '––'), if (widget.post.canVote) Padding( - padding: EdgeInsets.only(left: 10), + padding: EdgeInsets.only(left: 5), child: InkWell( onTap: () => _vote(VoteType.DOWN), - child: widget.post.votedDown - ? Icon(Icons.mood_bad, color: Colors.red[600], size: 22) - : Icon(Icons.mood_bad, size: 22), + borderRadius: BorderRadius.circular(30), + child: Padding( + padding: const EdgeInsets.all(5), + child: widget.post.votedDown + ? Icon(Icons.mood_bad, + color: Colors.red[600], size: 22) + : Icon(Icons.mood_bad, size: 22), + ), ), ), ]) diff --git a/lib/core/common/menu.dart b/lib/core/common/menu.dart index d068a7f..8cf0d2e 100644 --- a/lib/core/common/menu.dart +++ b/lib/core/common/menu.dart @@ -21,6 +21,10 @@ class Menu { ) ]; + void addItem(MenuItem item) { + items.add(item); + } + void process(int? index) { if (index != null) { items[index].onSelect(); diff --git a/lib/core/common/retry-network-image.dart b/lib/core/common/retry-network-image.dart index 81c39bd..4475b46 100644 --- a/lib/core/common/retry-network-image.dart +++ b/lib/core/common/retry-network-image.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:typed_data'; import 'package:advanced_image/cache.dart'; @@ -35,10 +36,14 @@ class AppNetworkImageWithRetry extends io.AdvancedNetworkImage { return _cacheManager.has(this.url.hashCode.toString()); } - Future loadFromDiskCache() { + Future loadContentsFromDiskCache() { return _cacheManager.get(this.url.hashCode.toString()); } + Future loadFromDiskCache() { + return _cacheManager.getFile(this.url.hashCode.toString()); + } + static isUrlExistInCache(String url) async { return _cacheManager.has(url.hashCode.toString()); } diff --git a/pubspec.lock b/pubspec.lock index 212dad5..8ca734b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,7 +13,7 @@ packages: description: path: "." ref: master - resolved-ref: "9dc46192066b9aca0f3506238d3a8134a83e54d8" + resolved-ref: "584ab843b0b6b59423263b03367706fbeedb360f" url: "https://github.com/AlimZanibekov/advanced_image.git" source: git version: "0.0.5" @@ -212,7 +212,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "4.5.0" flutter: dependency: "direct main" description: flutter @@ -231,7 +231,7 @@ packages: name: flutter_local_notifications url: "https://pub.dartlang.org" source: hosted - version: "9.3.2" + version: "9.3.3" flutter_local_notifications_linux: dependency: transitive description: @@ -505,14 +505,14 @@ packages: name: permission_handler_android url: "https://pub.dartlang.org" source: hosted - version: "9.0.2" + version: "9.0.2+1" permission_handler_apple: dependency: transitive description: name: permission_handler_apple url: "https://pub.dartlang.org" source: hosted - version: "9.0.2" + version: "9.0.3" permission_handler_platform_interface: dependency: transitive description: @@ -590,6 +590,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.3.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + share_plus_linux: + dependency: transitive + description: + name: share_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + share_plus_macos: + dependency: transitive + description: + name: share_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + share_plus_web: + dependency: transitive + description: + name: share_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + share_plus_windows: + dependency: transitive + description: + name: share_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7bc60f0..ff38736 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: reactor description: JoyReactor client. -version: 0.7.5 +version: 0.7.6 environment: sdk: '>=2.14.0 <3.0.0' @@ -27,11 +27,13 @@ dependencies: scrollable_positioned_list: ^0.2.3 uni_links: ^0.5.1 sentry: ^6.3.0 - file_picker: ^4.4.0 + file_picker: ^4.5.0 device_info_plus: ^3.2.2 package_info_plus: ^1.4.0 path_provider: ^2.0.9 - flutter_local_notifications: ^9.3.2 + flutter_local_notifications: ^9.3.3 + share_plus: ^3.1.0 + flutter: sdk: flutter flutter_localizations: