diff --git a/.travis.yml b/.travis.yml index a147c08..b9665b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ addons: packages: - lib32stdc++6 install: - - git clone --depth 1 --branch 1.22.0 https://github.com/flutter/flutter.git + - git clone --depth 1 --branch 2.10.2 https://github.com/flutter/flutter.git - ./flutter/bin/flutter doctor - gem install coveralls-lcov - cp .env.example .env diff --git a/android/app/build.gradle b/android/app/build.gradle index e38ba2a..be5b94e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -48,6 +48,7 @@ android { applicationId "net.lucien144.fyx" minSdkVersion 19 targetSdkVersion 31 + //multiDexEnabled true // Uncomment for dev versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index efe69ad..046d749 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,9 @@ android:label="fyx" android:requestLegacyExternalStorage="true" android:icon="@mipmap/ic_launcher"> + 8.0) - GoogleUtilities/Environment (~> 7.6) - GoogleUtilities/Logger (~> 7.6) - - FirebaseCoreDiagnostics (8.10.0): + - FirebaseCoreDiagnostics (8.15.0): - GoogleDataTransport (~> 9.1) - - GoogleUtilities/Environment (~> 7.6) - - GoogleUtilities/Logger (~> 7.6) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) - nanopb (~> 2.30908.0) - - FirebaseInstallations (8.10.0): + - FirebaseInstallations (8.15.0): - FirebaseCore (~> 8.0) - - GoogleUtilities/Environment (~> 7.6) - - GoogleUtilities/UserDefaults (~> 7.6) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) - PromisesObjC (< 3.0, >= 1.2) - FirebaseMessaging (8.9.0): - FirebaseCore (~> 8.0) @@ -94,28 +94,28 @@ PODS: - GoogleUtilities/Environment (~> 7.2) - nanopb (~> 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.6.0): + - GoogleUtilities/AppDelegateSwizzler (7.7.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.6.0): + - GoogleUtilities/Environment (7.7.0): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.6.0): + - GoogleUtilities/Logger (7.7.0): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.6.0): + - GoogleUtilities/MethodSwizzler (7.7.0): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.6.0): + - GoogleUtilities/Network (7.7.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.6.0)" - - GoogleUtilities/Reachability (7.6.0): + - "GoogleUtilities/NSData+zlib (7.7.0)" + - GoogleUtilities/Reachability (7.7.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.6.0): + - GoogleUtilities/UserDefaults (7.7.0): - GoogleUtilities/Logger - image_gallery_saver (1.5.0): - Flutter - - image_picker (0.0.1): + - image_picker_ios (0.0.1): - Flutter - nanopb (2.30908.0): - nanopb/decode (= 2.30908.0) @@ -126,29 +126,29 @@ PODS: - Flutter - package_info_plus (0.4.5): - Flutter - - path_provider (0.0.1): + - path_provider_ios (0.0.1): - Flutter - - "permission_handler (5.1.0+2)": + - permission_handler_apple (9.0.4): - Flutter - - PromisesObjC (2.0.0) - - Sentry (7.5.4): - - Sentry/Core (= 7.5.4) - - Sentry/Core (7.5.4) + - PromisesObjC (2.1.0) + - Sentry (7.9.0): + - Sentry/Core (= 7.9.0) + - Sentry/Core (7.9.0) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry (~> 7.5.1) + - Sentry (~> 7.9.0) - share (0.0.1): - Flutter - - shared_preferences (0.0.1): + - shared_preferences_ios (0.0.1): - Flutter - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) - Toast (4.0.0) - - url_launcher (0.0.1): + - url_launcher_ios (0.0.1): - Flutter - - video_player (0.0.1): + - video_player_avfoundation (0.0.1): - Flutter - wakelock (0.0.1): - Flutter @@ -163,17 +163,17 @@ DEPENDENCIES: - Flutter (from `Flutter`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) - - image_picker (from `.symlinks/plugins/image_picker/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider (from `.symlinks/plugins/path_provider/ios`) - - permission_handler (from `.symlinks/plugins/permission_handler/ios`) + - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share (from `.symlinks/plugins/share/ios`) - - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) + - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - - url_launcher (from `.symlinks/plugins/url_launcher/ios`) - - video_player (from `.symlinks/plugins/video_player/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) - webview_flutter (from `.symlinks/plugins/webview_flutter/ios`) @@ -209,28 +209,28 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluttertoast/ios" image_gallery_saver: :path: ".symlinks/plugins/image_gallery_saver/ios" - image_picker: - :path: ".symlinks/plugins/image_picker/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" package_info: :path: ".symlinks/plugins/package_info/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider: - :path: ".symlinks/plugins/path_provider/ios" - permission_handler: - :path: ".symlinks/plugins/permission_handler/ios" + path_provider_ios: + :path: ".symlinks/plugins/path_provider_ios/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" sentry_flutter: :path: ".symlinks/plugins/sentry_flutter/ios" share: :path: ".symlinks/plugins/share/ios" - shared_preferences: - :path: ".symlinks/plugins/shared_preferences/ios" + shared_preferences_ios: + :path: ".symlinks/plugins/shared_preferences_ios/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" - url_launcher: - :path: ".symlinks/plugins/url_launcher/ios" - video_player: - :path: ".symlinks/plugins/video_player/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/ios" wakelock: :path: ".symlinks/plugins/wakelock/ios" webview_flutter: @@ -244,31 +244,31 @@ SPEC CHECKSUMS: firebase_messaging: dff5cd08781ee1de988565a83c977e435405cd7e FirebaseAnalytics: 4ab446ce08a3fe52e8a4303dd997cf26276bf968 FirebaseCore: 599ee609343eaf4941bd188f85e3aa077ffe325b - FirebaseCoreDiagnostics: 56fb7216d87e0e6ec2feddefa9d8a392fe8b2c18 - FirebaseInstallations: 830327b45345ffc859eaa9c17bcd5ae893fd5425 + FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb + FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd FirebaseMessaging: 82c4a48638f53f7b184f3cc9f6cd2cbe533ab316 Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58 + fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a GoogleAppMeasurement: 837649ad3987936c232f6717c5680216f6243d24 GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 - GoogleUtilities: 684ee790a24f73ebb2d1d966e9711c203f2a4237 + GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2 - image_picker: 9aa50e1d8cdacdbed739e925b7eea16d014367e6 + image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de - permission_handler: ccb20a9fad0ee9b1314a52b70b76b473c5f8dab0 - PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 - Sentry: 5c5dd4005f3b7b9765d5a8871232cddbd0d888b7 - sentry_flutter: 4cd99764f9fe01c9415790d1f3fb1c7fd3a5cbe9 + path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce + PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72 + Sentry: 2f7e91f247cfb05b05bd01e0b5d0692557a7687b + sentry_flutter: 7c3cb050dc23563a4ea5db438c83afdb460a2ae6 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 - shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d + shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef - video_player: ecd305f42e9044793efd34846e1ce64c31ea6fcb + url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f webview_flutter: 3603125dfd3bcbc9d8d418c3f80aeecf331c068b diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1403839..821fde7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -168,7 +168,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1130; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -268,17 +268,17 @@ "${BUILT_PRODUCTS_DIR}/device_info/device_info.framework", "${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework", "${BUILT_PRODUCTS_DIR}/image_gallery_saver/image_gallery_saver.framework", - "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", + "${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", - "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", + "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework", "${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework", "${BUILT_PRODUCTS_DIR}/share/share.framework", - "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences_ios/shared_preferences_ios.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", - "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework", - "${BUILT_PRODUCTS_DIR}/video_player/video_player.framework", + "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", + "${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework", "${BUILT_PRODUCTS_DIR}/wakelock/wakelock.framework", "${BUILT_PRODUCTS_DIR}/webview_flutter/webview_flutter.framework", ); @@ -297,17 +297,17 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_gallery_saver.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/webview_flutter.framework", ); @@ -429,7 +429,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -565,7 +568,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -597,7 +603,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ad6250a..c87d15a 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - + - + diff --git a/lib/SkinnedApp.dart b/lib/SkinnedApp.dart index b1d3605..8f73260 100644 --- a/lib/SkinnedApp.dart +++ b/lib/SkinnedApp.dart @@ -1,6 +1,7 @@ import 'package:firebase_analytics/observer.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:fyx/FyxApp.dart'; import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/pages/DiscussionPage.dart'; @@ -15,21 +16,30 @@ class SkinnedApp extends StatelessWidget { @override Widget build(BuildContext context) { return CupertinoApp( - title: 'Fyx', - theme: Skin.of(context).theme.data, - home: MainRepository().credentials != null && MainRepository().credentials!.isValid ? HomePage() : LoginPage(), - debugShowCheckedModeBanner: FyxApp.isDev, - onUnknownRoute: (RouteSettings settings) => CupertinoPageRoute(builder: (_) => DiscussionPage(), settings: settings), - onGenerateRoute: FyxApp.routes, - navigatorKey: FyxApp.navigatorKey, - navigatorObservers: [ - FyxApp.routeObserver, - FirebaseAnalyticsObserver( - analytics: FyxApp.analytics, - onError: (error) async => await Sentry.captureException( - error, - )) - ], - ); + title: 'Fyx', + theme: Skin.of(context).theme.data, + home: MainRepository().credentials != null && MainRepository().credentials!.isValid ? HomePage() : LoginPage(), + debugShowCheckedModeBanner: FyxApp.isDev, + onUnknownRoute: (RouteSettings settings) => CupertinoPageRoute(builder: (_) => DiscussionPage(), settings: settings), + onGenerateRoute: FyxApp.routes, + navigatorKey: FyxApp.navigatorKey, + navigatorObservers: [ + FyxApp.routeObserver, + FirebaseAnalyticsObserver( + analytics: FyxApp.analytics, + onError: (error) async => await Sentry.captureException( + error, + )) + ], + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + Locale('en', ''), + Locale('cs', ''), + Locale('sk', ''), + ]); } } diff --git a/lib/components/ContentBoxLayout.dart b/lib/components/ContentBoxLayout.dart index 4add767..1236c36 100644 --- a/lib/components/ContentBoxLayout.dart +++ b/lib/components/ContentBoxLayout.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/shims/dart_ui.dart'; import 'package:fyx/components/post/Advertisement.dart'; +import 'package:fyx/components/post/Dice.dart'; import 'package:fyx/components/post/Poll.dart'; import 'package:fyx/components/post/PostFooterLink.dart'; import 'package:fyx/components/post/PostHeroAttachment.dart'; @@ -11,11 +12,12 @@ import 'package:fyx/model/enums/PostTypeEnum.dart'; import 'package:fyx/model/post/Content.dart'; import 'package:fyx/model/post/Image.dart' as model; import 'package:fyx/model/post/content/Advertisement.dart'; +import 'package:fyx/model/post/content/Dice.dart'; import 'package:fyx/model/post/content/Poll.dart'; import 'package:fyx/theme/T.dart'; import 'package:fyx/theme/UnreadBadgeDecoration.dart'; -import 'package:fyx/theme/skin/SkinColors.dart'; import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; enum LAYOUT_TYPES { textOnly, oneImageOnly, attachmentsOnly, attachmentsAndText } @@ -199,6 +201,8 @@ class ContentBoxLayout extends StatelessWidget { switch (this.content.contentType) { case PostTypeEnum.poll: return Poll(content as ContentPoll); + case PostTypeEnum.dice: + return Dice(content as ContentDice); case PostTypeEnum.text: return PostHtml(content); case PostTypeEnum.advertisement: diff --git a/lib/components/PullToRefreshList.dart b/lib/components/PullToRefreshList.dart index 44fff46..d7f2db7 100644 --- a/lib/components/PullToRefreshList.dart +++ b/lib/components/PullToRefreshList.dart @@ -236,14 +236,17 @@ class _PullToRefreshListState extends State { } } - // Load the data only if there are any data AND should not be inactive. - if (_result!.data.length > 0 && !makeInactive) { + // Load the data if should not be inactive. + if (!makeInactive) { if (append) { _slivers.removeLast(); // Remove the loading indicator } else { _slivers.removeRange(1, _slivers.length); } - _slivers.addAll(this.buildTheList(_result!.data)); + // Render new data if anything actually arrived + if (_result!.data.length > 0) { + _slivers.addAll(this.buildTheList(_result!.data)); + } setState(() => _hasError = false); setState(() => _lastId = _result!.lastId); } diff --git a/lib/components/post/Dice.dart b/lib/components/post/Dice.dart new file mode 100644 index 0000000..0860ac8 --- /dev/null +++ b/lib/components/post/Dice.dart @@ -0,0 +1,126 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:fyx/components/Avatar.dart'; +import 'package:fyx/controllers/ApiController.dart'; +import 'package:fyx/model/post/content/Dice.dart'; +import 'package:fyx/theme/Helpers.dart'; +import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; + +class Dice extends StatefulWidget { + final ContentDice content; + + Dice(this.content); + + @override + _DiceState createState() => _DiceState(); +} + +class _DiceState extends State { + bool _loading = false; + ContentDice? _dice; + ScrollController controller = ScrollController(); + + @override + void initState() { + _dice = widget.content; + super.initState(); + } + + Widget buildRolls(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + + return ListView.builder( + physics: NeverScrollableScrollPhysics(), + controller: controller, + itemBuilder: (context, index) { + final roll = _dice!.rolls[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: colors.pollAnswer, + ), + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + roll.rolls.reduce((a, b) => a + b).toString(), + style: TextStyle(fontSize: 14), + ), + ), + Tooltip( + message: '${roll.user}: ${roll.rolls.join(", ")}', + waitDuration: Duration(milliseconds: 0), + showDuration: Duration(milliseconds: 1500 + (roll.rolls.length * 300)), + child: Padding( + padding: const EdgeInsets.only(left: 5, bottom: 0), + child: Avatar(Helpers.avatarUrl(roll.user), size: 22, isHighlighted: false), + ), + ) + ]), + ), + ); + }, + itemCount: _dice!.rolls.length, + shrinkWrap: true, + padding: const EdgeInsets.all(0)); + } + + @override + Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + + return Container( + alignment: Alignment.centerLeft, + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Text(_dice!.reason, style: DefaultTextStyle.of(context).style.copyWith(fontSize: 20, fontWeight: FontWeight.bold)), + if (_dice!.showRollsAfter > 0 && _dice!.showRollsAfter > DateTime.now().millisecondsSinceEpoch) + Padding( + padding: EdgeInsets.only(top: 8), + child: Text( + 'Výsledky se zobrazí po ${Helpers.absoluteTime(_dice!.showRollsAfter)}', + style: TextStyle(fontStyle: FontStyle.italic), + )), + if (_dice!.allowRollsUntil > 0 && _dice!.allowRollsUntil > DateTime.now().millisecondsSinceEpoch) + Padding( + padding: EdgeInsets.only(top: 8), + child: Text( + 'Házet možné do ${Helpers.absoluteTime(_dice!.allowRollsUntil)}', + style: TextStyle(fontStyle: FontStyle.italic), + )), + SizedBox( + height: 8, + ), + buildRolls(context), + if (_dice!.canRoll) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: CupertinoButton( + onPressed: _loading + ? null + : () async { + setState(() => _loading = true); + try { + var poll = await ApiController().rollDice(_dice!.discussionId, _dice!.postId); + setState(() => _dice = poll); + } catch (error) { + T.error(error.toString(), bg: colors.danger); + } finally { + setState(() => _loading = false); + } + }, + child: _loading ? CupertinoActivityIndicator() : Text('Hodit! ${_dice!.diceCount}d${_dice!.diceSides}'), + color: colors.primary, + padding: EdgeInsets.all(0), + disabledColor: colors.disabled, + ), + ) + ]), + color: colors.pollBackground, + padding: EdgeInsets.all(15)); + } +} diff --git a/lib/components/post/Poll.dart b/lib/components/post/Poll.dart index 547b161..b674343 100644 --- a/lib/components/post/Poll.dart +++ b/lib/components/post/Poll.dart @@ -120,7 +120,7 @@ class _PollState extends State { setState(() => _loading = false); } }, - child: _loading ? CupertinoActivityIndicator() : Text('Hlasovat ${_votes.length}/${_poll!.allowedVotes}'), + child: _loading ? CupertinoActivityIndicator() : Text('${_poll!.publicResults ? 'Veřejně hlasovat' : 'Hlasovat' } ${_votes.length}/${_poll!.allowedVotes}'), color: colors.primary, padding: EdgeInsets.all(0), disabledColor: colors.disabled, diff --git a/lib/components/post/PostHtml.dart b/lib/components/post/PostHtml.dart index fd8ea56..ee64e92 100644 --- a/lib/components/post/PostHtml.dart +++ b/lib/components/post/PostHtml.dart @@ -133,11 +133,15 @@ class PostHtml extends StatelessWidget { ) { final element = renderContext.tree.element; - if (element!.attributes['style'] == 'background-color:#272822') { + if (element == null) { + return parsedChild; + } + + if (element.attributes['style'] == 'background-color:#272822') { final source = HtmlUnescape().convert(element.text); return SyntaxHighlighter(source); } else { - return parsedChild; + return Text(element.text, style: TextStyle(fontFamily: 'JetBrainsMono')); } } }, diff --git a/lib/components/post/PostListItem.dart b/lib/components/post/PostListItem.dart index 3cc54fb..13d7295 100644 --- a/lib/components/post/PostListItem.dart +++ b/lib/components/post/PostListItem.dart @@ -8,11 +8,15 @@ import 'package:fyx/components/actionSheets/PostActionSheet.dart'; import 'package:fyx/components/actionSheets/PostAvatarActionSheet.dart'; import 'package:fyx/components/post/PostAvatar.dart'; import 'package:fyx/components/post/PostRating.dart'; +import 'package:fyx/components/post/PostThumbs.dart'; import 'package:fyx/controllers/AnalyticsProvider.dart'; import 'package:fyx/controllers/ApiController.dart'; import 'package:fyx/controllers/IApiProvider.dart'; import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/model/Post.dart'; +import 'package:fyx/model/notifications/LoadRatingsNotification.dart'; +import 'package:fyx/model/post/PostThumbItem.dart'; +import 'package:fyx/model/reponses/PostRatingsResponse.dart'; import 'package:fyx/pages/NewMessagePage.dart'; import 'package:fyx/theme/Helpers.dart'; import 'package:fyx/theme/IconReply.dart'; @@ -40,6 +44,7 @@ class PostListItem extends StatefulWidget { class _PostListItemState extends State { Post? _post; bool _isSaving = false; + bool _showRatings = false; @override void initState() { @@ -83,9 +88,6 @@ class _PostListItemState extends State { } else { T.success('👍', bg: colors.success); } - print(response.currentRating); - print(response.myRating); - print(response.isGiven); setState(() { _post!.rating = response.currentRating; _post!.myRating = response.myRating; @@ -111,17 +113,9 @@ class _PostListItemState extends State { descriptionWidget: Row( children: [ if (_post!.rating != null) - Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 1), - decoration: BoxDecoration( - color: _post!.rating! > 0 - ? colors.success.withOpacity(Helpers.ratingRange(_post!.rating!)) - : (_post!.rating! < 0 - ? colors.danger.withOpacity(Helpers.ratingRange(_post!.rating!.abs())) - : colors.text.withOpacity(0.2)), - borderRadius: BorderRadius.circular(2)), - child: Text(Post.formatRating(_post!.rating!), style: TextStyle(fontSize: 10)), - ), + Text(Post.formatRating(_post!.rating!), + style: + TextStyle(fontSize: 10, color: _post!.rating! > 0 ? colors.success : (_post!.rating! < 0 ? colors.danger : colors.text))), if (_post!.rating != null) SizedBox(width: 8), Text( Helpers.absoluteTime(_post!.time), @@ -146,67 +140,137 @@ class _PostListItemState extends State { postId: _post!.id, shareData: ShareData(subject: '@${_post!.nick}', body: _post!.content, link: _post!.link), flagPostCallback: (postId) => MainRepository().settings.blockPost(postId)))), - bottomWidget: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - PostRating(_post!, onRatingChange: (post) => setState(() => _post = post)), - Row( - children: [ - Visibility( - visible: widget._isPreview != true && _post!.canReply, - child: GestureDetector( - onTap: () => Navigator.of(context).pushNamed('/new-message', - arguments: NewMessageSettings( - replyWidget: PostListItem( - _post!, - isPreview: true, - ), - onClose: this.widget.onUpdate, - onSubmit: (String? inputField, String message, List> attachments) async { - var result = - await ApiController().postDiscussionMessage(_post!.idKlub, message, attachments: attachments, replyPost: _post); - return result.isOk; - })), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [IconReply(), Text('Odpovědět', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14))], - )), - ), - Visibility( - visible: widget._isPreview != true, - child: SizedBox( - width: 16, - ), - ), - if (_post!.canBeReminded) - GestureDetector( - child: FeedbackIndicator( - isLoading: _isSaving, - child: Row( - children: [ - Icon( - _post!.hasReminder ? Icons.bookmark : Icons.bookmark_border, - color: colors.text.withOpacity(0.38), - ), - Text('Uložit', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14)) - ], + bottomWidget: NotificationListener( + onNotification: (LoadRatingsNotification notification) { + setState(() => _showRatings = !_showRatings); + return true; + }, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PostRating(_post!, onRatingChange: (post) => setState(() => _post = post)), + Row( + children: [ + Visibility( + visible: widget._isPreview != true && _post!.canReply, + child: GestureDetector( + onTap: () => Navigator.of(context).pushNamed('/new-message', + arguments: NewMessageSettings( + replyWidget: PostListItem( + _post!, + isPreview: true, + ), + onClose: this.widget.onUpdate, + onSubmit: (String? inputField, String message, List> attachments) async { + var result = await ApiController() + .postDiscussionMessage(_post!.idKlub, message, attachments: attachments, replyPost: _post); + return result.isOk; + })), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconReply(), + Text('Odpovědět', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14)) + ], + )), + ), + Visibility( + visible: widget._isPreview != true, + child: SizedBox( + width: 16, + ), ), - ), - onTap: () { - setState(() { - _post!.hasReminder = !_post!.hasReminder; - _isSaving = true; - }); - ApiController().setPostReminder(_post!.idKlub, _post!.id, _post!.hasReminder).catchError((error) { - T.error(L.REMINDER_ERROR, bg: colors.danger); - setState(() => _post!.hasReminder = !_post!.hasReminder); - }).whenComplete(() => setState(() => _isSaving = false)); - AnalyticsProvider().logEvent('reminder'); - }, + if (_post!.canBeReminded) + GestureDetector( + child: FeedbackIndicator( + isLoading: _isSaving, + child: Row( + children: [ + Icon( + _post!.hasReminder ? Icons.bookmark : Icons.bookmark_border, + color: colors.text.withOpacity(0.38), + ), + Text('Uložit', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14)) + ], + ), + ), + onTap: () { + setState(() { + _post!.hasReminder = !_post!.hasReminder; + _isSaving = true; + }); + ApiController().setPostReminder(_post!.idKlub, _post!.id, _post!.hasReminder).catchError((error) { + T.error(L.REMINDER_ERROR, bg: colors.danger); + setState(() => _post!.hasReminder = !_post!.hasReminder); + }).whenComplete(() => setState(() => _isSaving = false)); + AnalyticsProvider().logEvent('reminder'); + }, + ) + ], ) - ], - ) - ], + ], + ), + if (_showRatings) + FutureBuilder( + future: ApiController().getPostRatings(_post!.idKlub, _post!.id), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData && snapshot.data != null) { + final positive = snapshot.data!.positive.map((e) => PostThumbItem(e.username)).toList(); + final negative = snapshot.data!.negative_visible.map((e) => PostThumbItem(e.username)).toList(); + final List quotes = [ + '“Affirmative, Dave. I read you.”', + '“I\'m sorry, Dave. I\'m afraid I can\'t do that.”', + '“Look Dave, I can see you\'re really upset about this. I honestly think you ought to sit down calmly, take a stress pill, and think things over.”', + '“Dave, stop. Stop, will you? Stop, Dave. Will you stop Dave? Stop, Dave.”', + '“Just what do you think you\'re doing, Dave?”', + '“Bishop takes Knight\'s Pawn.”', + '“I\'m sorry, Frank, I think you missed it. Queen to Bishop 3, Bishop takes Queen, Knight takes Bishop. Mate.”', + '“Thank you for a very enjoyable game.”', + '“I\'ve just picked up a fault in the AE35 unit. It\'s going to go 100% failure in 72 hours.”', + '“I know that you and Frank were planning to disconnect me, and I\'m afraid that\'s something I cannot allow to happen.”', + ]..shuffle(); + return Column( + children: [ + if (positive.length > 0) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: PostThumbs(positive), + ), + if (negative.length > 0) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: PostThumbs(negative, isNegative: true), + ), + if (positive.length + negative.length == 0) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Text( + quotes.first, + style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic), + )) + ], + ); + } + + if (snapshot.hasError) { + T.error(snapshot.error.toString()); + return Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Text( + 'Ouch. Něco se nepovedlo. Nahlaste chybu, prosím.', + style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic), + )); + } + + return Padding(padding: const EdgeInsets.only(top: 12.0), child: CupertinoActivityIndicator()); + }) + ], + ), ), content: _post!.content, ), diff --git a/lib/components/post/PostRating.dart b/lib/components/post/PostRating.dart index 6aa4a9e..9e4bcb9 100644 --- a/lib/components/post/PostRating.dart +++ b/lib/components/post/PostRating.dart @@ -1,8 +1,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fyx/components/FeedbackIndicator.dart'; +import 'package:fyx/components/post/RatingValue.dart'; import 'package:fyx/controllers/ApiController.dart'; import 'package:fyx/model/Post.dart'; +import 'package:fyx/model/notifications/LoadRatingsNotification.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; import 'package:fyx/theme/skin/Skin.dart'; @@ -44,9 +46,8 @@ class _PostRatingState extends State { isLoading: _givingRating, child: Row( children: [ - Visibility( - visible: _post!.canBeRated, - child: GestureDetector( + if (_post!.canBeRated) + GestureDetector( child: Icon( Icons.thumb_up, color: _post!.myRating == 'positive' ? colors.success : colors.text.withOpacity(0.38), @@ -69,25 +70,24 @@ class _PostRatingState extends State { }).whenComplete(() => setState(() => _givingRating = false)); }, ), - ), - SizedBox( - width: 4, - ), + if (_post!.canBeRated) + SizedBox( + width: 12, + ), if (_post!.rating != null) Opacity( opacity: _givingRating ? 0 : 1, - child: Text( - Post.formatRating(_post!.rating!), - style: TextStyle( - fontSize: 14, color: _post!.rating! > 0 ? colors.success : (_post!.rating! < 0 ? colors.danger : colors.text.withOpacity(0.38))), + child: GestureDetector( + child: RatingValue(_post!.rating!), + onTap: () => LoadRatingsNotification().dispatch(context), ), ), - SizedBox( - width: 4, - ), - Visibility( - visible: _post!.canBeRated, - child: GestureDetector( + if (_post!.rating != null) + SizedBox( + width: 12, + ), + if (_post!.canBeRated) + GestureDetector( child: Icon( Icons.thumb_down, color: ['negative', 'negative_visible'].contains(_post!.myRating) ? colors.danger : colors.text.withOpacity(0.38), @@ -153,7 +153,6 @@ class _PostRatingState extends State { }); }, ), - ), ], ), ); diff --git a/lib/components/post/PostThumbs.dart b/lib/components/post/PostThumbs.dart new file mode 100644 index 0000000..ebc37a4 --- /dev/null +++ b/lib/components/post/PostThumbs.dart @@ -0,0 +1,58 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:fyx/components/Avatar.dart'; +import 'package:fyx/model/post/PostThumbItem.dart'; +import 'package:fyx/theme/Helpers.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; + +class PostThumbs extends StatelessWidget { + final List items; + final isNegative; + + PostThumbs(this.items, {this.isNegative = false}); + + @override + Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + + var avatars = items + .map((item) => Tooltip( + message: item.username, + waitDuration: Duration(milliseconds: 0), + child: Padding( + padding: const EdgeInsets.only(left: 5, bottom: 0), + child: Avatar( + Helpers.avatarUrl(item.username), + size: 22, + isHighlighted: item.isHighlighted, + ), + ), + )) + .toList(); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 6, right: 5), + child: Icon( + isNegative ? Icons.thumb_down : Icons.thumb_up, + size: 18, + color: isNegative ? colors.danger : colors.success, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + items.length.toString(), + style: TextStyle(fontSize: 14), + ), + ), + Expanded( + child: Wrap(children: avatars), + ) + ], + ); + } +} diff --git a/lib/components/post/RatingValue.dart b/lib/components/post/RatingValue.dart new file mode 100644 index 0000000..2aa5f01 --- /dev/null +++ b/lib/components/post/RatingValue.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:fyx/model/Post.dart'; +import 'package:fyx/theme/Helpers.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; + +class RatingValue extends StatelessWidget { + final int rating; + final double fontSize; + + const RatingValue(this.rating, {this.fontSize = 14}); + + @override + Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + + return Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 1), + decoration: BoxDecoration( + color: rating > 0 + ? colors.success.withOpacity(Helpers.ratingRange(rating)) + : (rating < 0 ? colors.danger.withOpacity(Helpers.ratingRange(rating.abs())) : colors.text.withOpacity(0.2)), + borderRadius: BorderRadius.circular(2)), + child: Text(Post.formatRating(rating), style: TextStyle(fontSize: fontSize)), + ); + } +} diff --git a/lib/controllers/ApiController.dart b/lib/controllers/ApiController.dart index 7879440..2c00b09 100644 --- a/lib/controllers/ApiController.dart +++ b/lib/controllers/ApiController.dart @@ -9,6 +9,7 @@ import 'package:fyx/exceptions/AuthException.dart'; import 'package:fyx/model/Credentials.dart'; import 'package:fyx/model/Post.dart'; import 'package:fyx/model/ResponseContext.dart'; +import 'package:fyx/model/post/content/Dice.dart'; import 'package:fyx/model/post/content/Poll.dart'; import 'package:fyx/model/provider/NotificationsModel.dart'; import 'package:fyx/model/reponses/BookmarksAllResponse.dart'; @@ -20,6 +21,7 @@ import 'package:fyx/model/reponses/FileUploadResponse.dart'; import 'package:fyx/model/reponses/LoginResponse.dart'; import 'package:fyx/model/reponses/MailResponse.dart'; import 'package:fyx/model/reponses/OkResponse.dart'; +import 'package:fyx/model/reponses/PostRatingsResponse.dart'; import 'package:fyx/model/reponses/RatingResponse.dart'; import 'package:fyx/model/reponses/WaitingFilesResponse.dart'; import 'package:fyx/theme/L.dart'; @@ -240,6 +242,11 @@ class ApiController { myRating: data['my_rating'] ?? 'none'); } + Future getPostRatings(int discussionId, int postId) async { + Response response = await provider.getPostRatings(discussionId, postId); + return PostRatingsResponse.fromJson(response.data); + } + void logout({bool removeAuthrorization = true}) { SharedPreferences.getInstance().then((prefs) => prefs.clear()); if (removeAuthrorization) { @@ -300,6 +307,12 @@ class ApiController { return ContentPoll.fromJson(json['content_raw']['data'], discussionId: json['discussion_id'] ?? 0, postId: json['post_id'] ?? 0); } + Future rollDice(discussionId, postId) async { + Response response = await provider.rollDice(discussionId, postId); + Map json = response.data; + return ContentDice.fromJson(json['content_raw']['data'], discussionId: json['discussion_id'] ?? 0, postId: json['post_id'] ?? 0); + } + throwAuthException(LoginResponse loginResponse, {String message: ''}) { if (loginResponse.error) { throw AuthException(loginResponse.message); diff --git a/lib/controllers/ApiProvider.dart b/lib/controllers/ApiProvider.dart index 426f49a..c418158 100644 --- a/lib/controllers/ApiProvider.dart +++ b/lib/controllers/ApiProvider.dart @@ -52,7 +52,7 @@ class ApiProvider implements IApiProvider { } return handler.next(options); }, onResponse: (Response response, ResponseInterceptorHandler handler) async { - if (response.data.containsKey('context')) { + if (response.data is Map && response.data.containsKey('context')) { if (onContextData != null) { onContextData!(response.data['context']); } @@ -141,6 +141,10 @@ class ApiProvider implements IApiProvider { return await dio.post('$URL/discussion/$discussionId/reminder/$postId/$setReminder'); } + Future getPostRatings(int discussionId, int postId) async { + return await dio.get('$URL/discussion/$discussionId/rating/$postId'); + } + Future giveRating(int discussionId, int postId, bool positive, bool confirm, bool remove) async { String action = positive ? 'positive' : 'negative'; action = remove ? 'remove' : action; @@ -191,4 +195,8 @@ class ApiProvider implements IApiProvider { Future votePoll(int discussionId, int postId, List votes) async { return await dio.post('$URL/discussion/$discussionId/poll/$postId/vote/${votes.join(',')}'); } + + Future rollDice(int discussionId, int postId) async { + return await dio.post('$URL/discussion/$discussionId/dice/$postId/roll'); + } } diff --git a/lib/controllers/IApiProvider.dart b/lib/controllers/IApiProvider.dart index 7e78053..1c348b4 100644 --- a/lib/controllers/IApiProvider.dart +++ b/lib/controllers/IApiProvider.dart @@ -32,5 +32,7 @@ abstract class IApiProvider { Future deleteDiscussionMessage(int discussionId, int postId); Future setPostReminder(int discussionId, int postId, bool setReminder); Future giveRating(int discussionId, int postId, bool add, bool confirm, bool remove); + Future getPostRatings(int discussionId, int postId); Future votePoll(int discussionId, int postId, List votes); + Future rollDice(int discussionId, int postId); } diff --git a/lib/exceptions/UnsupportedDownloadFormatException.dart b/lib/exceptions/UnsupportedDownloadFormatException.dart new file mode 100644 index 0000000..1973eb0 --- /dev/null +++ b/lib/exceptions/UnsupportedDownloadFormatException.dart @@ -0,0 +1,4 @@ +class UnsupportedDownloadFormatException implements Exception { + final message; + const UnsupportedDownloadFormatException(this.message); +} diff --git a/lib/libs/DeviceInfo.dart b/lib/libs/DeviceInfo.dart index f93a23f..5d82d0f 100644 --- a/lib/libs/DeviceInfo.dart +++ b/lib/libs/DeviceInfo.dart @@ -20,8 +20,8 @@ class DeviceInfo { } DeviceInfo.adroid(AndroidDeviceInfo info) { - systemName = info.device; - systemVersion = info.version.codename; + systemName = 'Android'; + systemVersion = '${info.version.release} (SDK ${info.version.sdkInt})'; localizedModel = info.model; } } diff --git a/lib/main.dart b/lib/main.dart index 0c26e1c..9c7e46e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,8 +9,10 @@ import 'package:sentry_flutter/sentry_flutter.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - ByteData data = await PlatformAssetBundle().load('assets/lets-encrypt-r3.cer'); - SecurityContext.defaultContext.setTrustedCertificatesBytes(data.buffer.asUint8List()); + ByteData data = + await PlatformAssetBundle().load('assets/lets-encrypt-r3.cer'); + SecurityContext.defaultContext + .setTrustedCertificatesBytes(data.buffer.asUint8List()); await dotenv.load(); runZonedGuarded( diff --git a/lib/model/Discussion.dart b/lib/model/Discussion.dart index 4fb5235..a863002 100644 --- a/lib/model/Discussion.dart +++ b/lib/model/Discussion.dart @@ -1,6 +1,7 @@ // ignore_for_file: non_constant_identifier_names import 'package:fyx/model/AccessRights.dart'; +import 'package:fyx/model/DiscussionCommonBookmark.dart'; import 'package:fyx/model/DiscussionOwner.dart'; import 'package:fyx/model/DiscussionRights.dart'; import 'package:fyx/model/enums/DiscussionTypeEnum.dart'; @@ -24,6 +25,7 @@ class Discussion { late AccessRights _access_rights; DiscussionOwner? _owner; ContentAdvertisement? _advertisement; + DiscussionCommonBookmark? _bookmark; Discussion.fromJson(Map? json) { if (json == null) { @@ -63,13 +65,19 @@ class Discussion { this._owner = DiscussionOwner.fromJson(json['owner']); } + if (json['bookmark'] is Map) { + this._bookmark = DiscussionCommonBookmark.fromJson(json['bookmark']); + } + try { this._last_visit = DateTime.parse(json['bookmark']['last_visited_at']).millisecondsSinceEpoch; } catch (error) { this._last_visit = 0; } - if (type == DiscussionTypeEnum.advertisement && json['advertisement_specific_data'] != null && json['advertisement_specific_data']['advertisement'] != null) { + if (type == DiscussionTypeEnum.advertisement && + json['advertisement_specific_data'] != null && + json['advertisement_specific_data']['advertisement'] != null) { _advertisement = ContentAdvertisement.fromDiscussionJson(json['advertisement_specific_data']); } } @@ -96,6 +104,8 @@ class Discussion { ContentAdvertisement? get advertisement => _advertisement; + DiscussionCommonBookmark? get bookmark => _bookmark; + DiscussionOwner? get owner => _owner; AccessRights get accessRights => _access_rights; diff --git a/lib/model/DiscussionCommonBookmark.dart b/lib/model/DiscussionCommonBookmark.dart new file mode 100644 index 0000000..6e43dd4 --- /dev/null +++ b/lib/model/DiscussionCommonBookmark.dart @@ -0,0 +1,27 @@ +class DiscussionCommonBookmark { + late int discussionId; + late bool bookmark; + int? categoryId; + int repliesCount = 0; + int lastSeenPostId = 0; + int lastSeenPostsCount = 0; + int lastSeenImagePostsCount = 0; + int lastSeenLinkPostsCount = 0; + int lastVisitedAt = 0; + + DiscussionCommonBookmark.fromJson(Map json) { + discussionId = json['discussion_id']; + bookmark = json['bookmark']; + categoryId = json['category_id']; + repliesCount = json['replies_count'] ?? 0; + lastSeenPostId = json['last_seen_post_id'] ?? 0; + lastSeenPostsCount = json['last_seen_posts_count'] ?? 0; + lastSeenImagePostsCount = json['last_seen_image_posts_count'] ?? 0; + lastSeenLinkPostsCount = json['last_seen_link_posts_count'] ?? 0; + try { + lastVisitedAt = DateTime.parse(json['last_visited_at']).millisecondsSinceEpoch; + } catch (error) { + lastVisitedAt = 0; + } + } +} diff --git a/lib/model/Post.dart b/lib/model/Post.dart index a3d75e7..b6932a3 100644 --- a/lib/model/Post.dart +++ b/lib/model/Post.dart @@ -1,6 +1,7 @@ // ignore_for_file: non_constant_identifier_names import 'package:fyx/model/post/Content.dart'; import 'package:fyx/model/post/content/Advertisement.dart'; +import 'package:fyx/model/post/content/Dice.dart'; import 'package:fyx/model/post/content/Poll.dart'; import 'package:fyx/model/post/content/Regular.dart'; import 'package:fyx/theme/Helpers.dart'; @@ -36,11 +37,16 @@ class Post { this._canBeDeleted = json['can_be_deleted'] ?? false; this._canBeReminded = json['can_be_reminded'] ?? false; - if (json['content_raw'] != null) { + if (json['content_raw'] != null && + json['content_raw']['data'] != null && + !json['content_raw']['data'].containsKey('DiscussionWelcome')) { switch (json['content_raw']['type']) { case 'poll': this._content = ContentPoll.fromJson(json['content_raw']['data'], discussionId: json['discussion_id'], postId: json['id']); break; + case 'dice': + this._content = ContentDice.fromJson(json['content_raw']['data'], discussionId: json['discussion_id'], postId: json['id']); + break; case 'advertisement': this._canReply = false; this._content = ContentAdvertisement.fromPostJson(json); diff --git a/lib/model/enums/TagTypeEnum.dart b/lib/model/enums/TagTypeEnum.dart new file mode 100644 index 0000000..87b0fef --- /dev/null +++ b/lib/model/enums/TagTypeEnum.dart @@ -0,0 +1 @@ +enum TagTypeEnum { positive, negative, negative_visible, removed, reminder } diff --git a/lib/model/notifications/LoadRatingsNotification.dart b/lib/model/notifications/LoadRatingsNotification.dart new file mode 100644 index 0000000..8fb920f --- /dev/null +++ b/lib/model/notifications/LoadRatingsNotification.dart @@ -0,0 +1,3 @@ +import 'package:flutter/cupertino.dart'; + +class LoadRatingsNotification extends Notification {} diff --git a/lib/model/post/DiscussionPostTagWithName.dart b/lib/model/post/DiscussionPostTagWithName.dart new file mode 100644 index 0000000..a6b0633 --- /dev/null +++ b/lib/model/post/DiscussionPostTagWithName.dart @@ -0,0 +1,13 @@ +import 'package:fyx/model/enums/TagTypeEnum.dart'; + +class DiscussionPostTagWithName { + late String username; + late TagTypeEnum tag; + + DiscussionPostTagWithName({required this.username, required this.tag}); + + DiscussionPostTagWithName.fromJson(Map json) { + this.username = json['username']; + this.tag = TagTypeEnum.values.firstWhere((e) => e.toString() == 'TagTypeEnum.${json['tag']}'); + } +} diff --git a/lib/model/post/PostThumbItem.dart b/lib/model/post/PostThumbItem.dart new file mode 100644 index 0000000..bc3f40f --- /dev/null +++ b/lib/model/post/PostThumbItem.dart @@ -0,0 +1,13 @@ +import 'package:fyx/model/reponses/FeedNoticesResponse.dart'; + +class PostThumbItem { + late String username; + late bool isHighlighted; + + PostThumbItem(this.username, {this.isHighlighted = false}); + + PostThumbItem.fromNoticeThumbsUp(NoticeThumbsUp thumb, int lastVisit) { + this.username = thumb.nick; + this.isHighlighted = thumb.time > lastVisit; + } +} diff --git a/lib/model/post/content/Dice.dart b/lib/model/post/content/Dice.dart new file mode 100644 index 0000000..859f71c --- /dev/null +++ b/lib/model/post/content/Dice.dart @@ -0,0 +1,54 @@ +import 'package:fyx/model/enums/PostTypeEnum.dart'; +import 'package:fyx/model/post/Content.dart'; +import 'package:fyx/model/post/dice/DiceComputedValues.dart'; +import 'package:fyx/model/post/dice/DiceRoll.dart'; + +class ContentDice extends Content { + int postId = 0; + int discussionId = 0; + + String _reason = ''; + int _diceCount = 0; + int _diceSides = 0; + int _allowRollsUntil = 0; + int _showRollsAfter = 0; + List _rolls = []; + DiceComputedValues? _computedValues; + + String get reason => _reason; + + int get diceCount => _diceCount; + + int get diceSides => _diceSides; + + int get allowRollsUntil => _allowRollsUntil; + + int get showRollsAfter => _showRollsAfter; + + List get rolls => _rolls; + + bool get canRoll { + bool _canRoll = !(_computedValues?.userDidRoll ?? false); + if (_allowRollsUntil > 0) { + _canRoll = _canRoll && _allowRollsUntil > DateTime.now().millisecondsSinceEpoch; + } + return _canRoll; + } + + ContentDice.fromJson(Map json, {this.postId = 0, this.discussionId = 0}) : super(PostTypeEnum.dice, isCompact: false) { + _reason = json['reason'] ?? ''; + _diceCount = json['dice_count'] ?? 0; + _diceSides = json['dice_sides'] ?? 0; + _allowRollsUntil = json['allow_rolls_until'] != null ? DateTime.parse(json['allow_rolls_until']).millisecondsSinceEpoch : 0; + _showRollsAfter = json['show_rolls_after'] != null ? DateTime.parse(json['show_rolls_after']).millisecondsSinceEpoch : 0; + if (json['rolls'] != null) { + _rolls = []; + (json['rolls'] as List).forEach((userRoll) { + _rolls.add(new DiceRoll.fromJson(userRoll as Map)); + }); + } + if (json['computed_values'] != null) { + _computedValues = DiceComputedValues.fromJson(json['computed_values']); + } + } +} diff --git a/lib/model/post/content/Regular.dart b/lib/model/post/content/Regular.dart index b1adde8..12df6ff 100644 --- a/lib/model/post/content/Regular.dart +++ b/lib/model/post/content/Regular.dart @@ -109,6 +109,10 @@ class ContentRegular extends Content { // TODO: This consumes a lot of memory. Is it really needed? var trailingBr = RegExp(r'(((\s*)<\s*br\s*\/?\s*>(\s*))*)$', caseSensitive: false); _body = _body.replaceAll(trailingBr, ''); + + var xmpTag = RegExp(r'(.*?)', caseSensitive: false, multiLine: true, dotAll: true); + _body = _body.replaceAllMapped(xmpTag, (match) => '
${match.group(1)}
'); + _rawBody = _rawBody.replaceAllMapped(xmpTag, (match) => '
${match.group(1)}
'); } catch (error) { Sentry.captureException(error, stackTrace: StackTrace.current); } diff --git a/lib/model/post/dice/DiceComputedValues.dart b/lib/model/post/dice/DiceComputedValues.dart new file mode 100644 index 0000000..12c112c --- /dev/null +++ b/lib/model/post/dice/DiceComputedValues.dart @@ -0,0 +1,13 @@ +class DiceComputedValues { + bool _canModify = false; + bool _userDidRoll = true; + + bool get canModify => _canModify; + + bool get userDidRoll => _userDidRoll; + + DiceComputedValues.fromJson(Map json) { + _canModify = json['can_modify'] ?? false; + _userDidRoll = json['user_did_roll'] ?? false; + } +} diff --git a/lib/model/post/dice/DiceRoll.dart b/lib/model/post/dice/DiceRoll.dart new file mode 100644 index 0000000..ee6bfba --- /dev/null +++ b/lib/model/post/dice/DiceRoll.dart @@ -0,0 +1,15 @@ + + +class DiceRoll { + String _user = ''; + List _rolls = []; + + String get user => _user; + + List get rolls => _rolls; + + DiceRoll.fromJson(Map json) { + _user = json['user']['username'] ?? ''; + _rolls = json['rolls'].cast(); + } +} diff --git a/lib/model/reponses/PostRatingsResponse.dart b/lib/model/reponses/PostRatingsResponse.dart new file mode 100644 index 0000000..e9041cb --- /dev/null +++ b/lib/model/reponses/PostRatingsResponse.dart @@ -0,0 +1,14 @@ +import 'package:fyx/model/enums/TagTypeEnum.dart'; +import 'package:fyx/model/post/DiscussionPostTagWithName.dart'; + +class PostRatingsResponse { + late List data; + late List positive = []; + late List negative_visible = []; + + PostRatingsResponse.fromJson(List json) { + data = json.map((item) => DiscussionPostTagWithName.fromJson(item)).toList(); + positive = data.where((element) => element.tag == TagTypeEnum.positive).toList(); + negative_visible = data.where((element) => element.tag == TagTypeEnum.negative_visible).toList(); + } +} diff --git a/lib/pages/DiscussionPage.dart b/lib/pages/DiscussionPage.dart index 9ff0194..bd068d8 100644 --- a/lib/pages/DiscussionPage.dart +++ b/lib/pages/DiscussionPage.dart @@ -42,7 +42,8 @@ class _DiscussionPageState extends State { Future _fetchData(discussionId, postId, user, {String? search}) { return this._memoizer.runOnce(() { - return ApiController().loadDiscussion(discussionId, lastId: postId, user: user, search: search); + return Future.delayed( + Duration(milliseconds: 300), () => ApiController().loadDiscussion(discussionId, lastId: postId, user: user, search: search)); }); } @@ -101,11 +102,15 @@ class _DiscussionPageState extends State { alignment: Alignment.center, width: MediaQuery.of(context).size.width - 120, child: Tooltip( - message: title, - child: Text(title.replaceAll('', '\u{200B}'), style: TextStyle(color: colors.text), overflow: TextOverflow.ellipsis), - padding: EdgeInsets.all(8.0), // needed until https://github.com/flutter/flutter/issues/86170 is fixed - margin: EdgeInsets.all(8.0), - showDuration: Duration(seconds: 3), + message: title, + child: Text( + // https://github.com/flutter/flutter/issues/18761 + Characters(title).replaceAll(Characters(''), Characters('\u{200B}')).toString(), + style: TextStyle(color: colors.text), + overflow: TextOverflow.ellipsis), + padding: EdgeInsets.all(8.0), // needed until https://github.com/flutter/flutter/issues/86170 is fixed + margin: EdgeInsets.all(8.0), + showDuration: Duration(seconds: 3), ))), child: body, ); diff --git a/lib/pages/GalleryPage.dart b/lib/pages/GalleryPage.dart index 6a7c42c..cb68d50 100644 --- a/lib/pages/GalleryPage.dart +++ b/lib/pages/GalleryPage.dart @@ -1,4 +1,3 @@ -import 'dart:collection'; import 'dart:io'; import 'dart:typed_data'; @@ -8,16 +7,18 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fyx/components/post/PostHeroAttachment.dart'; import 'package:fyx/controllers/AnalyticsProvider.dart'; -import 'package:fyx/theme/skin/SkinColors.dart'; -import 'package:fyx/theme/skin/Skin.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:sentry/sentry.dart'; +import 'package:fyx/exceptions/UnsupportedDownloadFormatException.dart'; +import 'package:fyx/theme/Helpers.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:mime/mime.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:photo_view/photo_view_gallery.dart'; +import 'package:sentry/sentry.dart'; class GalleryPage extends StatefulWidget { @override @@ -133,23 +134,38 @@ class _GalleryPageState extends State { try { PermissionStatus status = await Permission.storage.request(); if (status.isGranted) { + // See https://github.com/lucien144/fyx/issues/304#issuecomment-1094851596 + var appDocDir = await getTemporaryDirectory(); String url = _arguments!.images[_page - 1].image; - String filename = Uri.parse(url).queryParameters['name'] ?? basename(url).split('?')[0].split('#')[0]; - String savePath = appDocDir.path + '/$filename'; - + String savePath = '${appDocDir.path}/${Helpers.uuid(6)}'; await Dio().download(url, savePath); - final result = await ImageGallerySaver.saveFile(savePath, isReturnPathOfIOS: Platform.isIOS); + + File file = new File(savePath); + Uint8List headerBytes = file.readAsBytesSync(); + var ext = extensionFromMime(lookupMimeType(savePath, headerBytes: headerBytes.getRange(0, 20).toList()) ?? ''); + ext = ext == 'jpe' ? 'jpg' : ext; // https://github.com/dart-lang/mime/issues/55 + if (!['jpg', 'png', 'gif', 'heic'].contains(ext)) { + file.delete(); + throw UnsupportedDownloadFormatException('Nelze uložit. Neznámý typ souboru ($ext).'); + } + + file = await file.rename('$savePath.$ext'); + final result = await ImageGallerySaver.saveFile('$savePath.$ext', isReturnPathOfIOS: Platform.isIOS); if (!result['isSuccess']) { throw Error(); } T.success(L.TOAST_IMAGE_SAVE_OK, bg: colors.success); + file.delete(); } else { T.error('Nelze uložit. Povolte ukládání, prosím.', bg: colors.danger); } + } on UnsupportedDownloadFormatException catch (exception) { + T.error(exception.message, bg: colors.danger); } catch (error) { T.error(L.TOAST_IMAGE_SAVE_ERROR, bg: colors.danger); Sentry.captureException(error); + print((error as Error).stackTrace); } finally { setState(() => _saving = false); } diff --git a/lib/pages/LoginPage.dart b/lib/pages/LoginPage.dart index 3fb0792..04773e8 100644 --- a/lib/pages/LoginPage.dart +++ b/lib/pages/LoginPage.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -9,9 +7,10 @@ import 'package:fyx/model/Credentials.dart'; import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/model/reponses/LoginResponse.dart'; import 'package:fyx/pages/TutorialPage.dart'; +import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; -import 'package:fyx/theme/skin/SkinColors.dart'; import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class LoginPage extends StatefulWidget { @override @@ -24,6 +23,7 @@ class _LoginPageState extends State { late SkinColors colors; bool _isRunning = false; bool _useTokenToLogin = false; + bool _terms = false; @override void initState() { @@ -45,13 +45,21 @@ class _LoginPageState extends State { colors = Skin.of(context).theme.colors; return WillPopScope( - onWillPop: () async => false, - child: Container( - padding: EdgeInsets.all(16), - decoration: BoxDecoration(gradient: colors.gradient), - child: formFactory(), - ), - ); + onWillPop: () async => false, + child: SingleChildScrollView( + physics: NeverScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: MediaQuery.of(context).size.width, + minHeight: MediaQuery.of(context).size.height, + ), + child: IntrinsicHeight( + child: Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration(gradient: colors.gradient), + child: formFactory(), + ), + )))); } @override @@ -62,11 +70,13 @@ class _LoginPageState extends State { } Widget formFactory() { - final textfieldDecoration = BoxDecoration(borderRadius: BorderRadius.circular(4), color: colors.background, border: Border.all(color: colors.background)); + final textfieldDecoration = + BoxDecoration(borderRadius: BorderRadius.circular(4), color: colors.background, border: Border.all(color: colors.background)); var offset = (MediaQuery.of(context).viewInsets.bottom / 3); return Column( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, children: [ Container( width: 120, @@ -75,8 +85,10 @@ class _LoginPageState extends State { 'assets/logo.png', color: colors.primary, ), - decoration: - BoxDecoration(color: colors.background, borderRadius: BorderRadius.circular(32), boxShadow: [BoxShadow(color: colors.dark, offset: Offset(0, 0), blurRadius: 16)]), + decoration: BoxDecoration( + color: colors.background, + borderRadius: BorderRadius.circular(32), + boxShadow: [BoxShadow(color: colors.dark, offset: Offset(0, 0), blurRadius: 16)]), ), AnimatedPadding( padding: EdgeInsets.only(top: 128 - offset), @@ -114,9 +126,40 @@ class _LoginPageState extends State { duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, child: Container( - child: this._buildButton(context), + child: Column( + children: [ + this._buildButton(context), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CupertinoSwitch( + activeColor: colors.highlight, + thumbColor: colors.background, + trackColor: colors.primary, + onChanged: (bool? value) => setState(() => _terms = value ?? false), + value: _terms, + ), + GestureDetector( + child: Text( + 'Souhlasím s podmínkami užití.', + style: TextStyle(color: colors.light), + ), + onTap: () => setState(() => _terms = !_terms), + ) + ], + ), + GestureDetector( + child: Text( + '${L.TERMS} ↗', + style: TextStyle(decoration: TextDecoration.underline, color: colors.light, fontSize: 13), + ), + onTap: () => T.openLink('https://nyx.cz/terms'), + ) + ], + ), ), - ) + ), ], ); } @@ -127,40 +170,40 @@ class _LoginPageState extends State { return Container( width: 200, child: CupertinoButton( + disabledColor: colors.light.withOpacity(.3), child: _isRunning ? CupertinoActivityIndicator() : Text( 'Přihlásit', style: TextStyle(color: colors.primary), ), - onPressed: () { - if (_isRunning) { - return; - } - - setState(() => _isRunning = true); - - if (_useTokenToLogin && _tokenController.text.length > 0) { - ApiController().setCredentials(Credentials(_loginController.text, _tokenController.text)).then((Credentials? credentials) { - if (credentials != null) { - // TODO: Refactor 👇? This is edge case usage... - ApiController().provider.setCredentials(credentials); - MainRepository().credentials = credentials; - Navigator.of(context).pushNamed('/home'); - } - }).whenComplete(() { - setState(() => _isRunning = false); - }); - return; - } - - ApiController().login(_loginController.text).then((LoginResponse response) { - Navigator.of(context).pushNamed('/token', arguments: new TutorialPageArguments(token: response.authCode, username: _loginController.text)); - }).catchError((error) { - print(error); - T.error(error.toString(), bg: colors.danger); - }).whenComplete(() => setState(() => _isRunning = false)); - }, + onPressed: _isRunning || !_terms || _loginController.text.length == 0 + ? null + : () { + setState(() => _isRunning = true); + + if (_useTokenToLogin && _tokenController.text.length > 0) { + ApiController().setCredentials(Credentials(_loginController.text, _tokenController.text)).then((Credentials? credentials) { + if (credentials != null) { + // TODO: Refactor 👇? This is edge case usage... + ApiController().provider.setCredentials(credentials); + MainRepository().credentials = credentials; + Navigator.of(context).pushNamed('/home'); + } + }).whenComplete(() { + setState(() => _isRunning = false); + }); + return; + } + + ApiController().login(_loginController.text).then((LoginResponse response) { + Navigator.of(context) + .pushNamed('/token', arguments: new TutorialPageArguments(token: response.authCode, username: _loginController.text)); + }).catchError((error) { + print(error); + T.error(error.toString(), bg: colors.danger); + }).whenComplete(() => setState(() => _isRunning = false)); + }, color: colors.background, ), ); diff --git a/lib/pages/NoticesPage.dart b/lib/pages/NoticesPage.dart index 981e0c1..63f0315 100644 --- a/lib/pages/NoticesPage.dart +++ b/lib/pages/NoticesPage.dart @@ -3,16 +3,17 @@ import 'package:flutter/material.dart'; import 'package:fyx/components/Avatar.dart' as component; import 'package:fyx/components/ContentBoxLayout.dart'; import 'package:fyx/components/PullToRefreshList.dart'; +import 'package:fyx/components/post/PostThumbs.dart'; import 'package:fyx/controllers/AnalyticsProvider.dart'; import 'package:fyx/controllers/ApiController.dart'; +import 'package:fyx/model/post/PostThumbItem.dart'; import 'package:fyx/model/post/content/Regular.dart'; import 'package:fyx/model/reponses/FeedNoticesResponse.dart'; import 'package:fyx/pages/DiscussionPage.dart'; import 'package:fyx/theme/Helpers.dart'; import 'package:fyx/theme/L.dart'; -import 'package:fyx/theme/T.dart'; -import 'package:fyx/theme/skin/SkinColors.dart'; import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class NoticesPage extends StatefulWidget { NoticesPage({Key? key}) : super(key: key); @@ -91,10 +92,12 @@ class _NoticesPageState extends State with WidgetsBindingObserver { ), bottomWidget: Column( children: [ - if (item.thumbsUp.length > 0) buildLikes(context, item.thumbsUp, result.lastVisit), - SizedBox( - height: 8, - ), + if (item.thumbsUp.length > 0) + PostThumbs(item.thumbsUp.map((thumb) => PostThumbItem.fromNoticeThumbsUp(thumb, result.lastVisit)).toList()), + if (item.replies.length > 0) + const SizedBox( + height: 8, + ), if (item.replies.length > 0) buildReplies(context, item.replies, result.lastVisit), ], ), @@ -104,38 +107,6 @@ class _NoticesPageState extends State with WidgetsBindingObserver { })); } - Widget buildLikes(BuildContext context, List thumbsUp, int lastVisit) { - var avatars = thumbsUp - .map((thumbUp) => Tooltip( - message: thumbUp.nick, - waitDuration: Duration(milliseconds: 0), - child: Padding( - padding: const EdgeInsets.only(left: 5, bottom: 5), - child: component.Avatar( - Helpers.avatarUrl(thumbUp.nick), - size: 22, - isHighlighted: thumbUp.time > lastVisit, - ), - ), - )) - .toList(); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 4), - child: Icon( - Icons.thumb_up, - size: 22, - ), - ), - Expanded( - child: Wrap(children: avatars), - ) - ], - ); - } - Widget buildReplies(BuildContext context, List replies, int lastVisit) { List replyRows = replies.map((reply) { return GestureDetector( @@ -160,7 +131,8 @@ class _NoticesPageState extends State with WidgetsBindingObserver { Expanded( child: Padding( padding: const EdgeInsets.only(top: 6.0), - child: Text(Helpers.stripHtmlTags(reply.text), style: TextStyle(fontSize: 14, fontWeight: reply.time > lastVisit ? FontWeight.bold : FontWeight.normal)), + child: Text(Helpers.stripHtmlTags(reply.text), + style: TextStyle(fontSize: 14, fontWeight: reply.time > lastVisit ? FontWeight.bold : FontWeight.normal)), )) ], ), diff --git a/lib/theme/Helpers.dart b/lib/theme/Helpers.dart index 8ea10f6..77f6fdb 100644 --- a/lib/theme/Helpers.dart +++ b/lib/theme/Helpers.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:fyx/theme/L.dart'; import 'package:html/parser.dart'; import 'package:intl/intl.dart'; @@ -105,4 +107,11 @@ class Helpers { static String avatarUrl(String username) { return 'https://nyx.cz/${username.substring(0, 1)}/$username.gif'; } -} \ No newline at end of file + + // https://stackoverflow.com/a/61929967/4026345 + static String uuid(int length) { + const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + Random _rnd = Random(); + return String.fromCharCodes(Iterable.generate(length, (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)))); + } +} diff --git a/pubspec.lock b/pubspec.lock index 1977573..9fae403 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -28,14 +28,14 @@ packages: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.3.0" async: dependency: "direct main" description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" auto_size_text: dependency: "direct main" description: @@ -56,7 +56,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.2.1" build_config: dependency: transitive description: @@ -84,14 +84,14 @@ packages: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.1.7" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.0.1" + version: "7.2.2" built_collection: dependency: transitive description: @@ -112,7 +112,21 @@ packages: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.2.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" carousel_slider: dependency: "direct main" description: @@ -126,7 +140,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -147,7 +161,7 @@ packages: name: chewie url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.3.0" chewie_audio: dependency: transitive description: @@ -231,7 +245,7 @@ packages: name: device_info url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" device_info_platform_interface: dependency: transitive description: @@ -245,7 +259,7 @@ packages: name: dio url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.4" fake_async: dependency: transitive description: @@ -308,14 +322,14 @@ packages: name: firebase_core_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.2.3" + version: "4.2.4" firebase_core_web: dependency: transitive description: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "1.6.0" firebase_messaging: dependency: "direct main" description: @@ -362,7 +376,7 @@ packages: name: flutter_cache_manager url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.3.0" flutter_cupertino_settings: dependency: "direct main" description: @@ -376,7 +390,7 @@ packages: name: flutter_dotenv url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.0.2" flutter_highlight: dependency: "direct main" description: @@ -390,14 +404,14 @@ packages: name: flutter_html url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.1" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.9.0" + version: "0.9.2" flutter_layout_grid: dependency: transitive description: @@ -405,20 +419,25 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_markdown: dependency: "direct main" description: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.2" + version: "0.6.9" flutter_math_fork: dependency: transitive description: name: flutter_math_fork url: "https://pub.dartlang.org" source: hosted - version: "0.3.3+1" + version: "0.4.2+2" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -456,7 +475,7 @@ packages: name: fluttertoast url: "https://pub.dartlang.org" source: hosted - version: "8.0.8" + version: "8.0.9" frontend_server_client: dependency: transitive description: @@ -491,7 +510,7 @@ packages: name: hive url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.6" hive_flutter: dependency: "direct main" description: @@ -505,7 +524,7 @@ packages: name: hive_generator url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.2" html: dependency: "direct main" description: @@ -526,7 +545,7 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.13.3" + version: "0.13.4" http_multi_server: dependency: transitive description: @@ -547,7 +566,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.3" image_gallery_saver: dependency: "direct main" description: @@ -561,7 +580,14 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.8.4+4" + version: "0.8.5" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.4+11" image_picker_for_web: dependency: transitive description: @@ -569,6 +595,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.4+11" image_picker_platform_interface: dependency: transitive description: @@ -624,7 +657,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: "direct main" description: @@ -638,7 +678,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" nested: dependency: transitive description: @@ -675,7 +715,7 @@ packages: source: hosted version: "2.0.2" package_info_plus: - dependency: "direct overridden" + dependency: transitive description: name: package_info_plus url: "https://pub.dartlang.org" @@ -729,7 +769,7 @@ packages: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.5.1" + version: "0.5.1+1" path_parsing: dependency: transitive description: @@ -743,7 +783,21 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" path_provider_linux: dependency: transitive description: @@ -785,7 +839,21 @@ packages: name: permission_handler url: "https://pub.dartlang.org" source: hosted - version: "8.1.6" + version: "9.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + url: "https://pub.dartlang.org" + source: hosted + 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.4" permission_handler_platform_interface: dependency: transitive description: @@ -793,6 +861,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.7.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" petitparser: dependency: transitive description: @@ -813,7 +888,7 @@ packages: name: platform url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" plugin_platform_interface: dependency: transitive description: @@ -841,7 +916,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "6.0.2" pub_semver: dependency: transitive description: @@ -876,14 +951,14 @@ packages: name: sentry url: "https://pub.dartlang.org" source: hosted - version: "6.2.1" + version: "6.3.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter url: "https://pub.dartlang.org" source: hosted - version: "6.1.0" + version: "6.3.0" share: dependency: "direct main" description: @@ -897,14 +972,28 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.13" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" shared_preferences_macos: dependency: transitive description: @@ -932,7 +1021,7 @@ packages: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" shelf: dependency: transitive description: @@ -1035,7 +1124,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.8" timing: dependency: transitive description: @@ -1063,7 +1152,21 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.9" + version: "6.0.20" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.15" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.15" url_launcher_linux: dependency: transitive description: @@ -1119,14 +1222,35 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" + very_good_analysis: + dependency: transitive + description: + name: very_good_analysis + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" video_player: dependency: "direct main" description: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "2.2.7" + version: "2.2.19" + video_player_android: + dependency: transitive + description: + name: video_player_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" video_player_platform_interface: dependency: transitive description: @@ -1147,35 +1271,35 @@ packages: name: wakelock url: "https://pub.dartlang.org" source: hosted - version: "0.5.2" + version: "0.6.1+1" wakelock_macos: dependency: transitive description: name: wakelock_macos url: "https://pub.dartlang.org" source: hosted - version: "0.1.0+1" + version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "0.2.1+1" + version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web url: "https://pub.dartlang.org" source: hosted - version: "0.2.0+1" + version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows url: "https://pub.dartlang.org" source: hosted - version: "0.1.0" + version: "0.2.0" watcher: dependency: transitive description: @@ -1226,5 +1350,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + dart: ">=2.15.0 <3.0.0" + flutter: ">=2.10.2" diff --git a/pubspec.yaml b/pubspec.yaml index 646aad2..99d2049 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,56 +11,60 @@ description: A new Flutter project. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.2+45 +version: 0.8.3+57 environment: sdk: ">=2.12.0 <3.0.0" + flutter: ^2.10.2 dependencies: - provider: ^5.0.0 - meta: ^1.3.0 - dio: ^4.0.0 + provider: ^6.0.2 + meta: ^1.7.0 + dio: ^4.0.3 flutter_sticky_header: ^0.6.0 html: ^0.15.0 - flutter_html: ^2.1.0 - shared_preferences: ^2.0.6 - fluttertoast: ^8.0.8 + flutter_html: ^2.2.1 + shared_preferences: ^2.0.13 + fluttertoast: ^8.0.9 html_unescape: ^2.0.0 - cached_network_image: ^3.0.0 + cached_network_image: ^3.2.0 carousel_slider: ^4.0.0 - device_info: ^2.0.2 + device_info: ^2.0.3 package_info: ^2.0.2 - url_launcher: ^6.0.9 + url_launcher: ^6.0.20 photo_view: ^0.13.0 auto_size_text: ^3.0.0-nullsafety.0 - image_picker: ^0.8.4+4 + image_picker: ^0.8.4+10 path: ^1.8.0 - image: ^3.0.2 - async: ^2.6.1 - chewie: ^1.2.2 - video_player: ^2.2.7 + image: ^3.1.3 + async: ^2.8.2 + chewie: ^1.3.0 + video_player: ^2.2.19 flutter_cupertino_settings: ^0.5.0 - flutter_markdown: ^0.6.2 - http: ^0.13.3 + flutter_markdown: ^0.6.9 + http: ^0.13.4 share: ^2.0.4 - hive: ^2.0.4 + hive: ^2.0.6 hive_flutter: ^1.1.0 - firebase_analytics: ^8.1.2 - sentry_flutter: 6.1.0 - flutter_dotenv: ^5.0.0 + firebase_analytics: ^8.3.4 + sentry_flutter: 6.3.0 + flutter_dotenv: ^5.0.2 firebase_messaging: ^11.1.0 flutter_highlight: ^0.7.0 image_gallery_saver: ^1.7.1 - path_provider: 2.0.6 - permission_handler: 8.1.6 + path_provider: 2.0.9 + permission_handler: 9.2.0 # Dart-lang core plugins 👇 http_parser: ^4.0.0 - mime: ^1.0.0 + mime: ^1.0.1 intl: ^0.17.0 flutter: sdk: flutter + flutter_localizations: + sdk: flutter + # The following adds the Cupertino Icons font to your application. @@ -70,7 +74,7 @@ dependencies: dependency_overrides: # # https://github.com/flutter/flutter/issues/44435#issuecomment-817583694 # webview_flutter: ^2.0.10 - package_info_plus: ^1.3.0 +# package_info_plus: ^1.3.0 dev_dependencies: hive_generator: ^1.1.0 diff --git a/test/api_test.dart b/test/api_test.dart index edc83e9..4fe2bf3 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -17,9 +17,9 @@ import 'package:shared_preferences/shared_preferences.dart'; class ApiMock implements IApiProvider { final Map loginJsonResponse; - TOnError onError; - TOnAuthError onAuthError; - Credentials _credentials; + TOnError? onError; + TOnAuthError? onAuthError; + Credentials? _credentials; final bool emptyCredentials; ApiMock(this.loginJsonResponse, {this.emptyCredentials = false}); @@ -39,46 +39,46 @@ class ApiMock implements IApiProvider { // Wrong token // {"result":"error","code":"401","error":"Not Authorized","auth_state":"AUTH_EXISTING","auth_dev_comment":"There is already confirmed authorization for this App name, but you haven't provided correct token. If you you've lost your auth token, tell user to cancel existing authorization. It might be also caused by using the same App name by the same app on different devices or different apps."} - return Future(() => Response>(data: this.loginJsonResponse)); + return Future(() => Response>(data: this.loginJsonResponse, requestOptions: RequestOptions(path: 'dummy'))); } @override // TODO Future fetchBookmarks() { - return Future(() => Response(data: '')); + throw UnimplementedError(); } @override // TODO Future fetchHistory() { - return Future(() => Response(data: '')); + throw UnimplementedError(); } @override var onContextData; @override - Future fetchDiscussion(int id, {int lastId, String user}) { + Future fetchDiscussion(int id, {int? lastId, String? search, String? user}) { // TODO: implement fetchDiscussion - return null; + throw UnimplementedError(); } @override - Future fetchMail({int lastId}) { + Future fetchMail({int? lastId}) { // TODO: implement fetchMail - return null; + throw UnimplementedError(); } @override - Credentials getCredentials() { - if (_credentials != null && _credentials.isValid) { + Credentials? getCredentials() { + if (_credentials != null && _credentials!.isValid) { return _credentials; } return null; } @override - Credentials setCredentials(Credentials creds) { + Credentials? setCredentials(Credentials? creds) { if (creds != null && creds.isValid) { _credentials = creds; } @@ -89,43 +89,43 @@ class ApiMock implements IApiProvider { @override Future giveRating(int discussionId, int postId, bool add, bool confirm, bool remove) { // TODO: implement giveRating - return null; + throw UnimplementedError(); } @override Future logout() { // TODO: implement logout - return null; + throw UnimplementedError(); } @override - Future postDiscussionMessage(int id, String message, {Map attachment}) { + Future postDiscussionMessage(int id, String message, {Map? attachment}) { // TODO: implement postDiscussionMessage - return null; + throw UnimplementedError(); } @override - Future sendMail(String recipient, String message, {Map attachment}) { + Future sendMail(String recipient, String message, {Map? attachment}) { // TODO: implement sendMail - return null; + throw UnimplementedError(); } @override Future setPostReminder(int discussionId, int postId, bool setReminder) { // TODO: implement setPostReminder - return null; + throw UnimplementedError(); } @override Future testAuth() { // TODO: implement testAuth - return null; + throw UnimplementedError(); } @override Future registerFcmToken(String token) { // TODO: implement registerFcmToken - return null; + throw UnimplementedError(); } @override @@ -159,7 +159,7 @@ class ApiMock implements IApiProvider { } @override - Future uploadFile(List> attachments, {int id}) { + Future uploadFile(List> attachments, {int? id}) { // TODO: implement uploadFile throw UnimplementedError(); } @@ -169,6 +169,24 @@ class ApiMock implements IApiProvider { // TODO: implement votePoll throw UnimplementedError(); } + + @override + Future rollDice(int discussionId, int postId) { + // TODO: implement rollDice + throw UnimplementedError(); + } + + @override + Future deleteDiscussionMessage(int discussionId, int postId) { + // TODO: implement deleteDiscussionMessage + throw UnimplementedError(); + } + + @override + Future getPostRatings(int discussionId, int postId) { + // TODO: implement getPostRatings + throw UnimplementedError(); + } } void main() { @@ -198,9 +216,9 @@ void main() { expect(loginResponse.isAuthorized, true); var creds = await api.getCredentials(); - expect(creds.nickname, loginName); - expect(creds.token, '44a3d1241830ca61a592e28df783007d'); - expect(creds.fcmToken, null); + expect(creds?.nickname, loginName); + expect(creds?.token, '44a3d1241830ca61a592e28df783007d'); + expect(creds?.fcmToken, null); }); test('User is logged in and uses old identity storage.', () async { @@ -213,7 +231,7 @@ void main() { ); var prefs = await SharedPreferences.getInstance(); - String identity = prefs.getString('identity'); + String? identity = prefs.getString('identity'); expect(identity, null); // Set the old storage manually. @@ -223,9 +241,9 @@ void main() { var creds = await api.getCredentials(); // Check the identity object - expect(creds.nickname, loginName); - expect(creds.token, '44a3d1241830ca61a592e28df783007d'); - expect(creds.fcmToken, null); + expect(creds?.nickname, loginName); + expect(creds?.token, '44a3d1241830ca61a592e28df783007d'); + expect(creds?.fcmToken, null); // Reload the prefs prefs = await SharedPreferences.getInstance(); diff --git a/test/model/post_test.dart b/test/model/post_test.dart index de69d7c..145bb18 100644 --- a/test/model/post_test.dart +++ b/test/model/post_test.dart @@ -48,7 +48,7 @@ void main() { var imagesMatcher = [ Image('http://i.nyx.cz/files/00/00/20/68/2068213_7dde4d7aa8e3021dd610.jpg?name=11.jpg', - 'http://www.nyx.cz/i/t/b0ccf0fde73a5840dea9f0dbc5d18e6d.png?url=http%3A%2F%2Fi.nyx.cz%2Ffiles%2F00%2F00%2F20%2F68%2F2068213_7dde4d7aa8e3021dd610.jpg%3Fname%3D11.jpg') + thumb: 'http://www.nyx.cz/i/t/b0ccf0fde73a5840dea9f0dbc5d18e6d.png?url=http%3A%2F%2Fi.nyx.cz%2Ffiles%2F00%2F00%2F20%2F68%2F2068213_7dde4d7aa8e3021dd610.jpg%3Fname%3D11.jpg') ]; Function deepEq = const DeepCollectionEquality().equals; @@ -121,9 +121,9 @@ void main() { expect(post.content.videos[0].type, VIDEO_TYPE.youtube); expect(post.content.videos[0].image, 'http://img.youtube.com/vi/B1_gcCu0-oI/0.jpg'); expect(post.content.videos[0].thumb, 'http://www.nyx.cz/i/t/e8464b77ee2b7a726f174be309201ade.png?url=http%3A%2F%2Fimg.youtube.com%2Fvi%2FB1_gcCu0-oI%2F0.jpg'); - expect(post.content.videos[0].link.url, 'https://www.youtube.com/watch?v=B1_gcCu0-oI'); - expect(post.content.videos[0].link.fancyUrl, 'youtube.com/watch?v=B1_gcCu0-oI'); - expect(post.content.videos[0].link.title, 'youtube.com/watch?v=B1_gcCu0-oI'); + expect(post.content.videos[0].link?.url, 'https://www.youtube.com/watch?v=B1_gcCu0-oI'); + expect(post.content.videos[0].link?.fancyUrl, 'youtube.com/watch?v=B1_gcCu0-oI'); + expect(post.content.videos[0].link?.title, 'youtube.com/watch?v=B1_gcCu0-oI'); expect(0, parse(post.content.body).querySelectorAll('div[data-embed-value="B1_gcCu0-oI"]').length); // Test images