diff --git a/ios/Flutter/.last_build_id b/ios/Flutter/.last_build_id new file mode 100644 index 0000000..2e00983 --- /dev/null +++ b/ios/Flutter/.last_build_id @@ -0,0 +1 @@ +20ad19f2b9a812ac7774ca58ddf04b2e \ No newline at end of file diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..e8efba1 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..399e934 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..1e8c3c9 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..ac80ad7 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,55 @@ +PODS: + - esys_flutter_share (0.0.1): + - Flutter + - Flutter (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - path_provider (0.0.1): + - Flutter + - shared_preferences (0.0.1): + - Flutter + - sqflite (0.0.1): + - Flutter + - FMDB (~> 2.7.2) + - url_launcher (0.0.1): + - Flutter + +DEPENDENCIES: + - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) + - Flutter (from `Flutter`) + - path_provider (from `.symlinks/plugins/path_provider/ios`) + - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) + - url_launcher (from `.symlinks/plugins/url_launcher/ios`) + +SPEC REPOS: + trunk: + - FMDB + +EXTERNAL SOURCES: + esys_flutter_share: + :path: ".symlinks/plugins/esys_flutter_share/ios" + Flutter: + :path: Flutter + path_provider: + :path: ".symlinks/plugins/path_provider/ios" + shared_preferences: + :path: ".symlinks/plugins/shared_preferences/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" + url_launcher: + :path: ".symlinks/plugins/url_launcher/ios" + +SPEC CHECKSUMS: + esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4 + Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c + shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d + sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0 + url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef + +PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c + +COCOAPODS: 1.9.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7ab9911..eb5416f 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 */ @@ -13,6 +13,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + F627D0FEEE0CC42B20D97D4D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E5591CE3BD9F89AE791097F /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -31,7 +32,11 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 20AF123CE6B282DF5FCC0E08 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4E5591CE3BD9F89AE791097F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5D359FC3B8BF643CBF087D7C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 5DC9EF56CF79F18EC6F8E97B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -49,12 +54,31 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F627D0FEEE0CC42B20D97D4D /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 721C96BBF01C118C12E658F5 /* Pods */ = { + isa = PBXGroup; + children = ( + 20AF123CE6B282DF5FCC0E08 /* Pods-Runner.debug.xcconfig */, + 5D359FC3B8BF643CBF087D7C /* Pods-Runner.release.xcconfig */, + 5DC9EF56CF79F18EC6F8E97B /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 78D8D0D60D5196915B006F06 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4E5591CE3BD9F89AE791097F /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +96,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 721C96BBF01C118C12E658F5 /* Pods */, + 78D8D0D60D5196915B006F06 /* Frameworks */, ); sourceTree = ""; }; @@ -113,12 +139,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 2C32CA1979B0B9EA89AB49DE /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ADBCA6764A9EF0DC6AA5E56E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -177,6 +205,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2C32CA1979B0B9EA89AB49DE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -205,6 +255,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + ADBCA6764A9EF0DC6AA5E56E /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -241,7 +308,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -297,13 +363,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = NMDSW6KGG7; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -318,7 +388,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -374,7 +443,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -418,7 +486,8 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -431,13 +500,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = NMDSW6KGG7; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -458,13 +531,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = NMDSW6KGG7; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - 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 a28140c..fb2dffc 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,8 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + + - - + + diff --git a/lib/comment_tree.dart b/lib/comment_tree.dart index d0da0da..f8d6630 100644 --- a/lib/comment_tree.dart +++ b/lib/comment_tree.dart @@ -1,5 +1,16 @@ import 'package:lemmy_api_client/lemmy_api_client.dart'; +import 'util/hot_rank.dart'; + +enum CommentSortType { + hot, + top, + // ignore: constant_identifier_names + new_, + old, + chat, +} + class CommentTree { CommentView comment; List children; @@ -30,4 +41,69 @@ class CommentTree { var result = parents.map(gatherChildren).toList(); return result; } + + void sort(CommentSortType sortType) { + switch (sortType) { + case CommentSortType.chat: + // throw Exception('i dont do this kinda stuff kido'); + return; + case CommentSortType.hot: + return _sort((b, a) => + a.comment.computedHotRank.compareTo(b.comment.computedHotRank)); + case CommentSortType.new_: + return _sort( + (b, a) => a.comment.published.compareTo(b.comment.published)); + case CommentSortType.old: + return _sort( + (b, a) => b.comment.published.compareTo(a.comment.published)); + case CommentSortType.top: + return _sort((b, a) => a.comment.score.compareTo(b.comment.score)); + } + } + + void _sort(int compare(CommentTree a, CommentTree b)) { + children.sort(compare); + for (var el in children) { + el._sort(compare); + } + } + + static List sortList( + CommentSortType sortType, List comms) { + switch (sortType) { + case CommentSortType.chat: + throw Exception('i dont do this kinda stuff kido'); + case CommentSortType.hot: + comms.sort((b, a) => + a.comment.computedHotRank.compareTo(b.comment.computedHotRank)); + for (var i = 0; i < comms.length; i++) { + comms[i].sort(sortType); + } + return comms; + + case CommentSortType.new_: + comms + .sort((b, a) => a.comment.published.compareTo(b.comment.published)); + for (var i = 0; i < comms.length; i++) { + comms[i].sort(sortType); + } + return comms; + + case CommentSortType.old: + comms + .sort((b, a) => b.comment.published.compareTo(a.comment.published)); + for (var i = 0; i < comms.length; i++) { + comms[i].sort(sortType); + } + return comms; + + case CommentSortType.top: + comms.sort((b, a) => a.comment.score.compareTo(b.comment.score)); + for (var i = 0; i < comms.length; i++) { + comms[i].sort(sortType); + } + return comms; + } + throw Exception('unreachable'); + } } diff --git a/lib/pages/full_post.dart b/lib/pages/full_post.dart new file mode 100644 index 0000000..ec44ca9 --- /dev/null +++ b/lib/pages/full_post.dart @@ -0,0 +1,103 @@ +import 'package:esys_flutter_share/esys_flutter_share.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:lemmy_api_client/lemmy_api_client.dart'; + +import '../widgets/comment_section.dart'; +import '../widgets/post.dart'; + +class FullPostPage extends HookWidget { + final Future fullPost; + final PostView post; + + FullPostPage({@required int id, @required String instanceUrl}) + : assert(id != null), + assert(instanceUrl != null), + fullPost = LemmyApi(instanceUrl).v1.getPost(id: id), + post = null; + FullPostPage.fromPostView(this.post) + : fullPost = LemmyApi(post.communityActorId.split('/')[2]) + .v1 + .getPost(id: post.id); + + void sharePost() => Share.text('Share post', post.apId, 'text/plain'); + + void savePost() { + // + } + + @override + Widget build(BuildContext context) { + final fullPostSnap = useFuture(this.fullPost); + final fullPost = fullPostSnap.data; + + final savedIcon = () { + if (fullPostSnap.hasData) { + if (fullPost.post.saved == null || !fullPost.post.saved) { + return Icons.bookmark_border; + } else { + return Icons.bookmark; + } + } + + if (post != null) { + if (post.saved == null || !post.saved) { + return Icons.bookmark_border; + } else { + return Icons.bookmark; + } + } + + return Icons.bookmark_border; + }(); + + return Scaffold( + appBar: AppBar( + leading: BackButton(), + actions: [ + IconButton(icon: Icon(Icons.share), onPressed: sharePost), + IconButton(icon: Icon(savedIcon), onPressed: savePost), + IconButton( + icon: Icon(Icons.more_vert), onPressed: () {}), // TODO: more menu + ], + ), + body: fullPostSnap.hasData || post != null + // FUTURE SUCCESS + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + if (fullPostSnap.hasData) + Post(fullPost.post, fullPost: true) + else if (post != null) + Post(post, fullPost: true) + else + CircularProgressIndicator(), + if (fullPostSnap.hasData) + CommentSection(fullPost.comments, + postCreatorId: fullPost.post.creatorId) + else + Container( + child: Center(child: CircularProgressIndicator()), + padding: EdgeInsets.only(top: 40), + ), + ], + ) + : fullPostSnap.hasError + // FUTURE FAILURE + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error, size: 30), + Padding(padding: EdgeInsets.all(5)), + Text('ERROR: ${fullPostSnap.error.toString()}'), + ], + ), + ) + // FUTURE IN PROGRESS + : Container( + child: Center(child: CircularProgressIndicator()), + color: Theme.of(context).canvasColor), + ); + } +} diff --git a/lib/pages/profile_tab.dart b/lib/pages/profile_tab.dart index a7bd4ba..36e212c 100644 --- a/lib/pages/profile_tab.dart +++ b/lib/pages/profile_tab.dart @@ -102,7 +102,7 @@ class UserProfileTab extends HookWidget { ), onPressed: () { Navigator.of(context) - .push(MaterialPageRoute(builder: (_) => Settings())); + .push(MaterialPageRoute(builder: (_) => SettingsPage())); }, ) ], diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 3f83f7b..4282b98 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import '../stores/accounts_store.dart'; import '../stores/config_store.dart'; -class Settings extends StatelessWidget { +class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { var theme = Theme.of(context); diff --git a/lib/util/hot_rank.dart b/lib/util/hot_rank.dart new file mode 100644 index 0000000..6d56f4b --- /dev/null +++ b/lib/util/hot_rank.dart @@ -0,0 +1,21 @@ +import 'dart:math' show log, max, pow, ln10; + +import 'package:lemmy_api_client/lemmy_api_client.dart'; + +/// Calculates hot rank +/// because API always claims it's `0` +/// and web version of lemmy also calculates it when loading comments +/// +/// implementation taken from here: +/// https://github.com/LemmyNet/lemmy/blob/main/ui/src/utils.ts#L182-L203 +double _calculateHotRank(int score, DateTime time) { + log10(num x) => log(x) / ln10; + + final elapsed = (time.difference(DateTime.now()).inMilliseconds).abs() / 36e5; + + return (10000 * log10(max(1, 3 + score))) / pow(elapsed + 2, 1.8); +} + +extension CommentHotRank on CommentView { + double get computedHotRank => _calculateHotRank(score, published); +} diff --git a/lib/util/text_color.dart b/lib/util/text_color.dart new file mode 100644 index 0000000..0a2f4de --- /dev/null +++ b/lib/util/text_color.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +Color textColorBasedOnBackground(Color color) { + if (color.computeLuminance() > 0.5) { + return Colors.black; + } else { + return Colors.white; + } +} diff --git a/lib/widgets/comment.dart b/lib/widgets/comment.dart index be479bc..641e04d 100644 --- a/lib/widgets/comment.dart +++ b/lib/widgets/comment.dart @@ -1,7 +1,10 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:lemmy_api_client/lemmy_api_client.dart'; +import 'package:timeago/timeago.dart' as timeago; import '../comment_tree.dart'; +import '../util/text_color.dart'; import 'markdown_text.dart'; class Comment extends StatelessWidget { @@ -14,41 +17,62 @@ class Comment extends StatelessWidget { @required this.postCreatorId, }); + void _openMoreMenu() { + print('OPEN MORE MENU'); + } + void _goToUser() { print('GO TO USER'); } + void _save(bool save) { + print('SAVE COMMENT, $save'); + } + + void _reply() { + print('OPEN REPLY BOX'); + } + + void _vote(VoteType vote) { + print('COMMENT VOTE: ${vote.toString()}'); + } + bool get isOP => commentTree.comment.creatorId == postCreatorId; @override Widget build(BuildContext context) { - var comment = commentTree.comment; + final comment = commentTree.comment; + + final saved = comment.saved ?? false; // decide which username to use - var username; - if (comment.creatorPreferredUsername != null && - comment.creatorPreferredUsername != '') { - username = comment.creatorPreferredUsername; - } else { - username = '@${comment.creatorName}'; - } + final username = () { + if (comment.creatorPreferredUsername != null && + comment.creatorPreferredUsername != '') { + return comment.creatorPreferredUsername; + } else { + return '@${comment.creatorName}'; + } + }(); + + final body = () { + if (comment.deleted) { + return Flexible( + child: Text( + 'comment deleted by creator', + style: TextStyle(fontStyle: FontStyle.italic), + )); + } else if (comment.removed) { + return Flexible( + child: Text( + 'comment deleted by moderator', + style: TextStyle(fontStyle: FontStyle.italic), + )); + } else { + return Flexible(child: MarkdownText(commentTree.comment.content)); + } + }(); - var body; - if (comment.deleted) { - body = Flexible( - child: Text( - 'comment deleted by creator', - style: TextStyle(fontStyle: FontStyle.italic), - )); - } else if (comment.removed) { - body = Flexible( - child: Text( - 'comment deleted by moderator', - style: TextStyle(fontStyle: FontStyle.italic), - )); - } else { - body = Flexible(child: MarkdownText(commentTree.comment.content)); - } return Column( children: [ Container( @@ -56,10 +80,10 @@ class Comment extends StatelessWidget { children: [ Row(children: [ if (comment.creatorAvatar != null) - InkWell( - onTap: _goToUser, - child: Padding( - padding: const EdgeInsets.only(right: 5), + Padding( + padding: const EdgeInsets.only(right: 5), + child: InkWell( + onTap: _goToUser, child: CachedNetworkImage( imageUrl: comment.creatorAvatar, height: 20, @@ -83,17 +107,43 @@ class Comment extends StatelessWidget { )), onLongPress: _goToUser, ), - if (isOP) CommentTag('OP', Theme.of(context).accentColor), - if (comment.banned) CommentTag('BANNED', Colors.red), + if (isOP) _CommentTag('OP', Theme.of(context).accentColor), + if (comment.banned) _CommentTag('BANNED', Colors.red), if (comment.bannedFromCommunity) - CommentTag('BANNED FROM COMMUNITY', Colors.red), + _CommentTag('BANNED FROM COMMUNITY', Colors.red), Spacer(), Text(comment.score.toString()), + Text(' 路 '), + Text(timeago.format(comment.published, locale: 'en_short')), ]), Row(children: [body]), Row(children: [ Spacer(), - // actions go here + _CommentAction( + icon: Icons.more_horiz, + onPressed: _openMoreMenu, + tooltip: 'more', + ), + _CommentAction( + icon: saved ? Icons.bookmark : Icons.bookmark_border, + onPressed: () => _save(!saved), + tooltip: '${saved ? 'unsave' : 'save'} comment', + ), + _CommentAction( + icon: Icons.reply, + onPressed: _reply, + tooltip: 'reply', + ), + _CommentAction( + icon: Icons.arrow_upward, + onPressed: () => _vote(VoteType.up), + tooltip: 'upvote', + ), + _CommentAction( + icon: Icons.arrow_downward, + onPressed: () => _vote(VoteType.down), + tooltip: 'downvote', + ), ]) ], ), @@ -117,11 +167,11 @@ class Comment extends StatelessWidget { } } -class CommentTag extends StatelessWidget { +class _CommentTag extends StatelessWidget { final String text; final Color bgColor; - const CommentTag(this.text, this.bgColor); + const _CommentTag(this.text, this.bgColor); @override Widget build(BuildContext context) => Padding( @@ -134,10 +184,37 @@ class CommentTag extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 3, vertical: 2), child: Text(text, style: TextStyle( - color: Colors.white, + color: textColorBasedOnBackground(bgColor), fontSize: Theme.of(context).textTheme.bodyText1.fontSize - 5, fontWeight: FontWeight.w800, )), ), ); } + +class _CommentAction extends StatelessWidget { + final IconData icon; + final void Function() onPressed; + final String tooltip; + + const _CommentAction({ + Key key, + @required this.icon, + @required this.onPressed, + @required this.tooltip, + }) : super(key: key); + + @override + Widget build(BuildContext context) => IconButton( + constraints: BoxConstraints.tight(Size(32, 26)), + icon: Icon( + icon, + color: Theme.of(context).iconTheme.color.withAlpha(190), + ), + splashRadius: 25, + onPressed: onPressed, + iconSize: 22, + tooltip: tooltip, + padding: EdgeInsets.all(0), + ); +} diff --git a/lib/widgets/comment_section.dart b/lib/widgets/comment_section.dart index 3b73205..1b1b262 100644 --- a/lib/widgets/comment_section.dart +++ b/lib/widgets/comment_section.dart @@ -10,22 +10,84 @@ class CommentSection extends HookWidget { final List rawComments; final List comments; final int postCreatorId; + final CommentSortType sortType; - CommentSection(this.rawComments, {@required this.postCreatorId}) - : comments = CommentTree.fromList(rawComments), + CommentSection( + List rawComments, { + @required this.postCreatorId, + this.sortType = CommentSortType.hot, + }) : comments = + CommentTree.sortList(sortType, CommentTree.fromList(rawComments)), + rawComments = rawComments + ..sort((b, a) => a.published.compareTo(b.published)), assert(postCreatorId != null); @override - Widget build(BuildContext context) => Column(children: [ - // sorting menu goes here - if (comments.isEmpty) - Padding( - padding: EdgeInsets.symmetric(vertical: 50), - child: Text( - 'no comments yet', - style: TextStyle(fontStyle: FontStyle.italic), + Widget build(BuildContext context) { + var sorting = useState(sortType); + var rawComms = useState(rawComments); + var comms = useState(comments); + + void sortComments(CommentSortType sort) { + if (sort != sorting.value && sort != CommentSortType.chat) { + CommentTree.sortList(sort, comms.value); + } + + sorting.value = sort; + } + + return Column(children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: BoxDecoration( + border: Border.all(color: Colors.black45), + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: DropdownButton( + // TODO: change it to universal BottomModal + underline: Container(), + isDense: true, + onChanged: sortComments, + value: sorting.value, + items: [ + DropdownMenuItem( + child: Text('Hot'), value: CommentSortType.hot), + DropdownMenuItem( + child: Text('Top'), value: CommentSortType.top), + DropdownMenuItem( + child: Text('New'), value: CommentSortType.new_), + DropdownMenuItem( + child: Text('Old'), value: CommentSortType.old), + DropdownMenuItem( + child: Text('Chat'), value: CommentSortType.chat), + ], + ), ), + Spacer(), + ], + ), + ), + // sorting menu goes here + if (comments.isEmpty) + Padding( + padding: EdgeInsets.symmetric(vertical: 50), + child: Text( + 'no comments yet', + style: TextStyle(fontStyle: FontStyle.italic), ), - for (var com in comments) Comment(com, postCreatorId: postCreatorId), - ]); + ) + else if (sorting.value == CommentSortType.chat) + for (final com in rawComms.value) + Comment( + CommentTree(com), + postCreatorId: postCreatorId, + ) + else + for (var com in comms.value) Comment(com, postCreatorId: postCreatorId), + ]); + } } diff --git a/lib/widgets/post.dart b/lib/widgets/post.dart index 8609e9d..d629c57 100644 --- a/lib/widgets/post.dart +++ b/lib/widgets/post.dart @@ -6,6 +6,8 @@ import 'package:intl/intl.dart'; import 'package:lemmy_api_client/lemmy_api_client.dart'; import 'package:timeago/timeago.dart' as timeago; +import '../pages/full_post.dart'; +import '../url_launcher.dart'; import 'markdown_text.dart'; enum MediaType { @@ -28,11 +30,12 @@ MediaType whatType(String url) { class Post extends StatelessWidget { final PostView post; final String instanceUrl; + final bool fullPost; /// nullable final String postUrlDomain; - Post(this.post) + Post(this.post, {this.fullPost = false}) : instanceUrl = post.communityActorId.split('/')[2], postUrlDomain = post.url != null ? post.url.split('/')[2] : null; @@ -40,6 +43,7 @@ class Post extends StatelessWidget { void _openLink() { print('OPEN LINK'); + urlLauncher(post.url); } void _goToUser() { @@ -47,7 +51,8 @@ class Post extends StatelessWidget { } void _goToPost(BuildContext context) { - print('GO TO POST'); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => FullPostPage.fromPostView(post))); } void _goToCommunity() { @@ -173,9 +178,17 @@ class Post extends StatelessWidget { TextSpan( text: ''' 路 ${timeago.format(post.published, locale: 'en_short')}'''), + if (post.locked) TextSpan(text: ' 路 馃敀'), + if (post.stickied) TextSpan(text: ' 路 馃搶'), + if (post.nsfw) TextSpan(text: ' 路 '), + if (post.nsfw) + TextSpan( + text: 'NSFW', + style: TextStyle(color: Colors.red)), if (postUrlDomain != null) TextSpan(text: ' 路 $postUrlDomain'), - if (post.locked) TextSpan(text: ' 路 馃敀'), + if (post.removed) TextSpan(text: ' 路 REMOVED'), + if (post.deleted) TextSpan(text: ' 路 DELETED'), ], )) ]), @@ -183,14 +196,15 @@ class Post extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, ), Spacer(), - Column( - children: [ - IconButton( - onPressed: _showMoreMenu, - icon: Icon(Icons.more_vert), - ) - ], - ) + if (!fullPost) + Column( + children: [ + IconButton( + onPressed: _showMoreMenu, + icon: Icon(Icons.more_vert), + ) + ], + ) ]), ), ]); @@ -257,7 +271,9 @@ class Post extends StatelessWidget { onTap: _openLink, child: Container( decoration: BoxDecoration( - border: Border.all(width: 1), + border: Border.all( + width: 1, + color: Theme.of(context).iconTheme.color.withAlpha(170)), borderRadius: BorderRadius.circular(5)), child: Padding( padding: const EdgeInsets.all(10), @@ -314,15 +330,17 @@ class Post extends StatelessWidget { ), ), Spacer(), - IconButton( - icon: Icon(Icons.share), - onPressed: () => Share.text('Share post url', post.apId, - 'text/plain')), // TODO: find a way to mark it as url - IconButton( - icon: post.saved == true - ? Icon(Icons.bookmark) - : Icon(Icons.bookmark_border), - onPressed: _savePost), + if (!fullPost) + IconButton( + icon: Icon(Icons.share), + onPressed: () => Share.text('Share post url', post.apId, + 'text/plain')), // TODO: find a way to mark it as url + if (!fullPost) + IconButton( + icon: post.saved == true + ? Icon(Icons.bookmark) + : Icon(Icons.bookmark_border), + onPressed: _savePost), IconButton( icon: Icon(Icons.arrow_upward), onPressed: _upvotePost), Text(NumberFormat.compact().format(post.score)), @@ -339,7 +357,7 @@ class Post extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(20)), ), child: InkWell( - onTap: () => _goToPost(context), + onTap: fullPost ? null : () => _goToPost(context), child: Column( children: [ info(),